Compare commits

..

1 Commits

Author SHA1 Message Date
b4fb903bd0 Add recursive isEven() test 2024-07-22 20:03:49 -04:00
59 changed files with 1586 additions and 2090 deletions

View File

@ -175,7 +175,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
}
// deselect line tool
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.getByRole('button', { name: 'Line' }).click()
await page.waitForTimeout(500)
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
@ -203,7 +203,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
await expect(page.locator('.cm-cursor')).toHaveCount(2)
}
await page.getByRole('button', { name: 'Length: open menu' }).click()
await page.getByRole('button', { name: 'Constraints' }).click()
await page.getByRole('button', { name: 'Equal Length' }).click()
// Open the code pane.
@ -452,7 +452,7 @@ test.describe('Testing Camera Movement', () => {
// await expect(u.codeLocator).toHaveText(code)
// click the line button
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.getByRole('button', { name: 'Line' }).click()
const hoverOverNothing = async () => {
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
@ -1462,9 +1462,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
await page.mouse.click(clickCoords.x, clickCoords.y)
await page.waitForTimeout(300) // wait for animation
await expect(
page.getByRole('button', { name: 'Line', exact: true })
).toBeVisible()
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
// draw a line
const startXPx = 600
@ -1474,7 +1472,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
await expect(page.locator('.cm-content')).toHaveText(code)
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.getByRole('button', { name: 'Line' }).click()
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
@ -1513,8 +1511,6 @@ test.describe('Can create sketches on all planes and their back sides', () => {
})
test.describe('Copilot ghost text', () => {
test.skip(true, 'Needs to get covered again')
test('completes code in empty file', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -1553,9 +1549,7 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
})
test.skip('copilot disabled in sketch mode no select plane', async ({
page,
}) => {
test('copilot disabled in sketch mode no select plane', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
@ -2102,7 +2096,7 @@ test.describe('Testing settings', () => {
.hover()
await page
.getByRole('button', {
name: 'Roll back theme',
name: 'Roll back theme ; Has tooltip: Roll back to match default',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
@ -2154,7 +2148,7 @@ test.describe('Testing settings', () => {
.hover()
await page
.getByRole('button', {
name: 'Roll back theme',
name: 'Roll back theme ; Has tooltip: Roll back to match default',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
@ -2568,7 +2562,7 @@ test.describe('Testing selections', () => {
|> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.getByRole('button', { name: 'Line' }).click()
await u.closeDebugPanel()
const selectionSequence = async () => {
@ -2593,10 +2587,8 @@ test.describe('Testing selections', () => {
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
await topHorzSegmentClick()
await page.keyboard.down('Shift')
const constrainButton = page.getByRole('button', {
name: 'Length: open menu',
})
const absYButton = page.getByRole('button', { name: 'Absolute Y' })
const constrainButton = page.getByRole('button', { name: 'Constraints' })
const absYButton = page.getByRole('button', { name: 'ABS Y' })
await constrainButton.click()
await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
@ -3420,6 +3412,21 @@ const extrude001 = extrude(50, sketch001)
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeVisible()
// selecting an editable sketch but clicking "start sketch" should start a new sketch and not edit the existing one
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(200)
await page.getByTestId('KCL Code').click()
await page.waitForTimeout(200)
await page.mouse.click(734, 134)
await page.waitForTimeout(100)
await page.getByTestId('KCL Code').click()
// expect main content to contain `sketch005` i.e. started a new sketch
await page.waitForTimeout(300)
await expect(page.locator('.cm-content')).toHaveText(
/sketch001 = startSketchOn\('XZ'\)/
)
})
test('Deselecting line tool should mean nothing happens on click', async ({
@ -3456,7 +3463,7 @@ const extrude001 = extrude(50, sketch001)
let previousCodeContent = await page.locator('.cm-content').innerText()
// deselect the line tool by clicking it
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.getByRole('button', { name: 'Line' }).click()
await page.mouse.click(700, 200)
await page.waitForTimeout(100)
@ -3469,7 +3476,7 @@ const extrude001 = extrude(50, sketch001)
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
// select line tool again
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.getByRole('button', { name: 'Line' }).click()
await u.closeDebugPanel()
@ -3802,24 +3809,13 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' })
const rectangleToolCommand = page.getByRole('option', {
name: 'rectangle',
})
const rectangleToolButton = page.getByRole('button', {
name: 'Corner rectangle',
exact: true,
})
const lineToolCommand = page.getByRole('option', {
name: 'Line',
})
const lineToolButton = page.getByRole('button', {
name: 'Line',
exact: true,
name: 'Rectangle',
})
const rectangleToolButton = page.getByRole('button', { name: 'Rectangle' })
const lineToolCommand = page.getByRole('option', { name: 'Line' })
const lineToolButton = page.getByRole('button', { name: 'Line' })
const arcToolCommand = page.getByRole('option', { name: 'Tangential Arc' })
const arcToolButton = page.getByRole('button', {
name: 'Tangential Arc',
exact: true,
})
const arcToolButton = page.getByRole('button', { name: 'Tangential Arc' })
// Start a sketch
await sketchButton.click()
@ -3866,7 +3862,10 @@ test.describe('Regression tests', () => {
await u.waitForAuthSkipAppStart()
// expand variables section
const variablesTabButton = page.getByTestId('variables-pane-button')
const variablesTabButton = page.getByRole('tab', {
name: 'Variables',
exact: false,
})
await variablesTabButton.click()
// can find sketch001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
@ -3891,7 +3890,10 @@ test.describe('Regression tests', () => {
await u.waitForAuthSkipAppStart()
const variablesTabButton = page.getByTestId('variables-pane-button')
const variablesTabButton = page.getByRole('tab', {
name: 'Variables',
exact: false,
})
await variablesTabButton.click()
// expect to see "myVar:5"
await expect(
@ -4152,7 +4154,7 @@ test.describe('Sketch tests', () => {
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.getByRole('button', { name: 'Line' }).click()
await page.waitForTimeout(100)
await page.mouse.click(700, 200)
@ -4179,7 +4181,9 @@ test.describe('Sketch tests', () => {
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(page.getByText('select a plane or face')).toBeVisible()
await expect(
page.getByText('click plane or face to sketch on')
).toBeVisible()
await page.keyboard.press('Escape')
await expect(
@ -4730,7 +4734,7 @@ test.describe('Sketch tests', () => {
await expect(page.locator('.cm-content')).toHaveText(code)
// Assert the tool was unequipped
await expect(
page.getByRole('button', { name: 'Line', exact: true })
page.getByRole('button', { name: 'Line' })
).not.toHaveAttribute('aria-pressed', 'true')
// exit sketch
@ -4892,7 +4896,8 @@ test.describe('Testing constraints', () => {
await page.mouse.click(834, 244)
await page.keyboard.up('Shift')
await page.getByRole('button', { name: 'Length', exact: true }).click()
await page.getByRole('button', { name: 'Constraints', exact: true }).click()
await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByText('Add constraining value').click()
await expect(page.locator('.cm-content')).toHaveText(
@ -4946,10 +4951,12 @@ const part002 = startSketchOn('XZ')
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
await page
.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
.click()
await page.getByRole('button', { name: 'remove constraints' }).click()
await page
.getByRole('button', { name: 'remove constraints', exact: true })
.click()
await page.getByText('line([39.13, 68.63], %)').click()
const activeLinesContent = await page.locator('.cm-activeLine').all()
@ -5010,11 +5017,11 @@ const part002 = startSketchOn('XZ')
await page.keyboard.up('Shift')
await page
.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
.click()
await page
.getByRole('button', { name: 'Perpendicular Distance' })
.getByRole('button', { name: 'perpendicular distance', exact: true })
.click()
const createNewVariableCheckbox = page.getByTestId(
@ -5109,10 +5116,12 @@ const part002 = startSketchOn('XZ')
await page.keyboard.up('Shift')
await page
.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
.click()
await page.getByRole('button', { name: constraint }).click()
await page
.getByRole('button', { name: constraint, exact: true })
.click()
const createNewVariableCheckbox = page.getByTestId(
'create-new-variable-checkbox'
@ -5153,25 +5162,25 @@ const part002 = startSketchOn('XZ')
{
testName: 'Add variable',
addVariable: true,
constraint: 'Absolute X',
constraint: 'ABS X',
value: 'xDis001, 61.34',
},
{
testName: 'No variable',
addVariable: false,
constraint: 'Absolute X',
constraint: 'ABS X',
value: '154.9, 61.34',
},
{
testName: 'Add variable',
addVariable: true,
constraint: 'Absolute Y',
constraint: 'ABS Y',
value: '154.9, yDis001',
},
{
testName: 'No variable',
addVariable: false,
constraint: 'Absolute Y',
constraint: 'ABS Y',
value: '154.9, 61.34',
},
] as const
@ -5207,7 +5216,7 @@ const part002 = startSketchOn('XZ')
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
])
if (constraint === 'Absolute X') {
if (constraint === 'ABS X') {
await page.mouse.click(600, 130)
} else {
await page.mouse.click(900, 250)
@ -5218,7 +5227,7 @@ const part002 = startSketchOn('XZ')
await page.keyboard.up('Shift')
await page
.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
.click()
await page
@ -5326,10 +5335,10 @@ const part002 = startSketchOn('XZ')
await page.keyboard.up('Shift')
await page
.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
.click()
await page.getByTestId('dropdown-constraint-angle').click()
await page.getByTestId('angle').click()
const createNewVariableCheckbox = page.getByTestId(
'create-new-variable-checkbox'
@ -5427,10 +5436,10 @@ const part002 = startSketchOn('XZ')
await page.mouse.click(line3.x, line3.y)
await page
.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
.click()
await page.getByTestId('dropdown-constraint-' + constraint).click()
await page.getByTestId(constraint).click()
if (!addVariable) {
await page.getByTestId('create-new-variable-checkbox').click()
@ -5518,7 +5527,7 @@ const part002 = startSketchOn('XZ')
await expect(activeLinesContent).toHaveLength(codeAfter.length)
const constraintMenuButton = page.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
const constraintButton = page
.getByRole('button', {
@ -5601,7 +5610,7 @@ const part002 = startSketchOn('XZ')
await page.mouse.click(line3.x - 3, line3.y + 20)
await page.keyboard.up('Shift')
const constraintMenuButton = page.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
const constraintButton = page.getByRole('button', {
name: constraintName,
@ -5678,7 +5687,7 @@ const part002 = startSketchOn('XZ')
await page.mouse.click(axisClick.x, axisClick.y)
await page.keyboard.up('Shift')
const constraintMenuButton = page.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
const constraintButton = page.getByRole('button', {
name: constraintName,
@ -5737,10 +5746,10 @@ const part002 = startSketchOn('XZ')
await page
.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
.click()
await page.getByRole('button', { name: 'Horizontal', exact: true }).click()
await page.getByRole('button', { name: 'horizontal', exact: true }).click()
let activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(3.13, %)`)
@ -5761,13 +5770,13 @@ const part002 = startSketchOn('XZ')
await page.waitForTimeout(300)
await page
.getByRole('button', {
name: 'Length: open menu',
name: 'Constraints',
})
.click()
// await expect(page.getByRole('button', { name: 'length', exact: true })).toBeVisible()
await page.waitForTimeout(200)
// await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByTestId('dropdown-constraint-length').click()
await page.locator('[data-testid="length"]').click()
await page.getByLabel('length Value').fill('10')
await page.getByRole('button', { name: 'Add constraining value' }).click()
@ -7059,8 +7068,6 @@ test.describe('Test network and connection issues', () => {
await u.waitForAuthSkipAppStart()
const networkToggle = page.getByTestId('network-toggle')
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -7074,7 +7081,7 @@ test.describe('Test network and connection issues', () => {
await expect(networkPopover).not.toBeVisible()
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
// Click the network widget
await networkWidget.click()
@ -7096,7 +7103,7 @@ test.describe('Test network and connection issues', () => {
})
// Expect the network to be down
await expect(networkToggle).toContainText('Offline')
await expect(page.getByText('Network Health (Offline)')).toBeVisible()
// Click the network widget
await networkWidget.click()
@ -7122,7 +7129,7 @@ test.describe('Test network and connection issues', () => {
).not.toBeDisabled({ timeout: 15000 })
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
})
test('Engine disconnect & reconnect in sketch mode', async ({
@ -7134,8 +7141,6 @@ test.describe('Test network and connection issues', () => {
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -7178,7 +7183,7 @@ test.describe('Test network and connection issues', () => {
|> line([${commonPoints.num1}, 0], %)`)
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
// simulate network down
await u.emulateNetworkConditions({
@ -7190,7 +7195,7 @@ test.describe('Test network and connection issues', () => {
})
// Expect the network to be down
await expect(networkToggle).toContainText('Offline')
await expect(page.getByText('Network Health (Offline)')).toBeVisible()
// Ensure we are not in sketch mode
await expect(
@ -7215,7 +7220,7 @@ test.describe('Test network and connection issues', () => {
).not.toBeDisabled({ timeout: 15000 })
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
// Click off the code pane.
@ -7232,7 +7237,7 @@ test.describe('Test network and connection issues', () => {
await page.waitForTimeout(150)
// Click the line tool
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.getByRole('button', { name: 'Line' }).click()
await page.waitForTimeout(150)
@ -7259,7 +7264,7 @@ test.describe('Test network and connection issues', () => {
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'Line', exact: true })
page.getByRole('button', { name: 'Line' })
).not.toHaveAttribute('aria-pressed', 'true')
// Exit sketch
@ -7664,7 +7669,7 @@ test('Keyboard shortcuts can be viewed through the help menu', async ({
.waitFor({ state: 'visible' })
// Open the help menu
await page.getByRole('button', { name: 'Help and resources' }).click()
await page.getByRole('button', { name: 'Help', exact: false }).click()
// Open the keyboard shortcuts
await page.getByRole('button', { name: 'Keyboard Shortcuts' }).click()
@ -7688,11 +7693,8 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const lineButton = page.getByRole('button', { name: 'Line', exact: true })
const arcButton = page.getByRole('button', {
name: 'Tangential Arc',
exact: true,
})
const lineButton = page.getByRole('button', { name: 'Line' })
const arcButton = page.getByRole('button', { name: 'Tangential Arc' })
// Test these hotkeys perform actions when
// focus is on the canvas
@ -7704,7 +7706,6 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
await page.mouse.move(800, 300)
await page.mouse.click(800, 300)
await page.waitForTimeout(1000)
await expect(lineButton).toBeVisible()
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
// Draw a line
@ -7774,12 +7775,9 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
await u.closeDebugPanel()
const codePane = page.getByRole('textbox').locator('div')
const codePaneButton = page.getByTestId('code-pane-button')
const lineButton = page.getByRole('button', { name: 'Line', exact: true })
const arcButton = page.getByRole('button', {
name: 'Tangential Arc',
exact: true,
})
const codePaneButton = page.getByRole('tab', { name: 'KCL Code' })
const lineButton = page.getByRole('button', { name: 'Line' })
const arcButton = page.getByRole('button', { name: 'Tangential Arc' })
const extrudeButton = page.getByRole('button', { name: 'Extrude' })
// Test that the hotkeys do nothing when
@ -7800,7 +7798,7 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
await page.mouse.click(600, 250)
// work-around: to stop "keyboard.press('s')" from typing in the editor even when it should be blurred
await page.getByRole('button', { name: 'Commands' }).click()
await page.getByRole('button', { name: 'Commands ⌘K' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Escape')
await page.waitForTimeout(100)

View File

@ -431,9 +431,7 @@ test('Draft segments should look right', async ({ page, context }) => {
|> line([7.25, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(code)
await page
.getByRole('button', { name: 'Tangential Arc', exact: true })
.click()
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
@ -477,10 +475,8 @@ test('Draft rectangles should look right', async ({ page, context }) => {
const startXPx = 600
// Equip the rectangle tool
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page
.getByRole('button', { name: 'Corner rectangle', exact: true })
.click()
await page.getByRole('button', { name: 'Line' }).click()
await page.getByRole('button', { name: 'Rectangle' }).click()
// Draw the rectangle
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 30)
@ -539,9 +535,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|> line([7.25, 0], %)`
await expect(u.codeLocator).toHaveText(code)
await page
.getByRole('button', { name: 'Tangential Arc', exact: true })
.click()
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
@ -551,9 +545,7 @@ test.describe('Client side scene scale should match engine scale', () => {
await expect(u.codeLocator).toHaveText(code)
// click tangential arc tool again to unequip it
await page
.getByRole('button', { name: 'Tangential Arc', exact: true })
.click()
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch
@ -642,9 +634,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|> line([184.3, 0], %)`
await expect(u.codeLocator).toHaveText(code)
await page
.getByRole('button', { name: 'Tangential Arc', exact: true })
.click()
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
@ -653,9 +643,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|> tangentialArcTo([551.2, -62.01], %)`
await expect(u.codeLocator).toHaveText(code)
await page
.getByRole('button', { name: 'Tangential Arc', exact: true })
.click()
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100)
// screen shot should show the sketch

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 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: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -21,7 +21,7 @@ async function waitForPageLoad(page: Page) {
timeout: 20_000,
})
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeEnabled({
await expect(page.getByTestId('start-sketch')).toBeEnabled({
timeout: 20_000,
})
}
@ -58,45 +58,44 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
}
async function openKclCodePanel(page: Page) {
const paneLocator = page.getByTestId('code-pane-button')
const ariaSelected = await paneLocator?.getAttribute('aria-pressed')
const isOpen = ariaSelected === 'true'
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
if (!isOpen) {
await paneLocator.click()
await expect(paneLocator).toHaveAttribute('aria-pressed', 'true')
await paneLocator.and(page.locator('[aria-selected="true"]')).waitFor()
}
}
async function closeKclCodePanel(page: Page) {
const paneLocator = page.getByTestId('code-pane-button')
const ariaSelected = await paneLocator?.getAttribute('aria-pressed')
const isOpen = ariaSelected === 'true'
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
if (isOpen) {
await paneLocator.click()
await expect(paneLocator).not.toHaveAttribute('aria-pressed', 'true')
await paneLocator
.and(page.locator(':not([aria-selected="true"])'))
.waitFor()
}
}
async function openDebugPanel(page: Page) {
const debugLocator = page.getByTestId('debug-pane-button')
await expect(debugLocator).toBeVisible()
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
if (!isOpen) {
await debugLocator.click()
await expect(debugLocator).toHaveAttribute('aria-pressed', 'true')
await debugLocator.and(page.locator('[aria-selected="true"]')).waitFor()
}
}
async function closeDebugPanel(page: Page) {
const debugLocator = page.getByTestId('debug-pane-button')
await expect(debugLocator).toBeVisible()
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
if (isOpen) {
await debugLocator.click()
await expect(debugLocator).not.toHaveAttribute('aria-pressed', 'true')
await debugLocator
.and(page.locator(':not([aria-selected="true"])'))
.waitFor()
}
}
@ -472,11 +471,10 @@ export const doExport = async (
page: Page
): Promise<Paths> => {
await page.getByRole('button', { name: APP_NAME }).click()
const exportMenuButton = page.getByRole('button', {
name: 'Export current part',
})
await expect(exportMenuButton).toBeVisible()
await exportMenuButton.click()
await expect(
page.getByRole('button', { name: 'Export', exact: false })
).toBeVisible()
await page.getByRole('button', { name: 'Export', exact: false }).click()
await expect(page.getByTestId('command-bar')).toBeVisible()
// Go through export via command bar

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.24.4",
"version": "0.24.3",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",

View File

@ -17,7 +17,6 @@ import type {
PluginSpec,
ViewPlugin,
} from '@codemirror/view'
import { setDiagnosticsEffect } from '@codemirror/lint'
import { EditorView, Tooltip } from '@codemirror/view'
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
@ -215,21 +214,6 @@ export class LanguageServerPlugin implements PluginValue {
}
if (!this.client.ready) return
// TODO(paultag): This is the *wrong* place for this to live.
//
// We need to clear diagnostics before updating the code, because
// if the code shrinks, the errors/diagnostics can go out of range
// of the source code, which cause an exception, breaking all the
// things.
//
// We need some sort of clear diagnostics boolean on the editor
// and we can drop this.
this.view.dispatch({
effects: [setDiagnosticsEffect.of([])],
annotations: [],
})
try {
// Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({

View File

@ -80,5 +80,5 @@
}
},
"productName": "Zoo Modeling App",
"version": "0.24.4"
"version": "0.24.3"
}

View File

@ -1,4 +1,4 @@
import { useRef, useMemo, memo } from 'react'
import { WheelEvent, useRef, useMemo } from 'react'
import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
@ -12,14 +12,11 @@ import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from 'components/Tooltip'
import { useAppState } from 'AppState'
import { CustomIcon } from 'components/CustomIcon'
import {
toolbarConfig,
ToolbarItem,
ToolbarItemCallbackProps,
ToolbarItemResolved,
ToolbarModeName,
} from 'lib/toolbar'
canRectangleTool,
isEditingExistingSketch,
} from 'machines/modelingMachine'
import { DEV } from 'env'
export function Toolbar({
className = '',
@ -28,14 +25,12 @@ export function Toolbar({
const { state, send, context } = useModelingContext()
const { commandBarSend } = useCommandsContext()
const iconClassName =
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
const bgClassName = '!bg-transparent'
const buttonBgClassName =
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
const buttonBorderClassName =
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const sketchPathId = useMemo(() => {
'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-primary dark:group-enabled:group-hover:!text-inherit group-pressed:!text-chalkboard-10 group-ui-open:!text-chalkboard-10 dark:group-ui-open:!text-chalkboard-10'
const bgClassName =
'group-disabled:!bg-transparent group-enabled:group-hover:bg-primary/10 dark:group-enabled:group-hover:bg-primary group-pressed:bg-primary group-ui-open:bg-primary'
const buttonClassName =
'bg-chalkboard-10 dark:bg-chalkboard-100 enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!border-primary ui-open:!border-primary'
const pathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
return false
}
@ -56,292 +51,401 @@ export function Toolbar({
isExecuting ||
!isStreamReady
const currentMode =
(Object.entries(toolbarConfig).find(([_, mode]) =>
mode.check(state)
)?.[0] as ToolbarModeName) || 'modeling'
/** These are the props that will be passed to the callbacks in the toolbar config
* They are memoized to prevent unnecessary re-renders,
* but they still get a lot of churn from the state machine
* so I think there's a lot of room for improvement here
*/
const configCallbackProps: ToolbarItemCallbackProps = useMemo(
() => ({
modelingStateMatches: state.matches,
modelingSend: send,
commandBarSend,
sketchPathId,
const disableLineButton =
state.matches('Sketch.Rectangle tool.Awaiting second corner') ||
disableAllButtons
useHotkeys(
'l',
() =>
state.matches('Sketch.Line tool')
? send('CancelSketch')
: send({
type: 'change tool',
data: { tool: 'line' },
}),
[state.matches, send, commandBarSend, sketchPathId]
{ enabled: !disableLineButton, scopes: ['sketch'] }
)
const disableTangentialArc =
(!isEditingExistingSketch(context) &&
!state.matches('Sketch.Tangential arc to')) ||
disableAllButtons
useHotkeys(
'a',
() =>
state.matches('Sketch.Tangential arc to')
? send('CancelSketch')
: send({
type: 'change tool',
data: { tool: 'tangentialArc' },
}),
{ enabled: !disableTangentialArc, scopes: ['sketch'] }
)
const disableRectangle =
(!canRectangleTool(context) && !state.matches('Sketch.Rectangle tool')) ||
disableAllButtons
useHotkeys(
'r',
() =>
state.matches('Sketch.Rectangle tool')
? send('CancelSketch')
: send({
type: 'change tool',
data: { tool: 'rectangle' },
}),
{ enabled: !disableRectangle, scopes: ['sketch'] }
)
useHotkeys(
's',
() =>
state.nextEvents.includes('Enter sketch') && pathId
? send({ type: 'Enter sketch' })
: send({ type: 'Enter sketch', data: { forceNewSketch: true } }),
{ enabled: !disableAllButtons, scopes: ['modeling'] }
)
useHotkeys(
'esc',
() =>
['Sketch no face', 'Sketch.SketchIdle'].some(state.matches)
? send('Cancel')
: send('CancelSketch'),
{ enabled: !disableAllButtons, scopes: ['sketch'] }
)
useHotkeys(
'e',
() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Extrude', groupId: 'modeling' },
}),
{ enabled: !disableAllButtons, scopes: ['modeling'] }
)
const disableFillet = !state.can('Fillet') || disableAllButtons
useHotkeys(
'f',
() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
}),
{ enabled: !disableFillet, scopes: ['modeling'] }
)
/**
* Resolve all the callbacks and values for the current mode,
* so we don't need to worry about the other modes
*/
const currentModeItems: (
| ToolbarItemResolved
| ToolbarItemResolved[]
| 'break'
)[] = useMemo(() => {
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
if (maybeIconConfig === 'break') {
return 'break'
} else if (Array.isArray(maybeIconConfig)) {
return maybeIconConfig.map(resolveItemConfig)
} else {
return resolveItemConfig(maybeIconConfig)
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current
if (!span) {
return
}
span.scrollLeft = span.scrollLeft += ev.deltaY
}
const nextEvents = useMemo(() => state.nextEvents, [state.nextEvents])
const splitMenuItems = useMemo(
() =>
nextEvents
.filter(
(eventName) =>
eventName.includes('Make segment') ||
eventName.includes('Constrain')
)
.sort((a, b) => {
const aisEnabled = nextEvents
.filter((event) => state.can(event as any))
.includes(a)
const bIsEnabled = nextEvents
.filter((event) => state.can(event as any))
.includes(b)
if (aisEnabled && !bIsEnabled) {
return -1
}
if (!aisEnabled && bIsEnabled) {
return 1
}
return 0
})
function resolveItemConfig(
maybeIconConfig: ToolbarItem
): ToolbarItemResolved {
return {
...maybeIconConfig,
title:
typeof maybeIconConfig.title === 'string'
? maybeIconConfig.title
: maybeIconConfig.title(configCallbackProps),
description: maybeIconConfig.description,
links: maybeIconConfig.links || [],
isActive: maybeIconConfig.isActive?.(state),
hotkey:
typeof maybeIconConfig.hotkey === 'string'
? maybeIconConfig.hotkey
: maybeIconConfig.hotkey?.(state),
.map((eventName) => ({
label: eventName
.replace('Make segment ', '')
.replace('Constrain ', ''),
onClick: () => send(eventName),
disabled:
disableAllButtons ||
maybeIconConfig.status !== 'available' ||
maybeIconConfig.disabled?.(state) === true,
disableHotkey: maybeIconConfig.disableHotkey?.(state),
status: maybeIconConfig.status,
}
}
}, [currentMode, disableAllButtons, configCallbackProps])
!nextEvents
.filter((event) => state.can(event as any))
.includes(eventName) || disableAllButtons,
})),
[JSON.stringify(nextEvents), state]
)
return (
<menu className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-20 dark:border-chalkboard-80 border-t-0 shadow-sm">
<menu className="max-w-full whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
<ul
{...props}
ref={toolbarButtonsRef}
className={
'has-[[aria-expanded=true]]:!pointer-events-none m-0 py-1 rounded-l-sm flex gap-1.5 items-center ' +
className
}
>
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
{currentModeItems.map((maybeIconConfig, i) => {
if (maybeIconConfig === 'break') {
return (
<div
key={'break-' + i}
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
/>
)
} else if (Array.isArray(maybeIconConfig)) {
return (
<ActionButtonDropdown
Element="button"
key={maybeIconConfig[0].id}
data-testid={maybeIconConfig[0].id + '-dropdown'}
id={maybeIconConfig[0].id + '-dropdown'}
name={maybeIconConfig[0].title}
className={
'group/wrapper ' +
buttonBorderClassName +
' !bg-transparent relative group !gap-0'
}
splitMenuItems={maybeIconConfig.map((itemConfig) => ({
id: itemConfig.id,
label: itemConfig.title,
hotkey: itemConfig.hotkey,
onClick: () => itemConfig.onClick(configCallbackProps),
disabled:
disableAllButtons ||
itemConfig.status !== 'available' ||
itemConfig.disabled === true,
status: itemConfig.status,
}))}
onWheel={handleToolbarButtonsWheelEvent}
className={'m-0 py-1 rounded-l-sm flex gap-2 items-center ' + className}
style={{ scrollbarWidth: 'thin' }}
>
{nextEvents.includes('Enter sketch') && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
id={maybeIconConfig[0].id}
data-testid={maybeIconConfig[0].id}
iconStart={{
icon: maybeIconConfig[0].icon,
className: iconClassName,
bgClassName: bgClassName,
}}
className={
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
buttonBgClassName
}
aria-pressed={maybeIconConfig[0].isActive}
disabled={
disableAllButtons ||
maybeIconConfig[0].status !== 'available' ||
maybeIconConfig[0].disabled
}
name={maybeIconConfig[0].title}
aria-description={maybeIconConfig[0].description}
onClick={() =>
maybeIconConfig[0].onClick(configCallbackProps)
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
}
>
<ToolbarItemContents
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
</ActionButton>
</ActionButtonDropdown>
)
}
const itemConfig = maybeIconConfig
return (
<ActionButton
Element="button"
key={itemConfig.id}
id={itemConfig.id}
data-testid={itemConfig.id}
iconStart={{
icon: itemConfig.icon,
className: iconClassName,
bgClassName: bgClassName,
icon: 'sketch',
iconClassName,
bgClassName,
}}
className={
'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
buttonBorderClassName +
' ' +
buttonBgClassName +
(!itemConfig.showTitle ? ' !px-0' : '')
}
name={itemConfig.title}
aria-description={itemConfig.description}
aria-pressed={itemConfig.isActive}
disabled={
disableAllButtons ||
itemConfig.status !== 'available' ||
itemConfig.disabled
}
onClick={() => itemConfig.onClick(configCallbackProps)}
disabled={disableAllButtons}
>
<ToolbarItemContents
itemConfig={itemConfig}
configCallbackProps={configCallbackProps}
/>
<span data-testid="start-sketch">Start Sketch</span>
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: S
</Tooltip>
</ActionButton>
)
})}
</ul>
{state.matches('Sketch no face') && (
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 py-1 px-2 bg-chalkboard-10 dark:bg-chalkboard-90 border border-chalkboard-20 dark:border-chalkboard-80 rounded shadow-lg">
<p className="text-xs">Select a plane or face to start sketching</p>
</div>
</li>
)}
{nextEvents.includes('Enter sketch') && pathId && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() => send({ type: 'Enter sketch' })}
iconStart={{
icon: 'sketch',
iconClassName,
bgClassName,
}}
disabled={disableAllButtons}
>
Edit Sketch
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: S
</Tooltip>
</ActionButton>
</li>
)}
{nextEvents.includes('Cancel') && !state.matches('idle') && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() => send({ type: 'Cancel' })}
iconStart={{
icon: 'arrowLeft',
iconClassName,
bgClassName,
}}
disabled={disableAllButtons}
>
Exit Sketch
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: Esc
</Tooltip>
</ActionButton>
</li>
)}
{state.matches('Sketch no face') && (
<li className="contents">
<div className="mx-2 text-sm">click plane or face to sketch on</div>
</li>
)}
{state.matches('Sketch') && !state.matches('idle') && (
<>
<li className="contents" key="line-button">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
state?.matches('Sketch.Line tool')
? send('CancelSketch')
: send({
type: 'change tool',
data: { tool: 'line' },
})
}
aria-pressed={state?.matches('Sketch.Line tool')}
iconStart={{
icon: 'line',
iconClassName,
bgClassName,
}}
disabled={disableLineButton}
>
Line
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: L
</Tooltip>
</ActionButton>
</li>
<li className="contents" key="tangential-arc-button">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
state.matches('Sketch.Tangential arc to')
? send('CancelSketch')
: send({
type: 'change tool',
data: { tool: 'tangentialArc' },
})
}
aria-pressed={state.matches('Sketch.Tangential arc to')}
iconStart={{
icon: 'arc',
iconClassName,
bgClassName,
}}
disabled={disableTangentialArc}
>
Tangential Arc
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: A
</Tooltip>
</ActionButton>
</li>
<li className="contents" key="rectangle-button">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
state.matches('Sketch.Rectangle tool')
? send('CancelSketch')
: send({
type: 'change tool',
data: { tool: 'rectangle' },
})
}
aria-pressed={state.matches('Sketch.Rectangle tool')}
iconStart={{
icon: 'rectangle',
iconClassName,
bgClassName,
}}
disabled={disableRectangle}
title={
canRectangleTool(context)
? 'Rectangle'
: 'Can only be used when a sketch is empty currently'
}
>
Rectangle
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: R
</Tooltip>
</ActionButton>
</li>
</>
)}
{state.matches('Sketch.SketchIdle') &&
nextEvents.filter(
(eventName) =>
eventName.includes('Make segment') ||
eventName.includes('Constrain')
).length > 0 && (
<ActionButtonDropdown
splitMenuItems={splitMenuItems}
className={buttonClassName}
Element="button"
iconStart={{
icon: 'dimension',
iconClassName,
bgClassName,
}}
>
Constraints
</ActionButtonDropdown>
)}
{state.matches('idle') && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Extrude', groupId: 'modeling' },
})
}
disabled={!state.can('Extrude') || disableAllButtons}
title={
state.can('Extrude')
? 'extrude'
: 'sketches need to be closed, or not already extruded'
}
iconStart={{
icon: 'extrude',
iconClassName,
bgClassName,
}}
>
Extrude
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: E
</Tooltip>
</ActionButton>
</li>
)}
{state.matches('idle') && (DEV || (window as any)._enableFillet) && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
})
}
disabled={disableFillet}
title={disableFillet ? 'fillet' : "edge can't be filleted"}
iconStart={{
icon: 'fillet', // todo: add fillet icon
iconClassName,
bgClassName,
}}
>
Fillet
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: F
</Tooltip>
</ActionButton>
</li>
)}
</ul>
</menu>
)
}
/**
* The single button and dropdown button share content, so we extract it here
* It contains a tooltip with the title, description, and links
* and a hotkey listener
*/
const ToolbarItemContents = memo(function ToolbarItemContents({
itemConfig,
configCallbackProps,
}: {
itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps
}) {
useHotkeys(
itemConfig.hotkey || '',
() => {
itemConfig.onClick(configCallbackProps)
},
{
enabled:
itemConfig.status === 'available' &&
!!itemConfig.hotkey &&
!itemConfig.disabled &&
!itemConfig.disableHotkey,
}
)
return (
<>
<span className={!itemConfig.showTitle ? 'sr-only' : ''}>
{itemConfig.title}
</span>
<Tooltip
position="bottom"
wrapperClassName="!p-4 !pointer-events-auto"
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
>
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
<span
className={`text-sm flex-1 ${
itemConfig.status !== 'available'
? 'text-chalkboard-70 dark:text-chalkboard-40'
: ''
}`}
>
{itemConfig.title}
</span>
{itemConfig.status === 'available' && itemConfig.hotkey ? (
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
) : itemConfig.status === 'kcl-only' ? (
<>
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
KCL code only
</span>
<CustomIcon
name="code"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
) : (
itemConfig.status === 'unavailable' && (
<>
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
In development
</span>
<CustomIcon
name="lockClosed"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
)
)}
</div>
<p className="px-2 text-ch font-sans">{itemConfig.description}</p>
{itemConfig.links.length > 0 && (
<>
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
<ul className="p-0 px-1 m-0 flex flex-col">
{itemConfig.links.map((link) => (
<li key={link.label} className="contents">
<a
href={link.url}
target="_blank"
rel="noreferrer"
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
onClickCapture={(e) =>
e.nativeEvent.stopImmediatePropagation()
}
>
<span className="flex-1">Open {link.label}</span>
<CustomIcon name="link" className="w-4 h-4" />
</a>
</li>
))}
</ul>
</>
)}
</Tooltip>
</>
)
})

View File

@ -2022,17 +2022,13 @@ export async function getFaceDetails(
entity_id: entityId,
},
})
const resp = await engineCommandManager.sendSceneCommand({
const faceInfo: Models['GetSketchModePlane_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'get_sketch_mode_plane' },
})
const faceInfo =
resp?.success &&
resp?.resp.type === 'modeling' &&
resp?.resp?.data?.modeling_response?.type === 'get_sketch_mode_plane'
? resp?.resp?.data?.modeling_response.data
: ({} as Models['GetSketchModePlane_type'])
)?.data?.data
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),

View File

@ -1,93 +1,56 @@
import { Popover } from '@headlessui/react'
import { ActionButtonProps } from './ActionButton'
import { CustomIcon } from './CustomIcon'
import { ActionButton, ActionButtonProps } from './ActionButton'
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
name?: string
type ActionButtonSplitProps = Omit<ActionButtonProps, 'iconEnd'> & {
splitMenuItems: {
id: string
label: string
shortcut?: string
onClick: () => void
disabled?: boolean
status?: 'available' | 'unavailable' | 'kcl-only'
}[]
}
export function ActionButtonDropdown({
splitMenuItems,
className,
children,
...props
}: ActionButtonSplitProps) {
const baseClassNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10`
return (
<Popover className={`${baseClassNames} ${className}`}>
{({ close }) => (
<>
{children}
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
<CustomIcon
name="caretDown"
className={
'w-3.5 h-5 text-chalkboard-70 dark:text-chalkboard-40 rounded-none ' +
'ui-open:rotate-180 ui-open:!text-chalkboard-10'
}
<Popover className="relative">
<Popover.Button
as={ActionButton}
className={className}
{...props}
Element="button"
iconEnd={{
icon: 'caretDown',
className: 'ui-open:rotate-180',
bgClassName:
'bg-chalkboard-20 dark:bg-chalkboard-80 ui-open:bg-primary ui-open:text-chalkboard-10',
}}
/>
<span className="sr-only">
{props.name ? props.name + ': ' : ''}open menu
</span>
</Popover.Button>
<Popover.Panel
as="ul"
className="!pointer-events-auto absolute z-20 left-1/2 -translate-x-1/2 top-full mt-4 w-fit max-w-[280px] max-h-[80vh] overflow-y-auto py-2 flex flex-col align-stretch text-inherit dark:text-chalkboard-10 bg-chalkboard-10 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-30 dark:border-chalkboard-80 text-sm m-0 p-0"
className="absolute z-20 left-1/2 -translate-x-1/2 top-full mt-1 w-fit max-h-[80vh] overflow-y-auto py-2 flex flex-col gap-1 align-stretch text-inherit dark:text-chalkboard-10 bg-chalkboard-10 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-30 dark:border-chalkboard-80 text-sm m-0 p-0"
>
{splitMenuItems.map((item) => (
<li className="contents" key={item.label}>
<button
onClick={() => {
item.onClick()
// Close the popover
close()
}}
className="group/button flex items-center gap-6 px-3 py-1 font-sans text-xs hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
onClick={item.onClick}
className="block px-3 py-1 hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 text-sm w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
disabled={item.disabled}
data-testid={'dropdown-' + item.id}
data-testid={item.label}
>
<span className="capitalize flex-grow text-left">
{item.label}
</span>
{item.status === 'unavailable' ? (
<div className="flex flex-none items-center gap-1">
<span className="text-chalkboard-70 dark:text-chalkboard-40">
In development
</span>
<CustomIcon
name="lockClosed"
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40"
/>
</div>
) : item.status === 'kcl-only' ? (
<div className="flex flex-none items-center gap-1">
<span className="text-chalkboard-70 dark:text-chalkboard-40">
KCL code only
</span>
<CustomIcon
name="code"
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40"
/>
</div>
) : item.shortcut ? (
<kbd className="hotkey flex-none group-disabled/button:text-chalkboard-50 dark:group-disabled/button:text-chalkboard-70 group-disabled/button:border-chalkboard-20 dark:group-disabled/button:border-chalkboard-80">
<span className="capitalize">{item.label}</span>
{item.shortcut && (
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90">
{item.shortcut}
</kbd>
) : null}
)}
</button>
</li>
))}
</Popover.Panel>
</>
)}
</Popover>
)
}

View File

@ -30,7 +30,7 @@ export const AppHeader = ({
className={
'w-full grid ' +
styles.header +
' overlaid-panes sticky top-0 z-20 px-2 items-start ' +
' overlaid-panes sticky top-0 z-20 px-2 items-center ' +
className
}
>

View File

@ -6,7 +6,7 @@ const CustomIconMap = {
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.5 5C11.3284 5 12 4.32843 12 3.5C12 2.67157 11.3284 2 10.5 2C9.82349 2 9.25159 2.44785 9.06458 3.06324C8.24048 3.17471 7.44026 3.43234 6.7024 3.82649C5.68662 4.36911 4.82039 5.15374 4.18045 6.11089C3.54051 7.06805 3.14664 8.16818 3.03373 9.31385C2.92082 10.4595 3.09238 11.6153 3.53318 12.6789C3.97399 13.7424 4.67043 14.6809 5.56079 15.4112C6.45115 16.1414 7.50795 16.6409 8.63758 16.8655C9.7672 17.0901 10.9348 17.0327 12.037 16.6986C12.6805 16.5035 13.2901 16.2176 13.8482 15.8513C14.0453 15.9466 14.2664 16 14.5 16C15.3284 16 16 15.3284 16 14.5C16 13.6716 15.3284 13 14.5 13C13.6716 13 13 13.6716 13 14.5C13 14.7212 13.0479 14.9312 13.1338 15.1202C12.7013 15.3842 12.2355 15.5935 11.7468 15.7416C10.802 16.0281 9.80098 16.0772 8.83255 15.8847C7.86413 15.6922 6.95818 15.264 6.19496 14.638C5.43174 14.012 4.83479 13.2076 4.45698 12.296C4.07917 11.3844 3.93214 10.3938 4.02891 9.41193C4.12568 8.43002 4.46326 7.4871 5.01177 6.66669C5.56028 5.84628 6.3028 5.17369 7.17358 4.70853C7.77986 4.38466 8.43528 4.16831 9.11077 4.06676C9.33434 4.61422 9.87212 5 10.5 5ZM12.571 4.57608C12.8364 4.70157 13.0934 4.84685 13.3396 5.01126C13.5858 5.17563 13.8184 5.35726 14.0359 5.55431L14.7073 4.81315C14.4534 4.58326 14.1821 4.37137 13.895 4.17964C13.6078 3.98786 13.308 3.81837 12.9983 3.67198L12.571 4.57608ZM15.1537 6.9154C15.3046 7.16714 15.4375 7.43061 15.5508 7.704C15.6641 7.97734 15.7566 8.25751 15.8279 8.54215L16.7979 8.29903C16.7147 7.96691 16.6068 7.64002 16.4746 7.32112C16.3425 7.00216 16.1874 6.69476 16.0112 6.40105L15.1537 6.9154ZM16.0006 10.2944C15.9862 10.5875 15.9502 10.8803 15.8925 11.1705C15.8347 11.4607 15.7558 11.745 15.6569 12.0213L16.5983 12.3584C16.7138 12.036 16.8058 11.7043 16.8732 11.3657C16.9406 11.0271 16.9826 10.6855 16.9994 10.3435L16.0006 10.2944Z"
d="M10 1.5C8.60217 1.5 7.22591 1.84474 5.99313 2.50367C4.76035 3.1626 3.70911 4.1154 2.93251 5.27765C2.15592 6.43991 1.67794 7.77575 1.54093 9.16685C1.40392 10.558 1.6121 11.9614 2.14703 13.2528C2.68195 14.5442 3.52712 15.6838 4.60766 16.5706C5.6882 17.4574 6.97076 18.064 8.34173 18.3367C9.71271 18.6094 11.1298 18.5398 12.4674 18.134C13.8051 17.7282 15.022 16.9988 16.0104 16.0104C16.3068 15.714 16.5796 15.3974 16.8273 15.0634L16.0241 14.4677C15.8055 14.7624 15.5648 15.0418 15.3033 15.3033C14.4312 16.1754 13.3574 16.819 12.1771 17.1771C10.9969 17.5351 9.74651 17.5965 8.53682 17.3559C7.32714 17.1153 6.19547 16.58 5.24205 15.7976C4.28863 15.0151 3.5429 14.0096 3.07091 12.8701C2.59891 11.7306 2.41522 10.4923 2.53612 9.26487C2.65701 8.03743 3.07875 6.85874 3.76398 5.83322C4.44921 4.8077 5.37678 3.967 6.46453 3.38559C7.55227 2.80418 8.76662 2.5 10 2.5C10.3699 2.5 10.7376 2.52734 11.1005 2.58117L11.2472 1.59199C10.836 1.53099 10.4192 1.5 10 1.5ZM13.2067 3.22008C13.5383 3.37691 13.8593 3.5585 14.1668 3.76398C14.4743 3.96946 14.7649 4.19652 15.0367 4.44286L15.7083 3.70191C15.4002 3.42271 15.0709 3.16538 14.7223 2.93251C14.3738 2.69964 14.0101 2.49384 13.6342 2.31609L13.2067 3.22008ZM16.433 6.14423C16.6216 6.45886 16.7876 6.78818 16.9291 7.12987C17.0706 7.47157 17.1861 7.82181 17.2752 8.17765L18.2453 7.93467C18.1443 7.53138 18.0134 7.13444 17.853 6.74719C17.6926 6.35995 17.5044 5.98672 17.2907 5.63012L16.433 6.14423ZM17.491 10.368C17.473 10.7344 17.428 11.1004 17.3559 11.4632C17.2837 11.8259 17.1852 12.1813 17.0616 12.5267L18.0031 12.8636C18.1432 12.4721 18.2549 12.0694 18.3367 11.6583C18.4184 11.2472 18.4694 10.8323 18.4898 10.4171L17.491 10.368Z"
fill="currentColor"
/>
</svg>
@ -71,46 +71,6 @@ const CustomIconMap = {
/>
</svg>
),
booleanExclude: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 5H5V12H8.33325V15.3333H15.3333V8.33333H12V5ZM12 8.33333H8.33325V12H12V8.33333Z"
fill="currentColor"
/>
</svg>
),
booleanIntersect: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 6H11V8H9H8V9V11H6V6ZM8 12H6H5V11V6V5H6H11H12V6V8H14H15V9V14V15H14H9H8V14V12ZM9 12V14H14V9H12V11V12H12H11H9Z"
fill="currentColor"
/>
</svg>
),
booleanSubtract: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 6H11V11H6V6ZM5 5H6H11H12V6V8H15V15H7.99998V12H6H5V11V6V5Z"
fill="currentColor"
/>
</svg>
),
booleanUnion: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 5H5V12H8V15H15V8H12V5Z"
fill="currentColor"
/>
</svg>
),
bug: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -141,26 +101,6 @@ const CustomIconMap = {
/>
</svg>
),
chamfer3d: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 6V5H7H9L15 11V13V14H14V15H13H6H5V14V7V6H6ZM13 14H6V7H8.58579L13 11.4142V14Z"
fill="currentColor"
/>
</svg>
),
circle: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 4C9.21207 4 8.43186 4.15519 7.7039 4.45672C6.97595 4.75825 6.31451 5.20021 5.75736 5.75736C5.20021 6.31451 4.75825 6.97594 4.45672 7.7039C4.1552 8.43185 4 9.21207 4 10C4 10.7879 4.15519 11.5681 4.45672 12.2961C4.75825 13.0241 5.20021 13.6855 5.75736 14.2426C6.31451 14.7998 6.97594 15.2417 7.7039 15.5433C8.43185 15.8448 9.21207 16 10 16C10.7879 16 11.5681 15.8448 12.2961 15.5433C13.0241 15.2417 13.6855 14.7998 14.2426 14.2426C14.7998 13.6855 15.2417 13.0241 15.5433 12.2961C15.8448 11.5681 16 10.7879 16 10C16 9.21207 15.8448 8.43185 15.5433 7.7039C15.2417 6.97595 14.7998 6.31451 14.2426 5.75736C13.6855 5.20021 13.0241 4.75825 12.2961 4.45672C11.5681 4.15519 10.7879 4 10 4ZM7.32122 3.53284C8.1705 3.18106 9.08075 3 10 3C10.9193 3 11.8295 3.18106 12.6788 3.53284C13.5281 3.88463 14.2997 4.40024 14.9497 5.05025C15.5998 5.70026 16.1154 6.47194 16.4672 7.32122C16.8189 8.1705 17 9.08075 17 10C17 10.9193 16.8189 11.8295 16.4672 12.6788C16.1154 13.5281 15.5998 14.2997 14.9497 14.9497C14.2997 15.5998 13.5281 16.1154 12.6788 16.4672C11.8295 16.8189 10.9193 17 10 17C9.08074 17 8.17049 16.8189 7.32121 16.4672C6.47193 16.1154 5.70026 15.5998 5.05025 14.9497C4.40024 14.2997 3.88462 13.5281 3.53284 12.6788C3.18106 11.8295 3 10.9192 3 10C3 9.08074 3.18106 8.17049 3.53284 7.32121C3.88463 6.47193 4.40024 5.70026 5.05026 5.05025C5.70027 4.40024 6.47194 3.88462 7.32122 3.53284Z"
fill="currentColor"
/>
</svg>
),
clipboardCheckmark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -242,7 +182,7 @@ const CustomIconMap = {
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 2.51583L10.3536 2.86938L12.3536 4.86938L11.6465 5.57649L10.5 4.43004V10.1012C11.0826 10.3071 11.5 10.8627 11.5 11.5158C11.5 12.3443 10.8284 13.0158 10 13.0158C9.17157 13.0158 8.5 12.3443 8.5 11.5158C8.5 10.8627 8.9174 10.3071 9.5 10.1012V4.43004L8.35356 5.57649L7.64645 4.86938L9.64645 2.86938L10 2.51583ZM3.95886 10.8441L8.5 8.06893V9.24088L4.91773 11.43L10 14.5359L15.0823 11.43L11.5 9.24087V8.06893L16.0411 10.8441L17 11.43L17 13.4842H16V12.0412L10.5 15.4023V17.4842H9.5V15.4023L4 12.0412V13.4842H3V11.43L3.95886 10.8441Z"
d="M10 3L10.3536 3.35355L12.3536 5.35355L11.6465 6.06066L10.5 4.91421V11.5854C11.0826 11.7913 11.5 12.3469 11.5 13C11.5 13.8284 10.8284 14.5 10 14.5C9.17157 14.5 8.5 13.8284 8.5 13C8.5 12.3469 8.91741 11.7913 9.5 11.5854V4.91421L8.35356 6.06066L7.64645 5.35355L9.64645 3.35355L10 3ZM1.95887 12.3282L8 8.63644V9.80838L2.91773 12.9142L10 17.2423L17.0823 12.9142L12 9.80838V8.63644L18.0411 12.3282L19 12.9142L19 14.9683H18V13.5253L10.5 18.1087V19.9683H9.5V18.1087L2 13.5253V14.9683H1L1 12.9142L1.95887 12.3282Z"
fill="currentColor"
/>
</svg>
@ -263,16 +203,6 @@ const CustomIconMap = {
/>
</svg>
),
fillet3d: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 6V5H7H10C12.7614 5 15 7.23858 15 10V13V14H14V15H13H6H5V14V7V6H6ZM6 7H10C11.6569 7 13 8.34315 13 10V14H6V7Z"
fill="currentColor"
/>
</svg>
),
file: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -293,16 +223,6 @@ const CustomIconMap = {
/>
</svg>
),
floppyDiskArrow: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 4H4.5L7 4L13 4L13.2071 4L13.3536 4.14645L15.8536 6.64645L16 6.79289V7V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V7.20711L13.5 5.70711V7V7.5L13 7.5L7 7.5L6.5 7.5V7V5L5 5V15H6.5V10V9.5H7H13H13.5V10V10.5C13.1547 10.5 12.8196 10.5438 12.5 10.626V10.5H7.5V15H9.53095C9.57451 15.3493 9.66311 15.6847 9.79076 16H7H4.5H4V15.5V4.5V4ZM7.5 5V6.5L12.5 6.5V5L7.5 5ZM16.3904 14.1877L14.3904 11.6877L13.6096 12.3123L14.9597 14H11V15H14.9597L13.6096 16.6877L14.3904 17.3123L16.3904 14.8123L16.6403 14.5L16.3904 14.1877Z"
fill="currentColor"
/>
</svg>
),
folder: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -333,16 +253,6 @@ const CustomIconMap = {
/>
</svg>
),
hole: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.44141 3.77773L5.86999 5.29277L12.7003 6.74461L13.0466 6.81819L15.0555 4.88135L15.7415 5.59773L13.6999 7.56607L13.6785 7.54375V7.95252V15.3589L15.0555 14.0313L15.7415 14.7477L13.6999 16.7161L13.6785 16.6938V16.7185L12.7003 16.5105L5.685 15.0194L4.70685 14.8115V13.8115V6.04554V5.04554L4.73466 5.05145L4.71383 5.02969L6.75542 3.06135L7.44141 3.77773ZM5.685 6.25345L12.7003 7.74461V15.5105L5.685 14.0194V6.25345ZM8.63431 8.91669C8.82484 10.2312 9.80376 11.448 11.0461 11.9188C10.8003 12.6735 10.064 13.1027 9.19383 12.9178C8.12261 12.6901 7.25421 11.6177 7.25421 10.5225C7.25421 9.62626 7.83586 8.9925 8.63431 8.91669ZM8.65548 7.88811C7.30121 7.8585 6.27606 8.85522 6.27606 10.3146C6.27606 11.9621 7.58239 13.5753 9.19383 13.9178C10.6117 14.2191 11.7933 13.4364 12.0568 12.1219C12.06 12.1058 12.0631 12.0896 12.066 12.0734C12.096 11.9083 12.1116 11.7352 12.1116 11.555C12.1116 9.90758 10.8053 8.29439 9.19383 7.95187C9.00991 7.91278 8.82997 7.89193 8.65548 7.88811Z"
fill="currentColor"
/>
</svg>
),
horizontal: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -396,7 +306,7 @@ const CustomIconMap = {
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.75 6.375C15.5784 6.375 16.25 5.70343 16.25 4.875C16.25 4.04657 15.5784 3.375 14.75 3.375C13.9216 3.375 13.25 4.04657 13.25 4.875C13.25 5.16584 13.3328 5.43736 13.4761 5.66726L5.88512 13.7657C5.69226 13.6754 5.47702 13.625 5.25 13.625C4.42157 13.625 3.75 14.2966 3.75 15.125C3.75 15.9534 4.42157 16.625 5.25 16.625C6.07843 16.625 6.75 15.9534 6.75 15.125C6.75 14.889 6.69549 14.6657 6.59837 14.467L14.26 6.29315C14.4136 6.34619 14.5784 6.375 14.75 6.375Z"
d="M15.5 6C16.3284 6 17 5.32843 17 4.5C17 3.67157 16.3284 3 15.5 3C14.6716 3 14 3.67157 14 4.5C14 4.73107 14.0522 4.94993 14.1456 5.14543L5.14543 14.1456C4.94993 14.0522 4.73107 14 4.5 14C3.67157 14 3 14.6716 3 15.5C3 16.3284 3.67157 17 4.5 17C5.32843 17 6 16.3284 6 15.5C6 15.2679 5.94729 15.0482 5.8532 14.852L14.852 5.8532C15.0482 5.94729 15.2679 6 15.5 6Z"
fill="currentColor"
/>
</svg>
@ -411,36 +321,6 @@ const CustomIconMap = {
/>
</svg>
),
lockClosed: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 5.5C8.61929 5.5 7.5 6.61929 7.5 8V9H12.5V8C12.5 6.61929 11.3807 5.5 10 5.5ZM6.5 8V9H6H5V10V15V16H6H14H15V15V10V9H14H13.5V8C13.5 6.067 11.933 4.5 10 4.5C8.067 4.5 6.5 6.067 6.5 8ZM6 10V15H14V10H6ZM10.5 11V12.1338C10.7989 12.3067 11 12.6299 11 13C11 13.5523 10.5523 14 10 14C9.44772 14 9 13.5523 9 13C9 12.6299 9.2011 12.3067 9.5 12.1338V11H10.5Z"
fill="currentColor"
/>
</svg>
),
lockOpen: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 4.5C8.61929 4.5 7.5 5.61929 7.5 7H6.5C6.5 5.067 8.067 3.5 10 3.5C11.933 3.5 13.5 5.067 13.5 7V9H14H15V10V15V16H14H6H5V15V10V9H6H12.5V7C12.5 5.61929 11.3807 4.5 10 4.5ZM6 10V15H14V10H6ZM10.5 11V12.1338C10.7989 12.3067 11 12.6299 11 13C11 13.5523 10.5523 14 10 14C9.44772 14 9 13.5523 9 13C9 12.6299 9.2011 12.3067 9.5 12.1338V11H10.5Z"
fill="currentColor"
/>
</svg>
),
loft: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.7954 6.22689C14.7749 5.73715 15 5.2456 15 5C15 4.7544 14.7749 4.26285 13.7954 3.77311C12.8758 3.31327 11.5353 3 10 3C8.46473 3 7.12424 3.31327 6.20457 3.77311C5.22509 4.26285 5 4.7544 5 5C5 5.2456 5.22509 5.73715 6.20457 6.22689C6.29872 6.27397 6.39728 6.3195 6.5 6.36333L6.5 7.43698C4.98593 6.89239 4 6.00376 4 5C4 3.34315 6.68629 2 10 2C13.3137 2 16 3.34315 16 5C16 6.00376 15.0141 6.89239 13.5 7.43698V6.36333C13.6027 6.3195 13.7013 6.27397 13.7954 6.22689ZM11.5 8.5531V9.72505L15.0823 11.9142L10 15.0201L4.91773 11.9142L8.5 9.72505V8.5531L3.95886 11.3282L3 11.9142V13.9683H4V12.5253L9.5 15.8864V17.9683H10.5V15.8864L16 12.5253V13.9683H17L17 11.9142L16.0411 11.3282L11.5 8.5531ZM10 4.29289L10.3536 4.64645L12.3536 6.64644L11.6465 7.35355L10.5 6.20711V10.5854C11.0826 10.7913 11.5 11.3469 11.5 12C11.5 12.8284 10.8284 13.5 10 13.5C9.17157 13.5 8.5 12.8284 8.5 12C8.5 11.3469 8.91741 10.7913 9.5 10.5854V6.20711L8.35356 7.35355L7.64645 6.64644L9.64645 4.64645L10 4.29289Z"
fill="currentColor"
/>
</svg>
),
'make-variable': (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -461,16 +341,6 @@ const CustomIconMap = {
/>
</svg>
),
mirror: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.41667 13.8333V17H10.4167V13.7738L12.5 14.5179L12.6682 14.047L12.8363 13.5761L10.4167 12.7119V7.28805L12.8363 6.42388L12.6682 5.95301L12.5 5.48214L10.4167 6.22619V3H9.41667V6.16667L4 4.23214L3 3.875V4.93686V15.0631V16.125L4 15.7679L9.41667 13.8333ZM9.41667 12.7715V7.22853L4 5.294V14.706L9.41667 12.7715ZM16.5 6.0625H17V4.93686V4.40593V3.875L16.5 4.05357L16 4.23214L14.5 4.76786L14.6682 5.23873L14.8363 5.7096L16 5.294V6.0625H16.5ZM16.5 7.8125H17V12.1875H16.5H16V7.8125H16.5ZM16.5 13.9375H17V15.0631V15.5941V16.125L16.5 15.9464L16 15.7679L14.5 15.2321L14.6682 14.7613L14.8363 14.2904L16 14.706V13.9375H16.5Z"
fill="currentColor"
/>
</svg>
),
move: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -521,16 +391,6 @@ const CustomIconMap = {
/>
</svg>
),
plane: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.92871 5.11391L4.43964 5.00995V4.10898V3.60898V3.10898L4.92871 3.21293L5.41778 3.31689L6.29907 3.50421V4.00421V4.50421L5.41778 4.31689V5.21786L4.92871 5.11391ZM11.8774 4.68991L8.1585 3.89945V4.39945V4.89945L11.8774 5.68991V5.18991V4.68991ZM13.7368 5.08515V5.58515V6.08515L14.6181 6.27247V7.17344L15.1071 7.2774L15.5962 7.38135V6.48038V5.98038V5.48038L15.1071 5.37643L14.6181 5.27247L13.7368 5.08515ZM15.5962 9.28233L15.1071 9.17837L14.6181 9.07441V12.8764L15.1071 12.9803L15.5962 13.0843V9.28233ZM15.5962 14.9852L15.1071 14.8813L14.6181 14.7773V15.6783L13.7368 15.491V15.991V16.491L14.6181 16.6783L15.1071 16.7823L15.5962 16.8862V16.3862V15.8862V14.9852ZM11.8774 16.0957V15.5957V15.0957L8.1585 14.3053V14.8053V15.3053L11.8774 16.0957ZM6.29907 14.91V14.41V13.91L5.41778 13.7227V12.8217L4.92871 12.7178L4.43964 12.6138V13.5148V14.0148V14.5148L4.92871 14.6188L5.41778 14.7227L6.29907 14.91ZM4.43964 10.7129L4.92871 10.8168L5.41778 10.9208V7.11883L4.92871 7.01488L4.43964 6.91092V10.7129Z"
fill="currentColor"
/>
</svg>
),
plus: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -541,16 +401,6 @@ const CustomIconMap = {
/>
</svg>
),
polygon: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.99291 4.23607L4.03556 8.56434L6.31106 15.5676H13.6748L15.9503 8.56434L9.99291 4.23607ZM17.1258 8.18237L9.99291 3L2.85999 8.18237L5.58452 16.5676H14.4013L17.1258 8.18237Z"
fill="currentColor"
/>
</svg>
),
questionMark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -579,22 +429,6 @@ const CustomIconMap = {
/>
</svg>
),
revolve: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.89927 4.693C7.36143 4.41381 7.87 4.21672 8.40842 4.10376C7.83595 4.69964 7.47 5.50199 7.47 6.31393C7.47 7.34818 8.06376 8.11443 8.9218 8.3459C8.85859 8.44278 8.80068 8.54347 8.74845 8.64757C8.69108 8.76191 8.6411 8.87927 8.59864 8.99896L9.87934 8.72674C10.0433 8.52252 10.2451 8.35006 10.4752 8.21957L10.4736 8.21676C11.6445 7.74118 12.53 6.5014 12.53 5.2384C12.53 4.20393 11.936 3.43756 11.0776 3.20628L11.0781 3.19471C9.34471 2.75461 7.64421 2.9631 6.28878 3.78191C4.93335 4.60073 4.01395 5.97492 3.69937 7.6522C3.55495 8.4222 3.54245 9.23235 3.65633 10.0495L3.88904 10L4.59283 9.85042C4.53004 9.23247 4.55179 8.62188 4.6613 8.038C4.93318 6.58836 5.7278 5.40068 6.89927 4.693ZM7.47005 15.1799C6.65634 14.4835 5.96657 13.6244 5.46142 12.6658L4.54925 12.8597L4.57626 12.9127C5.40057 14.5194 6.672 15.8914 8.18092 16.8103C8.28057 16.8945 8.38913 16.9684 8.50564 17.0308L9.08339 16.1011C8.87304 15.9884 8.70487 15.8151 8.5944 15.5972C8.48392 15.3793 8.43471 15.1238 8.45129 14.8543C8.46788 14.5848 8.54973 14.31 8.68929 14.0552C8.82885 13.8005 9.02162 13.574 9.24981 13.3967C9.47801 13.2193 9.73427 13.0969 9.99495 13.0406C10.2556 12.9843 10.5123 12.996 10.7413 13.0747C10.9608 13.15 11.1481 13.2845 11.2874 13.4665C11.7756 13.525 12.1412 13.4705 12.4217 13.3627C12.354 13.1483 12.2554 12.9503 12.128 12.7748C11.8981 12.4582 11.5819 12.225 11.2086 12.0968C11.1027 12.0605 10.9931 12.0329 10.8808 12.0141C10.6353 11.9374 10.407 11.816 10.2071 11.6571L9.0562 11.9017C9.09421 11.9513 9.13379 11.9999 9.17491 12.0473C9.2375 12.1194 9.30323 12.1884 9.37183 12.254C9.16516 12.3521 8.9652 12.4755 8.77697 12.6218C8.40494 12.9109 8.09066 13.2801 7.86313 13.6955C7.6356 14.1108 7.50216 14.5588 7.47513 14.9982C7.47137 15.0592 7.46969 15.1198 7.47005 15.1799ZM9.99038 7.3647L10 7.36268C10.8571 7.18051 11.5518 6.32252 11.5518 5.44631C11.5518 4.5701 10.8571 4.00748 10 4.18965C9.14293 4.37183 8.44815 5.22981 8.44815 6.10602C8.44815 6.9614 9.11029 7.51793 9.93915 7.37442C9.95332 7.36608 9.96757 7.35785 9.98189 7.34973L9.99038 7.3647ZM16.6 8.29822L16.1109 8.40218L14.074 8.83515V9.83515L16.1109 9.40218L16.6 9.29822V8.29822ZM7.96305 11.1341L12.037 10.2681V9.26813L7.96305 10.1341V11.1341ZM3.8891 12L5.92607 11.567V10.567L3.8891 11L3.40002 11.104V12.104L3.8891 12ZM15.4172 11.1225L15.9703 10.8085L16.0604 11.4381L16.4532 14.1827L15.5948 14.3056L15.5027 13.662L15.5423 12.9555L15.3598 13.5867C15.1404 13.9788 14.8672 14.3472 14.529 14.662C13.8137 15.3278 12.8466 15.7187 11.5898 15.635C11.3638 16.0194 10.9459 16.2774 10.4678 16.2774C9.74943 16.2774 9.16711 15.6951 9.16711 14.9767C9.16711 14.2584 9.74943 13.6761 10.4678 13.6761C11.1178 13.6761 11.6565 14.153 11.753 14.776C12.7384 14.8173 13.4355 14.4952 13.9382 14.0273C14.4048 13.5929 14.7326 13.0058 14.9513 12.384L13.4607 13.2302L13.0326 12.4761L15.4172 11.1225Z"
fill="currentColor"
/>
</svg>
),
search: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -615,16 +449,6 @@ const CustomIconMap = {
/>
</svg>
),
shell: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.24585 3.87359L5.67443 5.38863L12.5048 6.84046L12.851 6.91405L14.8599 4.97721L15.5459 5.69359L13.5043 7.66193L13.4829 7.63961V8.04838V15.4548L14.8599 14.1272L15.5459 14.8436L13.5043 16.8119L13.4829 16.7896V16.8143L12.5048 16.6064L5.48944 15.1152L4.51129 14.9073V13.9073V6.14139V5.14139L4.5391 5.14731L4.51827 5.12555L6.55986 3.15721L7.24585 3.87359ZM5.48944 6.34931L12.5048 7.84046V15.6064L5.48944 14.1152V6.34931ZM10.5978 13.3098V11.2576L9.62543 11.0509L7.8814 12.7324L10.5978 13.3098ZM7.19195 12.0176V8.74893L9.13482 9.1619V10.1445L7.19195 12.0176ZM11.576 13.5177V11.4655L11.5761 11.4656V8.6808L11.576 8.68078L10.5978 8.47287L9.13482 8.16189L7.19195 7.74893L6.21381 7.54101L6.2138 7.54102V8.54102V12.3779V13.3779L7.19195 13.5859L10.5978 14.3098L11.576 14.5177L11.576 14.5177V13.5177Z"
fill="currentColor"
/>
</svg>
),
sketch: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -635,32 +459,6 @@ const CustomIconMap = {
/>
</svg>
),
spline: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0052 6.91281C15.5851 6.70544 16 6.15118 16 5.5C16 4.67157 15.3284 4 14.5 4C13.6716 4 13 4.67157 13 5.5C13 6.15503 13.4199 6.712 14.0051 6.91646C14.0058 7.31235 13.9653 7.7498 13.8414 8.1571C13.6773 8.69654 13.3777 9.1532 12.854 9.42296C12.3204 9.69782 11.4691 9.82282 10.1135 9.5099C8.62632 9.16662 7.50955 9.26015 6.69015 9.68223C5.86086 10.1094 5.42244 10.8281 5.20399 11.546C5.04308 12.0748 4.99567 12.6189 4.99693 13.0864C4.41594 13.2932 4 13.848 4 14.5C4 15.3284 4.67157 16 5.5 16C6.32843 16 7 15.3284 7 14.5C7 13.8458 6.58114 13.2893 5.99696 13.0843C5.99585 12.6867 6.03606 12.2466 6.16068 11.8371C6.32483 11.2976 6.62436 10.841 7.14807 10.5712C7.68167 10.2964 8.53295 10.1714 9.88859 10.4843C11.3758 10.8276 12.4925 10.734 13.3119 10.312C14.1412 9.88478 14.5796 9.16611 14.7981 8.44821C14.9584 7.92128 15.0061 7.3792 15.0052 6.91281Z"
fill="currentColor"
/>
</svg>
),
sweep: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.4743 3.19282L15.7063 2.49694L14.9738 2.53539L11.7806 2.703L11.833 3.70162L12.2189 3.68137L13.0309 3.47351L12.4065 3.79549C11.5985 4.23813 10.7423 4.7732 10.0381 5.41523C9.28624 6.1008 8.66584 6.94665 8.50598 7.97341C8.36948 8.8502 8.57961 9.78564 9.20597 10.7756C9.07507 10.9983 9 11.2577 9 11.5347C9 12.3631 9.67157 13.0347 10.5 13.0347C11.3284 13.0347 12 12.3631 12 11.5347C12 10.7063 11.3284 10.0347 10.5 10.0347C10.3174 10.0347 10.1425 10.0673 9.98067 10.127C9.51409 9.34655 9.40701 8.68651 9.49408 8.12725C9.60506 7.41442 10.0472 6.76028 10.7119 6.15418C11.3749 5.54964 12.218 5.03092 13.0491 4.58458C13.4267 4.38176 13.797 4.19644 14.1426 4.02567L13.5257 5.87659L14.4743 6.19282L15.4743 3.19282ZM10.4854 8.10284C10.4627 8.2645 10.4579 8.44276 10.4848 8.63934L15.0823 11.4489L10 14.5548L4.91773 11.4489L7.66526 9.76987C7.56422 9.41819 7.50187 9.06536 7.47765 8.71258L3.95886 10.8629L3 11.4489V13.5031H4V12.06L9.5 15.4211V17.5031H10.5V15.4211L16 12.06V13.5031H17L17 11.4489L16.0411 10.8629L10.6351 7.55929C10.5598 7.74567 10.5101 7.92698 10.4854 8.10284Z"
fill="currentColor"
/>
</svg>
),
tangent: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -671,14 +469,6 @@ const CustomIconMap = {
/>
</svg>
),
text: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.3107 14.9933L11.5524 12.3321H8.37616L7.61786 14.9933H5.98682L8.90553 5.00671H11.0946L14.0133 14.9933H12.3107ZM10.0215 6.62345H9.90705L8.67661 11.0015H11.2519L10.0215 6.62345Z"
fill="currentColor"
/>
</svg>
),
'three-dots': (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path

View File

@ -177,7 +177,7 @@ const FileTreeItem = ({
codeManager.writeToFile()
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
} else {

View File

@ -23,8 +23,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
name="questionMark"
className="w-7 h-7 rounded-full bg-chalkboard-110 dark:bg-chalkboard-80 text-chalkboard-10"
/>
<span className="sr-only">Help and resources</span>
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
<Tooltip position="top-right" className="ui-open:hidden">
Help and resources
</Tooltip>
</Popover.Button>

View File

@ -80,9 +80,7 @@ export function LowerRightControls({
name="bug"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<Tooltip position="top" contentClassName="text-xs">
Report a bug
</Tooltip>
<Tooltip position="top">Report a bug</Tooltip>
</a>
<Link
to={
@ -95,10 +93,7 @@ export function LowerRightControls({
name="settings"
className={`w-5 h-5 ${linkOverrideClassName}`}
/>
<span className="sr-only">Settings</span>
<Tooltip position="top" contentClassName="text-xs">
Settings
</Tooltip>
<Tooltip position="top">Settings</Tooltip>
</Link>
<NetworkHealthIndicator />
<HelpMenu />

View File

@ -138,23 +138,15 @@ export const ModelingMachineProvider = ({
sceneInfra.camControls.syncDirection = 'engineToClient'
const resp = await engineCommandManager.sendSceneCommand({
const settings: Models['CameraSettings_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
const settings =
resp &&
resp.success &&
resp.resp.type === 'modeling' &&
resp.resp.data.modeling_response.type ===
'default_camera_get_settings'
? resp.resp.data.modeling_response.data.settings
: ({} as Models['DefaultCameraGetSettings_type']['settings'])
)?.data?.data?.settings
if (settings.up.z !== 1) {
// workaround for gimbal lock situation
await engineCommandManager.sendSceneCommand({
@ -175,7 +167,7 @@ export const ModelingMachineProvider = ({
}
store.videoElement?.pause()
kclManager.executeCode().then(() => {
kclManager.executeCode(true).then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play()

View File

@ -1,5 +1,5 @@
.panel {
@apply relative z-0 rounded-r max-w-full flex-auto;
@apply relative z-0 rounded-r max-w-full h-full flex-1;
display: grid;
grid-template-rows: auto 1fr;
@apply bg-chalkboard-10/50 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-20;

View File

@ -3,14 +3,10 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useModelingContext } from 'hooks/useModelingContext'
import { ActionButton } from 'components/ActionButton'
import Tooltip from 'components/Tooltip'
import { CustomIconName } from 'components/CustomIcon'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from 'components/ActionIcon'
export interface ModelingPaneProps
extends React.PropsWithChildren,
React.HTMLAttributes<HTMLDivElement> {
icon?: CustomIconName | IconDefinition
title: string
Menu?: React.ReactNode | React.FC
detailsTestId?: string
@ -18,25 +14,13 @@ export interface ModelingPaneProps
}
export const ModelingPaneHeader = ({
icon,
title,
Menu,
onClose,
}: Pick<ModelingPaneProps, 'icon' | 'title' | 'Menu' | 'onClose'>) => {
}: Pick<ModelingPaneProps, 'title' | 'Menu' | 'onClose'>) => {
return (
<div className={styles.header}>
<div className="flex gap-2 items-center flex-1">
{icon && (
<ActionIcon
icon={icon}
className="p-1"
size="sm"
iconClassName="!text-chalkboard-80 dark:!text-chalkboard-30"
bgClassName="!bg-transparent"
/>
)}
<span>{title}</span>
</div>
<div className="flex gap-2 items-center flex-1">{title}</div>
{Menu instanceof Function ? <Menu /> : Menu}
<ActionButton
Element="button"
@ -58,7 +42,6 @@ export const ModelingPaneHeader = ({
export const ModelingPane = ({
title,
icon,
id,
children,
className,
@ -80,19 +63,14 @@ export const ModelingPane = ({
data-testid={detailsTestId}
id={id}
className={
'focus-within:border-primary dark:focus-within:border-chalkboard-50 ' +
'group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
pointerEventsCssClass +
styles.panel +
' group ' +
(className || '')
}
>
<ModelingPaneHeader
icon={icon}
title={title}
Menu={Menu}
onClose={onClose}
/>
<ModelingPaneHeader title={title} Menu={Menu} onClose={onClose} />
<div className="relative w-full">{children}</div>
</section>
)

View File

@ -66,7 +66,7 @@ export const KclEditorPane = () => {
useEffect(() => {
if (typeof window === 'undefined') return
const onlineCallback = () => kclManager.executeCode(true)
const onlineCallback = () => kclManager.executeCode(true, true)
window.addEventListener('online', onlineCallback)
return () => window.removeEventListener('online', onlineCallback)
}, [])

View File

@ -35,13 +35,13 @@ export type SidebarPane = {
hideOnPlatform?: 'desktop' | 'web'
}
export const sidebarPanes: SidebarPane[] = [
export const topPanes: SidebarPane[] = [
{
id: 'code',
title: 'KCL Code',
icon: faCode,
Content: KclEditorPane,
keybinding: 'Shift + C',
keybinding: 'shift + c',
Menu: KclEditorMenu,
},
{
@ -49,37 +49,40 @@ export const sidebarPanes: SidebarPane[] = [
title: 'Project Files',
icon: 'folder',
Content: FileTreeInner,
keybinding: 'Shift + F',
keybinding: 'shift + f',
Menu: FileTreeMenu,
hideOnPlatform: 'web',
},
]
export const bottomPanes: SidebarPane[] = [
{
id: 'variables',
title: 'Variables',
icon: faSquareRootVariable,
Content: MemoryPane,
Menu: MemoryPaneMenu,
keybinding: 'Shift + V',
keybinding: 'shift + v',
},
{
id: 'logs',
title: 'Logs',
icon: faCodeCommit,
Content: LogsPane,
keybinding: 'Shift + L',
keybinding: 'shift + l',
},
{
id: 'kclErrors',
title: 'KCL Errors',
icon: faExclamationCircle,
Content: KclErrorsPane,
keybinding: 'Shift + E',
keybinding: 'shift + e',
},
{
id: 'debug',
title: 'Debug',
icon: faBugSlash,
Content: DebugPane,
keybinding: 'Shift + D',
keybinding: 'shift + d',
},
]

View File

@ -1,84 +1,39 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import { useCallback, useMemo } from 'react'
import { HTMLAttributes, useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarType, sidebarPanes } from './ModelingPanes'
import { Tab } from '@headlessui/react'
import {
SidebarPane,
SidebarType,
bottomPanes,
topPanes,
} from './ModelingPanes'
import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri'
import { useModelingContext } from 'hooks/useModelingContext'
import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const { commandBarSend } = useCommandsContext()
const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus
const { send, context } = useModelingContext()
const { context } = useModelingContext()
const pointerEventsCssClass =
context.store?.buttonDownInStream ||
onboardingStatus.current === 'camera' ||
context.store?.openPanes.length === 0
? 'pointer-events-none '
: 'pointer-events-auto '
const showDebugPanel = settings.context.modeling.showDebugPanel
const sidebarActions: SidebarAction[] = [
{
id: 'export',
title: 'Export part',
icon: 'floppyDiskArrow',
iconClassName: '!p-0',
keybinding: 'Ctrl + Shift + E',
action: () =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Export', groupId: 'modeling' },
}),
},
]
// // Filter out the debug panel if it's not supposed to be shown
// // TODO: abstract out for allowing user to configure which panes to show
const filteredPanes = useMemo(
() =>
(showDebugPanel.current
? sidebarPanes
: sidebarPanes.filter((pane) => pane.id !== 'debug')
).filter(
(pane) =>
!pane.hideOnPlatform ||
(isTauri()
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
),
[sidebarPanes, showDebugPanel.current]
)
const togglePane = useCallback(
(newPane: SidebarType) => {
send({
type: 'Set context',
data: {
openPanes: context.store?.openPanes.includes(newPane)
? context.store?.openPanes.filter((pane) => pane !== newPane)
: [...context.store?.openPanes, newPane],
},
})
},
[context.store?.openPanes, send]
)
return (
<Resizable
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
className={`flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
defaultSize={{
width: '550px',
height: 'auto',
@ -99,64 +54,153 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
}}
>
<div id="app-sidebar" className={styles.grid + ' flex-1'}>
<ul
<ModelingSidebarSection id="sidebar-top" panes={topPanes} />
<ModelingSidebarSection
id="sidebar-bottom"
panes={bottomPanes}
alignButtons="end"
/>
</div>
</Resizable>
)
}
interface ModelingSidebarSectionProps extends HTMLAttributes<HTMLDivElement> {
panes: SidebarPane[]
alignButtons?: 'start' | 'end'
}
function ModelingSidebarSection({
panes,
alignButtons = 'start',
className,
...props
}: ModelingSidebarSectionProps) {
const { settings } = useSettingsAuthContext()
const showDebugPanel = settings.context.modeling.showDebugPanel
const paneIds = panes.map((pane) => pane.id)
const { send, context } = useModelingContext()
const foundOpenPane = context.store?.openPanes.find((pane) =>
paneIds.includes(pane)
)
const [currentPane, setCurrentPane] = useState(
foundOpenPane || ('none' as SidebarType | 'none')
)
const togglePane = useCallback(
(newPane: SidebarType | 'none') => {
if (newPane === 'none') {
send({
type: 'Set context',
data: {
openPanes: context.store?.openPanes.filter(
(p) => p !== currentPane
),
},
})
setCurrentPane('none')
} else if (newPane === currentPane) {
setCurrentPane('none')
send({
type: 'Set context',
data: {
openPanes: context.store?.openPanes.filter((p) => p !== newPane),
},
})
} else {
send({
type: 'Set context',
data: {
openPanes: [
...context.store?.openPanes.filter((p) => p !== currentPane),
newPane,
],
},
})
setCurrentPane(newPane)
}
},
[context.store?.openPanes, send, currentPane, setCurrentPane]
)
// Filter out the debug panel if it's not supposed to be shown
// TODO: abstract out for allowing user to configure which panes to show
const filteredPanes = (
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
).filter(
(pane) =>
!pane.hideOnPlatform ||
(isTauri()
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
)
useEffect(() => {
if (
!showDebugPanel.current &&
currentPane === 'debug' &&
context.store?.openPanes.includes('debug')
) {
togglePane('debug')
}
}, [showDebugPanel.current, togglePane, context.store?.openPanes])
return (
<div className={'group contents ' + className} {...props}>
<Tab.Group
vertical
selectedIndex={
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
}
onChange={(index) => {
const newPane = index === 0 ? 'none' : paneIds[index - 1]
togglePane(newPane)
}}
>
<Tab.List
id={`${props.id}-ribbon`}
className={
(context.store?.openPanes.length === 0 ? 'rounded-r ' : '') +
'relative z-[2] pointer-events-auto p-0 col-start-1 col-span-1 h-fit w-fit flex flex-col ' +
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 '
}
>
<ul
id="pane-buttons-section"
className={
'w-fit p-2 flex flex-col gap-2 ' +
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
'pointer-events-auto ' +
(alignButtons === 'start'
? 'justify-start self-start'
: 'justify-end self-end') +
(currentPane === 'none'
? ' rounded-r focus-within:!border-primary/50'
: ' border-r-0') +
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' +
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
(context.store?.openPanes.length === 1 && currentPane === 'none'
? 'pr-0.5'
: '')
}
>
<Tab key="none" className="sr-only">
No panes open
</Tab>
{filteredPanes.map((pane) => (
<ModelingPaneButton
key={pane.id}
paneConfig={pane}
paneIsOpen={context.store?.openPanes.includes(pane.id)}
onClick={() => togglePane(pane.id)}
aria-pressed={context.store?.openPanes.includes(pane.id)}
currentPane={currentPane}
togglePane={() => togglePane(pane.id)}
/>
))}
</ul>
<hr className="w-full border-chalkboard-20 dark:border-chalkboard-80" />
<ul id="sidebar-actions" className="w-fit p-2 flex flex-col gap-2">
{sidebarActions.map((action) => (
<ModelingPaneButton
key={action.id}
paneConfig={{
id: action.id,
title: action.title,
icon: action.icon,
keybinding: action.keybinding,
iconClassName: action.iconClassName,
iconSize: 'md',
}}
paneIsOpen={false}
onClick={action.action}
/>
))}
</ul>
</ul>
<ul
id="pane-section"
</Tab.List>
<Tab.Panels
id={`${props.id}-pane`}
as="article"
className={
'ml-[-1px] col-start-2 col-span-1 flex flex-col gap-2 ' +
(context.store?.openPanes.length >= 1
'col-start-2 col-span-1 ' +
(context.store?.openPanes.length === 1
? currentPane !== 'none'
? `row-start-1 row-end-3`
: `hidden`)
: `hidden`
: ``)
}
>
{filteredPanes
.filter((pane) => context?.store.openPanes.includes(pane.id))
.map((pane) => (
<Tab.Panel key="none" />
{filteredPanes.map((pane) => (
<Tab.Panel key={pane.id} className="h-full">
<ModelingPane
key={pane.id}
icon={pane.icon}
id={`${pane.id}-pane`}
title={pane.title}
Menu={pane.Menu}
@ -168,75 +212,55 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
pane.Content
)}
</ModelingPane>
</Tab.Panel>
))}
</ul>
</Tab.Panels>
</Tab.Group>
</div>
</Resizable>
)
}
interface ModelingPaneButtonProps
extends React.HTMLAttributes<HTMLButtonElement> {
paneConfig: {
id: string
title: string
icon: CustomIconName | IconDefinition
keybinding: string
iconClassName?: string
iconSize?: 'sm' | 'md' | 'lg'
}
onClick: () => void
paneIsOpen: boolean
interface ModelingPaneButtonProps {
paneConfig: SidebarPane
currentPane: SidebarType | 'none'
togglePane: () => void
}
function ModelingPaneButton({
paneConfig,
onClick,
paneIsOpen,
...props
currentPane,
togglePane,
}: ModelingPaneButtonProps) {
useHotkeys(paneConfig.keybinding, onClick, {
useHotkeys(paneConfig.keybinding, togglePane, {
scopes: ['modeling'],
})
return (
<button
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick}
name={paneConfig.title}
data-testid={paneConfig.id + '-pane-button'}
{...props}
<Tab
key={paneConfig.id}
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-none"
onClick={togglePane}
data-testid={paneConfig.title}
>
<ActionIcon
icon={paneConfig.icon}
className={'p-1 ' + paneConfig.iconClassName || ''}
size={paneConfig.iconSize || 'sm'}
className="p-1"
size="sm"
iconClassName={
paneIsOpen
paneConfig.id === currentPane
? ' !text-chalkboard-10'
: '!text-chalkboard-80 dark:!text-chalkboard-30'
}
bgClassName={
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
'rounded-sm ' +
(paneConfig.id === currentPane ? '!bg-primary' : '!bg-transparent')
}
/>
<span className="sr-only">{paneConfig.title} pane</span>
<Tooltip position="right" hoverOnly>
<span className="flex-1">{paneConfig.title} pane: </span>
<span className="hotkey text-xs capitalize">
{paneConfig.keybinding}
</span>
<Tooltip position="right" hoverOnly delay={800}>
<span>{paneConfig.title}</span>
<br />
<span className="text-xs capitalize">{paneConfig.keybinding}</span>
</Tooltip>
</button>
</Tab>
)
}
export type SidebarAction = {
id: string
title: string
icon: CustomIconName
iconClassName?: string // Just until we get rid of FontAwesome icons
keybinding: string
action: () => void
hideOnPlatform?: 'desktop' | 'web'
}

View File

@ -95,6 +95,7 @@ export const NetworkHealthIndicator = () => {
}
data-testid="network-toggle"
>
<span className="sr-only">Network Health</span>
<ActionIcon
icon={overallConnectionStateIcon[overallState]}
className="p-1"
@ -103,7 +104,7 @@ export const NetworkHealthIndicator = () => {
'rounded-sm ' + overallConnectionStateColor[overallState].bg
}
/>
<Tooltip position="top-right">
<Tooltip position="top-right" className="ui-open:hidden">
Network Health ({NETWORK_HEALTH_TEXT[overallState]})
</Tooltip>
</Popover.Button>

View File

@ -127,10 +127,7 @@ function ProjectMenuPopover({
<>
<span>Export current part</span>
{!findCommand(exportCommandInfo) && (
<Tooltip
position="right"
wrapperClassName="!max-w-none min-w-fit"
>
<Tooltip position="right" className="!max-w-none min-w-fit">
Awaiting engine connection
</Tooltip>
)}

View File

@ -177,7 +177,7 @@ export const SettingsAuthProviderBase = ({
},
'Execute AST': () => {
kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},

View File

@ -1,11 +1,16 @@
/* Adapted from https://github.com/argyleink/gui-challenges/blob/main/tooltips/tool-tip.css */
.tooltipWrapper {
.tooltip {
/* Used to power spacing and layout for RTL languages */
--isRTL: -1;
/* internal CSS vars */
--_delay: 200ms;
--_triangle-width: 8px;
--_triangle-height: 12px;
--_p-inline-arrow-alignment: calc(
50% + calc(var(--isRTL) * var(--_triangle-width) / 2)
);
--_p-block: 4px;
--_bg: var(--chalkboard-10);
--_shadow-alpha: 8%;
@ -13,21 +18,26 @@
pointer-events: none;
user-select: none;
visibility: hidden;
position: absolute;
z-index: 1;
/* The parts that will be transitioned */
opacity: 0;
transform: translate(var(--_x, 0), var(--_y, 0));
transition: transform 0.15s ease-out, opacity 0.11s ease-out;
}
.tooltip {
@apply relative;
position: absolute;
z-index: 1;
inline-size: max-content;
max-inline-size: 25ch;
text-align: start;
font-family: var(--mono-font-family);
text-transform: none;
font-size: 0.9rem;
font-weight: normal;
line-height: initial;
letter-spacing: 0;
padding: var(--_p-block) calc(2 * var(--_p-block));
margin: 0;
border-radius: 3px;
background: var(--_bg);
@apply text-chalkboard-110;
will-change: filter;
@ -47,28 +57,26 @@
}
/* :has and :is are pretty fresh CSS pseudo-selectors, may not see full support */
:has(> .tooltipWrapper) {
:has(> .tooltip) {
position: relative;
}
:is(:hover, :active) > .tooltipWrapper {
visibility: visible;
:is(:hover, :active) > .tooltip {
opacity: 1;
transition-delay: var(--_delay);
}
:is(:focus-visible) > .tooltipWrapper.withFocus {
visibility: visible;
:is(:focus-visible) > .tooltip.withFocus {
opacity: 1;
}
*:focus-visible .tooltipWrapper {
.tooltip:focus-visible {
--_delay: 0 !important;
}
/* prepend some prose for screen readers only */
.tooltip::before,
.tooltip::after {
.tooltip::before {
content: '; Has tooltip: ';
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
@ -79,19 +87,20 @@
position: absolute;
}
.tooltip::before {
content: '; Has tooltip: ';
}
/* Sometimes there's no visible label,
* so we'll use the tooltip as the label
*/
.tooltip:only-child::before {
content: '';
content: 'Tooltip:';
}
.tooltip:only-child::after {
content: ' (tooltip)';
.caret {
width: 8px;
height: var(--_triangle-height);
position: absolute;
z-index: -1;
transform-origin: center center;
color: var(--_bg);
}
.top,
@ -99,80 +108,129 @@
text-align: center;
}
.tooltipWrapper.top {
.tooltip.top {
inset-inline-start: 50%;
inset-block-end: 100%;
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height));
--_x: calc(50% * var(--isRTL));
}
.tooltipWrapper.top-right {
inset-block-end: 100%;
.top .caret {
inset-block-start: calc(100% - 1px);
inset-inline-start: 50%;
transform: translateX(-50%);
}
.tooltip.top-right {
inset-inline-end: var(--_p-inline-arrow-alignment);
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height));
}
/* The corner caret SVG is bottom-right oriented by default */
.top-right .caret {
inset-block-start: calc(100% - 1px);
inset-inline-end: 0;
}
.tooltipWrapper.right {
inset-inline-start: 100%;
.tooltip.right {
inset-inline-start: calc(100% + var(--_triangle-height));
inset-block-end: 50%;
--_y: 50%;
}
.tooltipWrapper.bottom-right {
inset-block-start: 100%;
inset-inline-end: 0;
.right .caret {
inset-inline-end: calc(100% - 1px);
inset-block-start: 50%;
transform: translateY(-50%) rotate(90deg);
}
.tooltipWrapper.bottom {
.tooltip.bottom-right {
inset-inline-end: var(--_p-inline-arrow-alignment);
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height));
}
.bottom-right .caret {
inset-block-end: calc(100% - 1px);
inset-inline-end: 0;
transform: rotate(180deg) scaleX(-1);
}
.tooltip.bottom {
--_x: calc(50% * var(--isRTL));
inset-inline-start: 50%;
inset-block-start: 100%;
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height));
}
.tooltipWrapper.bottom-left {
inset-block-start: 100%;
.bottom .caret {
inset-block-end: calc(100% - 1px);
inset-inline-start: 50%;
transform: translateX(-50%) scaleY(-1);
}
.tooltipWrapper.left {
inset-inline-end: 100%;
.tooltip.bottom-left {
inset-inline-start: var(--_p-inline-arrow-alignment);
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height));
}
.bottom-left .caret {
inset-block-end: calc(100% - 1px);
inset-inline-start: 0;
transform: rotate(180deg);
}
.tooltip.left {
inset-inline-end: calc(
100% + var(--_p-inline-arrow-alignment) + var(--_triangle-height)
);
inset-block-end: 50%;
--_y: 50%;
}
.tooltipWrapper.top-left {
inset-block-end: 100%;
.left .caret {
inset-inline-start: calc(100% - 1px);
inset-block-start: 50%;
transform: translateY(-50%) rotate(-90deg);
}
.tooltip.top-left {
inset-inline-start: var(--_p-inline-arrow-alignment);
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height));
}
.top-left .caret {
inset-block-start: calc(100% - 1px);
inset-inline-start: 0;
transform: rotate(-90deg);
}
@media (prefers-reduced-motion: no-preference) {
/* TOP || BLOCK-START */
:has(
> :is(
.tooltipWrapper.top,
.tooltipWrapper.top-left,
.tooltipWrapper.top-right
:has(> :is(.tooltip.top, .tooltip.top-left, .tooltip.top-right)):not(
:hover,
:active
)
):not(:hover, :active)
.tooltipWrapper {
.tooltip {
--_y: 3px;
}
/* RIGHT || INLINE-END */
:has(> :is(.tooltipWrapper.right)):not(:hover, :active) .tooltipWrapper {
:has(> :is(.tooltip.right)):not(:hover, :active) .tooltip {
--_x: calc(var(--isRTL) * -3px * -1);
}
/* BOTTOM || BLOCK-END */
:has(
> :is(
.tooltipWrapper.bottom,
.tooltipWrapper.bottom-left,
.tooltipWrapper.bottom-right
.tooltip.bottom,
.tooltip.tooltip.bottom-left,
.tooltip.bottom-right
)
):not(:hover, :active)
.tooltipWrapper {
.tooltip {
--_y: -3px;
}
/* BOTTOM || BLOCK-END */
:has(> :is(.tooltipWrapper.left)):not(:hover, :active) .tooltipWrapper {
:has(> :is(.tooltip.left)):not(:hover, :active) .tooltip {
--_x: calc(var(--isRTL) * 3px * -1);
}
}

View File

@ -3,6 +3,7 @@
// eslint-disable-next-line css-modules/no-unused-class
import styles from './Tooltip.module.css'
const SIDES = ['top', 'bottom', 'left', 'right'] as const
type TopOrBottom = 'top' | 'bottom'
type LeftOrRight = 'left' | 'right'
type Corner = `${TopOrBottom}-${LeftOrRight}`
@ -10,36 +11,53 @@ type TooltipPosition = TopOrBottom | LeftOrRight | Corner
interface TooltipProps extends React.PropsWithChildren {
position?: TooltipPosition
wrapperClassName?: string
contentClassName?: string
className?: string
delay?: number
hoverOnly?: boolean
inert?: boolean
}
export default function Tooltip({
children,
position = 'top',
wrapperClassName: className,
contentClassName,
className,
delay = 200,
hoverOnly = false,
inert = true,
}: TooltipProps) {
return (
<div
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
inert={inert}
inert="true"
role="tooltip"
className={`p-3 ${
position !== 'left' && position !== 'right' ? 'px-0' : ''
} ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${
className={`${styles.tooltip} ${hoverOnly ? '' : styles.withFocus} ${
styles[position]
} ${className}`}
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
>
<div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}>
{children}
<div className={styles.caret}>
{SIDES.includes(position as any) ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 8 12"
>
<path
fill="currentColor"
d="M3.0513 9.154c.304.9116 1.5935.9116 1.8974 0L8 0H0l3.0513 9.154Z"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 8 10"
>
<path
fill="currentColor"
d="m0 0 6.168 9.252C6.7168 10.0751 8 9.6865 8 8.6971V0H0Z"
/>
</svg>
)}
</div>
</div>
)

View File

@ -117,7 +117,11 @@ export class KclPlugin implements PluginValue {
}
if (!this.client.ready) return
try {
kclManager.executeCode()
} catch (e) {
console.error(e)
}
}
ensureDocUpdated() {

View File

@ -55,7 +55,7 @@ export function useSetupEngineManager(
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true).then(() => {
return kclManager.executeCode(true, true).then(() => {
kclManager.isFirstRender = false
})
},

View File

@ -38,6 +38,22 @@ export class KclManager {
private _wasmInitFailed = true
engineCommandManager: EngineCommandManager
private _defferer = deferExecution((code: string) => {
const ast = this.safeParse(code)
if (!ast) {
this.clearAst()
return
}
try {
const fmtAndStringify = (ast: Program) =>
JSON.stringify(parse(recast(ast)))
const isAstTheSame = fmtAndStringify(ast) === fmtAndStringify(this._ast)
if (isAstTheSame) return
} catch (e) {
console.error(e)
}
this.executeAst(ast)
}, 600)
private _isExecutingCallback: (arg: boolean) => void = () => {}
private _astCallBack: (arg: Program) => void = () => {}
@ -190,6 +206,11 @@ export class KclManager {
const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
// here we're going to clear diagnostics since we're the first
// one in. We're the only location where diagnostics are cleared;
// everything from here on out should be *appending*.
editorManager.clearDiagnostics()
this.isExecuting = true
await this.ensureWasmInit()
const { logs, errors, programMemory } = await executeAst({
@ -267,6 +288,11 @@ export class KclManager {
await this?.engineCommandManager?.waitForReady
this._ast = { ...newAst }
// here we're going to clear diagnostics since we're the first
// one in. We're the only location where diagnostics are cleared;
// everything from here on out should be *appending*.
editorManager.clearDiagnostics()
const { logs, errors, programMemory } = await executeAst({
ast: newAst,
engineCommandManager: this.engineCommandManager,
@ -305,8 +331,10 @@ export class KclManager {
this._cancelTokens.set(key, true)
})
}
async executeCode(zoomToFit?: boolean): Promise<void> {
console.log('[kcl/KclSingleton] executeCode')
async executeCode(force?: boolean, zoomToFit?: boolean): Promise<void> {
// If we want to force it we don't want to defer it.
if (!force) return this._defferer(codeManager.code)
const ast = this.safeParse(codeManager.code)
if (!ast) {
this.clearAst()

View File

@ -7,6 +7,8 @@ import { getNodePathFromSourceRange } from 'lang/queryAst'
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
let lastMessage = ''
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000
@ -56,6 +58,9 @@ function isHighlightSetEntity_type(
type WebSocketResponse = Models['WebSocketResponse_type']
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
type BatchResponseMap = {
[key: string]: Models['BatchResponse_type']
}
type ResultCommand = CommandInfo & {
type: 'result'
@ -1164,15 +1169,6 @@ export enum EngineCommandManagerEvents {
* It also maintains an {@link artifactMap} that keeps track of the state of each
* command, and the artifacts that have been generated by those commands.
*/
interface PendingMessage {
command: EngineCommand
range: SourceRange
idToRangeMap: { [key: string]: SourceRange }
resolve: (data: [Models['WebSocketResponse_type']]) => void
reject: (reason: string) => void
promise: Promise<[Models['WebSocketResponse_type']]>
}
export class EngineCommandManager extends EventTarget {
/**
* The artifactMap is a client-side representation of the commands that have been sent
@ -1186,25 +1182,6 @@ export class EngineCommandManager extends EventTarget {
* of the KCL code that generated it.
*/
artifactMap: ArtifactMap = {}
/**
* The pendingCommands object is a map of the commands that have been sent to the engine that are still waiting on a reply
*/
pendingCommands: {
[commandId: string]: PendingMessage
} = {}
/**
* The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long
* list of the individual commands, this is used to process all the commands into the artifactMap
*/
orderedCommands: {
command: EngineCommand
range: SourceRange
}[] = []
/**
* A map of the responses to the @this.orderedCommands, when processing the commands into the artifactMap, this response map allow
* us to look up the response by command id
*/
responseMap: { [commandId: string]: OkWebSocketResponseData } = {}
/**
* The client-side representation of the scene command artifacts that have been sent to the server;
* that is, the *non-modeling* commands and corresponding artifacts.
@ -1229,7 +1206,7 @@ export class EngineCommandManager extends EventTarget {
defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = []
pendingExport?: {
resolve: (a: null) => void
resolve: (filename?: string) => void
reject: (reason: any) => void
}
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
@ -1458,92 +1435,31 @@ export class EngineCommandManager extends EventTarget {
// export we send a binary blob.
// Pass this to our export function.
exportSave(event.data).then(() => {
this.pendingExport?.resolve(null)
this.pendingExport?.resolve()
}, this.pendingExport?.reject)
return
}
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
const pending = this.pendingCommands[message.request_id || '']
if (pending && !message.success) {
// handle bad case
pending.reject(`engine error: ${JSON.stringify(message.errors)}`)
delete this.pendingCommands[message.request_id || '']
}
} else {
const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (
!(
pending &&
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch')
)
)
return
if (
message.resp.type === 'modeling' &&
pending.command.type === 'modeling_cmd_req' &&
message.resp.type === 'modeling_batch') &&
message.request_id
) {
this.addCommandLog({
type: 'receive-reliable',
data: message.resp,
id: message?.request_id || '',
cmd_type: pending?.command?.cmd?.type,
})
const modelingResponse = message.resp.data.modeling_response
Object.values(
this.subscriptions[modelingResponse.type] || {}
).forEach((callback) => callback(modelingResponse))
this.responseMap[message.request_id] = message.resp
} else if (
message.resp.type === 'modeling_batch' &&
pending.command.type === 'modeling_cmd_batch_req'
) {
let individualPendingResponses: {
[key: string]: Models['WebSocketRequest_type']
} = {}
pending.command.requests.forEach(({ cmd, cmd_id }) => {
individualPendingResponses[cmd_id] = {
type: 'modeling_cmd_req',
cmd,
cmd_id,
}
})
Object.entries(message.resp.data.responses).forEach(
([key, response]) => {
if (!('response' in response)) return
const command = individualPendingResponses[key]
if (!command) return
if (command.type === 'modeling_cmd_req')
this.addCommandLog({
type: 'receive-reliable',
data: {
type: 'modeling',
data: {
modeling_response: response.response,
},
},
id: key,
cmd_type: command?.cmd?.type,
})
this.responseMap[key] = {
type: 'modeling',
data: {
modeling_response: response.response,
},
}
}
this.handleModelingCommand(
message.resp,
message.request_id,
message
)
} else if (
!message.success &&
message.request_id &&
this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message.request_id, message)
}
}
pending.resolve([message])
delete this.pendingCommands[message.request_id || '']
}) as EventListener)
this.onEngineConnectionNewTrack = ({
@ -1569,106 +1485,6 @@ export class EngineCommandManager extends EventTarget {
this.onEngineConnectionStarted
)
}
handleIndividualResponse({
id,
pendingMsg,
response,
}: {
id: string
pendingMsg: {
command: EngineCommand
range: SourceRange
}
response: OkWebSocketResponseData
}) {
const command = pendingMsg
if (command?.command?.type !== 'modeling_cmd_req') return
if (response?.type !== 'modeling') return
const command2 = command.command.cmd
const range = command.range
const pathToNode = getNodePathFromSourceRange(this.getAst(), range)
const getParentId = (): string | undefined => {
if (command2.type === 'extend_path') return command2.path
if (command2.type === 'solid3d_get_extrusion_face_info') {
const edgeArtifact = this.artifactMap[command2.edge_id]
// edges's parent id is to the original "start_path" artifact
if (edgeArtifact && edgeArtifact.parentId) {
return edgeArtifact.parentId
}
}
if (command2.type === 'close_path') return command2.path_id
if (command2.type === 'extrude') return command2.target
// handle other commands that have a parent here
}
const modelingResponse = response.data.modeling_response
if (command) {
const parentId = getParentId()
const artifact = {
type: 'result',
range: range,
pathToNode,
commandType: command.command.cmd.type,
parentId: parentId,
} as ArtifactMapCommand & { extrusions?: string[] }
this.artifactMap[id] = artifact
if (command2.type === 'extrude') {
;(artifact as any).target = command2.target
if (this.artifactMap[command2.target]?.commandType === 'start_path') {
if ((this.artifactMap[command2.target] as any)?.extrusions?.length) {
;(this.artifactMap[command2.target] as any).extrusions.push(id)
} else {
;(this.artifactMap[command2.target] as any).extrusions = [id]
}
}
}
this.artifactMap[id] = artifact
if (
(command2.type === 'entity_linear_pattern' &&
modelingResponse.type === 'entity_linear_pattern') ||
(command2.type === 'entity_circular_pattern' &&
modelingResponse.type === 'entity_circular_pattern')
) {
const entities = modelingResponse.data.entity_ids
entities?.forEach((entity: string) => {
this.artifactMap[entity] = artifact
})
}
if (
command2.type === 'solid3d_get_extrusion_face_info' &&
modelingResponse.type === 'solid3d_get_extrusion_face_info'
) {
const parent = this.artifactMap[parentId || '']
modelingResponse.data.faces.forEach((face) => {
if (face.cap !== 'none' && face.face_id && parent) {
this.artifactMap[face.face_id] = {
...parent,
commandType: 'solid3d_get_extrusion_face_info',
additionalData: {
type: 'cap',
info: face.cap === 'bottom' ? 'start' : 'end',
},
}
}
const curveArtifact = this.artifactMap[face?.curve_id || '']
if (curveArtifact && face?.face_id) {
this.artifactMap[face.face_id] = {
...curveArtifact,
commandType: 'solid3d_get_extrusion_face_info',
}
}
})
}
} else if (command) {
this.artifactMap[id] = {
type: 'result',
commandType: command2.type,
range,
pathToNode,
} as ArtifactMapCommand & { extrusions?: string[] }
}
}
handleResize({
streamWidth,
@ -1693,7 +1509,233 @@ export class EngineCommandManager extends EventTarget {
}
this.engineConnection?.send(resizeCmd)
}
handleModelingCommand(
message: OkWebSocketResponseData,
id: string,
raw: WebSocketResponse
) {
if (!(message.type === 'modeling' || message.type === 'modeling_batch')) {
return
}
const command = this.artifactMap[id]
let modelingResponse: Models['OkModelingCmdResponse_type'] = {
type: 'empty',
}
if ('modeling_response' in message.data) {
modelingResponse = message.data.modeling_response
}
if (
command?.type === 'pending' &&
command.commandType === 'batch' &&
command?.additionalData?.type === 'batch-ids'
) {
if ('responses' in message.data) {
const batchResponse = message.data.responses as BatchResponseMap
// Iterate over the map of responses.
Object.entries(batchResponse).forEach(([key, response]) => {
// If the response is a success, we resolve the promise.
if ('response' in response && response.response) {
this.handleModelingCommand(
{
type: 'modeling',
data: {
modeling_response: response.response,
},
},
key,
{
request_id: key,
resp: {
type: 'modeling',
data: {
modeling_response: response.response,
},
},
success: true,
}
)
} else if ('errors' in response) {
this.handleFailedModelingCommand(key, {
request_id: key,
success: false,
errors: response.errors,
})
}
})
} else {
command.additionalData.ids.forEach((id) => {
this.handleModelingCommand(message, id, raw)
})
}
// batch artifact is just a container, we don't need to keep it
// once we process all the commands inside it
const resolve = command.resolve
delete this.artifactMap[id]
resolve({
id,
commandType: command.commandType,
range: command.range,
raw,
})
return
}
const sceneCommand = this.sceneCommandArtifacts[id]
this.addCommandLog({
type: 'receive-reliable',
data: message,
id,
cmd_type: command?.commandType || sceneCommand?.commandType,
})
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
(callback) => callback(modelingResponse)
)
if (command && command.type === 'pending') {
const resolve = command.resolve
const oldArtifact = this.artifactMap[id] as ArtifactMapCommand & {
extrusions?: string[]
}
const artifact = {
type: 'result',
range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
data: modelingResponse,
raw,
} as ArtifactMapCommand & { extrusions?: string[] }
if (oldArtifact?.extrusions) {
artifact.extrusions = oldArtifact.extrusions
}
this.artifactMap[id] = artifact
if (
(command.commandType === 'entity_linear_pattern' &&
modelingResponse.type === 'entity_linear_pattern') ||
(command.commandType === 'entity_circular_pattern' &&
modelingResponse.type === 'entity_circular_pattern')
) {
const entities = modelingResponse.data.entity_ids
entities?.forEach((entity: string) => {
this.artifactMap[entity] = artifact
})
}
if (
command?.commandType === 'solid3d_get_extrusion_face_info' &&
modelingResponse.type === 'solid3d_get_extrusion_face_info'
) {
const parent = this.artifactMap[command?.parentId || '']
modelingResponse.data.faces.forEach((face) => {
if (face.cap !== 'none' && face.face_id && parent) {
this.artifactMap[face.face_id] = {
...parent,
commandType: 'solid3d_get_extrusion_face_info',
additionalData: {
type: 'cap',
info: face.cap === 'bottom' ? 'start' : 'end',
},
}
}
const curveArtifact = this.artifactMap[face?.curve_id || '']
if (curveArtifact && face?.face_id) {
this.artifactMap[face.face_id] = {
...curveArtifact,
commandType: 'solid3d_get_extrusion_face_info',
}
}
})
}
resolve({
id,
commandType: command.commandType,
range: command.range,
data: modelingResponse,
raw,
})
} else if (sceneCommand && sceneCommand.type === 'pending') {
const resolve = sceneCommand.resolve
const artifact = {
type: 'result',
range: sceneCommand.range,
pathToNode: sceneCommand.pathToNode,
commandType: sceneCommand.commandType,
parentId: sceneCommand.parentId ? sceneCommand.parentId : undefined,
data: modelingResponse,
raw,
} as const
this.sceneCommandArtifacts[id] = artifact
resolve({
id,
commandType: sceneCommand.commandType,
range: sceneCommand.range,
data: modelingResponse,
raw,
})
} else if (command) {
this.artifactMap[id] = {
type: 'result',
commandType: command?.commandType,
range: command?.range,
pathToNode: command?.pathToNode,
data: modelingResponse,
raw,
}
} else {
this.sceneCommandArtifacts[id] = {
type: 'result',
commandType: sceneCommand?.commandType,
range: sceneCommand?.range,
pathToNode: sceneCommand?.pathToNode,
data: modelingResponse,
raw,
}
}
}
handleFailedModelingCommand(id: string, raw: WebSocketResponse) {
const failed = raw as Models['FailureWebSocketResponse_type']
const errors = failed.errors
if (!id) return
const command = this.artifactMap[id]
if (command && command.type === 'pending') {
this.artifactMap[id] = {
type: 'failed',
range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
errors,
}
if (
command?.type === 'pending' &&
command.commandType === 'batch' &&
command?.additionalData?.type === 'batch-ids'
) {
command.additionalData.ids.forEach((id) => {
this.handleFailedModelingCommand(id, raw)
})
}
// batch artifact is just a container, we don't need to keep it
// once we process all the commands inside it
const resolve = command.resolve
delete this.artifactMap[id]
resolve({
id,
commandType: command.commandType,
range: command.range,
errors,
raw,
})
} else {
this.artifactMap[id] = {
type: 'failed',
range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined,
errors,
}
}
}
tearDown(opts?: { idleMode: boolean }) {
if (this.engineConnection) {
this.engineConnection.removeEventListener(
@ -1726,8 +1768,6 @@ export class EngineCommandManager extends EventTarget {
}
async startNewSession() {
this.artifactMap = {}
this.orderedCommands = []
this.responseMap = {}
await this.initPlanes()
}
subscribeTo<T extends ModelTypes>({
@ -1801,13 +1841,13 @@ export class EngineCommandManager extends EventTarget {
sendSceneCommand(
command: EngineCommand,
forceWebsocket = false
): Promise<Models['WebSocketResponse_type'] | null> {
): Promise<any> {
if (this.engineConnection === undefined) {
return Promise.resolve(null)
return Promise.resolve()
}
if (!this.engineConnection?.isReady()) {
return Promise.resolve(null)
return Promise.resolve()
}
if (
@ -1826,13 +1866,19 @@ export class EngineCommandManager extends EventTarget {
})
}
if (
command.type === 'modeling_cmd_req' &&
command.cmd.type !== lastMessage
) {
lastMessage = command.cmd.type
}
if (command.type === 'modeling_cmd_batch_req') {
this.engineConnection?.send(command)
// TODO - handlePendingCommands does not handle batch commands
// return this.handlePendingCommand(command.requests[0].cmd_id, command.cmd)
return Promise.resolve(null)
return Promise.resolve()
}
if (command.type !== 'modeling_cmd_req') return Promise.resolve(null)
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd
if (
(cmd.type === 'camera_drag_move' ||
@ -1845,7 +1891,7 @@ export class EngineCommandManager extends EventTarget {
;(cmd as any).sequence = this.outSequence
this.outSequence++
this.engineConnection?.unreliableSend(command)
return Promise.resolve(null)
return Promise.resolve()
} else if (
cmd.type === 'highlight_set_entity' &&
this.engineConnection?.unreliableDataChannel
@ -1853,7 +1899,7 @@ export class EngineCommandManager extends EventTarget {
cmd.sequence = this.outSequence
this.outSequence++
this.engineConnection?.unreliableSend(command)
return Promise.resolve(null)
return Promise.resolve()
} else if (
cmd.type === 'mouse_move' &&
this.engineConnection.unreliableDataChannel
@ -1861,9 +1907,9 @@ export class EngineCommandManager extends EventTarget {
cmd.sequence = this.outSequence
this.outSequence++
this.engineConnection?.unreliableSend(command)
return Promise.resolve(null)
return Promise.resolve()
} else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => {
const promise = new Promise((resolve, reject) => {
this.pendingExport = { resolve, reject }
})
this.engineConnection?.send(command)
@ -1876,15 +1922,194 @@ export class EngineCommandManager extends EventTarget {
;(cmd as any).sequence = this.outSequence++
}
// since it's not mouse drag or highlighting send over TCP and keep track of the command
return this.sendCommand(command.cmd_id, {
command,
idToRangeMap: {},
range: [0, 0],
}).then(([a]) => a)
this.engineConnection?.send(command)
return this.handlePendingSceneCommand(command.cmd_id, command.cmd)
}
sendModelingCommand({
id,
range,
command,
ast,
idToRangeMap,
}: {
id: string
range: SourceRange
command: EngineCommand
ast: Program
idToRangeMap?: { [key: string]: SourceRange }
}): Promise<ResolveCommand | void> {
if (this.engineConnection === undefined) {
return Promise.resolve()
}
if (!this.engineConnection?.isReady()) {
return Promise.resolve()
}
if (typeof command !== 'string') {
this.addCommandLog({
type: 'send-modeling',
data: command,
})
} else {
this.addCommandLog({
type: 'send-modeling',
data: JSON.parse(command),
})
}
this.engineConnection?.send(command)
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
return this.handlePendingCommand(id, command?.cmd, ast, range)
} else if (
typeof command !== 'string' &&
command.type === 'modeling_cmd_batch_req'
) {
return this.handlePendingBatchCommand(id, command.requests, idToRangeMap)
} else if (typeof command === 'string') {
const parseCommand: EngineCommand = JSON.parse(command)
if (parseCommand.type === 'modeling_cmd_req') {
return this.handlePendingCommand(id, parseCommand?.cmd, ast, range)
} else if (parseCommand.type === 'modeling_cmd_batch_req') {
return this.handlePendingBatchCommand(
id,
parseCommand.requests,
idToRangeMap
)
}
}
return Promise.reject(new Error('Expected unreachable reached'))
}
handlePendingSceneCommand(
id: string,
command: Models['ModelingCmd_type'],
ast?: Program,
range?: SourceRange
) {
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
})
const pathToNode = ast
? getNodePathFromSourceRange(ast, range || [0, 0])
: []
this.sceneCommandArtifacts[id] = {
range: range || [0, 0],
pathToNode,
type: 'pending',
commandType: command.type,
promise,
resolve,
}
return promise
}
handlePendingCommand(
id: string,
command: Models['ModelingCmd_type'],
ast?: Program,
range?: SourceRange
): Promise<ResolveCommand | void> {
let resolve: (val: any) => void = () => {}
const promise: Promise<ResolveCommand | void> = new Promise(
(_resolve, reject) => {
resolve = _resolve
}
)
const getParentId = (): string | undefined => {
if (command.type === 'extend_path') return command.path
if (command.type === 'solid3d_get_extrusion_face_info') {
const edgeArtifact = this.artifactMap[command.edge_id]
// edges's parent id is to the original "start_path" artifact
if (edgeArtifact && edgeArtifact.parentId) {
return edgeArtifact.parentId
}
}
if (command.type === 'close_path') return command.path_id
if (command.type === 'extrude') return command.target
// handle other commands that have a parent here
}
const pathToNode = ast
? getNodePathFromSourceRange(ast, range || [0, 0])
: []
this.artifactMap[id] = {
range: range || [0, 0],
pathToNode,
type: 'pending',
commandType: command.type,
parentId: getParentId(),
promise,
resolve,
}
if (command.type === 'extrude') {
this.artifactMap[id] = {
range: range || [0, 0],
pathToNode,
type: 'pending',
commandType: 'extrude',
parentId: getParentId(),
promise,
target: command.target,
resolve,
}
const target = this.artifactMap[command.target]
if (target.commandType === 'start_path') {
// tsc cannot infer that target can have extrusions
// from the commandType (why?) so we need to cast it
const typedTarget = target as (
| PendingCommand
| ResultCommand
| FailedCommand
) & { extrusions?: string[] }
if (typedTarget?.extrusions?.length) {
typedTarget.extrusions.push(id)
} else {
typedTarget.extrusions = [id]
}
// Update in the map.
this.artifactMap[command.target] = typedTarget
}
}
return promise
}
async handlePendingBatchCommand(
id: string,
commands: Models['ModelingCmdReq_type'][],
idToRangeMap?: { [key: string]: SourceRange },
ast?: Program,
range?: SourceRange
): Promise<ResolveCommand | void> {
let resolve: (val: any) => void = () => {}
const promise: Promise<ResolveCommand | void> = new Promise(
(_resolve, reject) => {
resolve = _resolve
}
)
if (!idToRangeMap) {
return Promise.reject(
new Error('idToRangeMap is required for batch commands')
)
}
// Add the overall batch command to the artifact map just so we can track all of the
// individual commands that are part of the batch.
// we'll delete this artifact once all of the individual commands have been processed.
this.artifactMap[id] = {
range: range || [0, 0],
pathToNode: [],
type: 'pending',
commandType: 'batch',
additionalData: { type: 'batch-ids', ids: commands.map((c) => c.cmd_id) },
parentId: undefined,
promise,
resolve,
}
Promise.all(
commands.map((c) =>
this.handlePendingCommand(c.cmd_id, c.cmd, ast, idToRangeMap[c.cmd_id])
)
)
return promise
}
/**
* A wrapper around the sendCommand where all inputs are JSON strings
*/
async sendModelingCommandFromWasm(
id: string,
rangeStr: string,
@ -1907,88 +2132,53 @@ export class EngineCommandManager extends EventTarget {
return Promise.reject(new Error('commandStr is undefined'))
}
const range: SourceRange = JSON.parse(rangeStr)
const command: EngineCommand = JSON.parse(commandStr)
const idToRangeMap: { [key: string]: SourceRange } =
JSON.parse(idToRangeStr)
const resp = await this.sendCommand(id, {
command,
const command: EngineCommand = JSON.parse(commandStr)
// We only care about the modeling command response.
return this.sendModelingCommand({
id,
range,
command,
ast: this.getAst(),
idToRangeMap,
})
return JSON.stringify(resp[0])
}).then((resp) => {
if (!resp) {
return Promise.reject(
new Error(
'returning modeling cmd response to the rust side is undefined or null'
)
)
}
/**
* Common send command function used for both modeling and scene commands
* So that both have a common way to send pending commands with promises for the responses
*/
async sendCommand(
id: string,
message: {
command: PendingMessage['command']
range: PendingMessage['range']
idToRangeMap: PendingMessage['idToRangeMap']
}
): Promise<[Models['WebSocketResponse_type']]> {
const { promise, resolve, reject } = promiseFactory<any>()
this.pendingCommands[id] = {
resolve,
reject,
promise,
command: message.command,
range: message.range,
idToRangeMap: message.idToRangeMap,
}
if (message.command.type === 'modeling_cmd_req') {
this.orderedCommands.push({
command: message.command,
range: message.range,
})
} else if (message.command.type === 'modeling_cmd_batch_req') {
message.command.requests.forEach((req) => {
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: req.cmd_id,
cmd: req.cmd,
}
this.orderedCommands.push({
command: cmd,
range: message.idToRangeMap[req.cmd_id || ''],
})
return JSON.stringify(resp.raw)
})
}
this.engineConnection?.send(message.command)
return promise
async commandResult(id: string): Promise<any> {
const command = this.artifactMap[id]
if (!command) {
return Promise.reject(new Error('No command found'))
}
/**
* When an execution takes place we want to wait until we've got replies for all of the commands
* When this is done when we build the artifact map synchronously.
*/
async waitForAllCommands() {
if (command.type === 'result') {
return command.data
} else if (command.type === 'failed') {
return Promise.resolve(command.errors)
}
return command.promise
}
async waitForAllCommands(): Promise<{
artifactMap: ArtifactMap
}> {
const pendingCommands = Object.values(this.artifactMap).filter(
({ type }) => type === 'pending'
) as PendingCommand[]
const proms = pendingCommands.map(({ promise }) => promise)
await Promise.all(proms)
const otherPending = Object.values(this.pendingCommands).map(
(a) => a.promise
)
await Promise.all([...proms, otherPending])
this.orderedCommands.forEach(({ command, range }) => {
// expect all to be `modeling_cmd_req` as batch commands have
// already been expanded before being added to orderedCommands
if (command.type !== 'modeling_cmd_req') return
const id = command.cmd_id
const response = this.responseMap[id]
this.handleIndividualResponse({
id,
pendingMsg: {
command,
range,
},
response,
})
})
return {
artifactMap: this.artifactMap,
}
}
private async initPlanes() {
if (this.planesInitialized()) return
@ -2019,7 +2209,7 @@ export class EngineCommandManager extends EventTarget {
this.onPlaneSelectCallback = callback
}
async setPlaneHidden(id: string, hidden: boolean) {
async setPlaneHidden(id: string, hidden: boolean): Promise<string> {
return await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
@ -2060,13 +2250,3 @@ export class EngineCommandManager extends EventTarget {
return undefined
}
}
function promiseFactory<T>() {
let resolve: (value: T | PromiseLike<T>) => void = () => {}
let reject: (value: T | PromiseLike<T>) => void = () => {}
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})
return { promise, resolve, reject }
}

View File

@ -88,7 +88,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
],
Export: {
description: 'Export the current model.',
icon: 'floppyDiskArrow',
icon: 'exportFile',
needsReview: true,
args: {
type: {

View File

@ -105,7 +105,7 @@ export const fileLoader: LoaderFunction = async ({
codeManager.updateCurrentFilePath(current_file_path)
codeManager.updateCodeStateEditor(code)
// We don't want to call await on execute code since we don't want to block the UI
kclManager.executeCode(true)
kclManager.executeCode(true, true)
// Set the file system manager to the project path
// So that WASM gets an updated path for operations

View File

@ -87,7 +87,8 @@ export async function getEventForSelectWithPoint(
// there's plans to get the faceId back from the solid2d creation
// https://github.com/KittyCAD/engine/issues/2094
// at which point we can add it to the artifact map and remove this logic
const resp = await engineCommandManager.sendSceneCommand({
const parentId = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'entity_get_parent_id',
@ -95,12 +96,7 @@ export async function getEventForSelectWithPoint(
},
cmd_id: uuidv4(),
})
const parentId =
resp?.success &&
resp?.resp?.type === 'modeling' &&
resp?.resp?.data?.modeling_response?.type === 'entity_get_parent_id'
? resp?.resp?.data?.modeling_response?.data?.entity_id
: ''
)?.data?.data?.entity_id
const parentArtifact = engineCommandManager.artifactMap[parentId]
if (parentArtifact) {
_artifact = parentArtifact
@ -580,7 +576,8 @@ export async function sendSelectEventToEngine(
el,
...streamDimensions,
})
const res = await engineCommandManager.sendSceneCommand({
const result: Models['SelectWithPoint_type'] = await engineCommandManager
.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'select_with_point',
@ -589,13 +586,8 @@ export async function sendSelectEventToEngine(
},
cmd_id: uuidv4(),
})
if (
res?.success &&
res?.resp?.type === 'modeling' &&
res?.resp?.data?.modeling_response.type === 'select_with_point'
)
return res?.resp?.data?.modeling_response?.data
return { entity_id: '' }
.then((res) => res.data.data)
return result
}
export function updateSelections(

View File

@ -1,714 +0,0 @@
import { CustomIconName } from 'components/CustomIcon'
import { DEV } from 'env'
import { commandBarMachine } from 'machines/commandBarMachine'
import {
canRectangleTool,
isEditingExistingSketch,
modelingMachine,
} from 'machines/modelingMachine'
import { EventFrom, StateFrom } from 'xstate'
export type ToolbarModeName = 'modeling' | 'sketching'
type ToolbarMode = {
check: (state: StateFrom<typeof modelingMachine>) => boolean
items: (ToolbarItem | ToolbarItem[] | 'break')[]
}
export interface ToolbarItemCallbackProps {
modelingStateMatches: StateFrom<typeof modelingMachine>['matches']
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
sketchPathId: string | false
}
export type ToolbarItem = {
id: string
onClick: (props: ToolbarItemCallbackProps) => void
icon?: CustomIconName
status: 'available' | 'unavailable' | 'kcl-only'
disabled?: (state: StateFrom<typeof modelingMachine>) => boolean
disableHotkey?: (state: StateFrom<typeof modelingMachine>) => boolean
title: string | ((props: ToolbarItemCallbackProps) => string)
showTitle?: boolean
hotkey?:
| string
| ((state: StateFrom<typeof modelingMachine>) => string | string[])
description: string
links: { label: string; url: string }[]
isActive?: (state: StateFrom<typeof modelingMachine>) => boolean
}
export type ToolbarItemResolved = Omit<
ToolbarItem,
'disabled' | 'disableHotkey' | 'hotkey' | 'isActive' | 'title'
> & {
title: string
disabled?: boolean
disableHotkey?: boolean
hotkey?: string | string[]
isActive?: boolean
}
export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
modeling: {
check: (state) =>
!(state.matches('Sketch') || state.matches('Sketch no face')),
items: [
{
id: 'sketch',
onClick: ({ modelingSend, sketchPathId }) =>
!sketchPathId
? modelingSend({
type: 'Enter sketch',
data: { forceNewSketch: true },
})
: modelingSend({ type: 'Enter sketch' }),
icon: 'sketch',
status: 'available',
disabled: (state) => !state.matches('idle'),
title: ({ sketchPathId }) =>
`${sketchPathId ? 'Edit' : 'Start'} Sketch`,
showTitle: true,
hotkey: 'S',
description: 'Start drawing a 2D sketch',
links: [
{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/startSketchOn' },
],
},
'break',
{
id: 'extrude',
onClick: ({ commandBarSend }) =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Extrude', groupId: 'modeling' },
}),
disabled: (state) => !state.can('Extrude'),
icon: 'extrude',
status: 'available',
title: 'Extrude',
hotkey: 'E',
description: 'Pull a sketch into 3D along its normal or perpendicular.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/extrude' }],
},
{
id: 'revolve',
onClick: () => console.error('Revolve not yet implemented'),
icon: 'revolve',
status: 'kcl-only',
title: 'Revolve',
hotkey: 'R',
description:
'Create a 3D body by rotating a sketch region about an axis.',
links: [
{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/revolve' },
{
label: 'KCL example',
url: 'https://zoo.dev/docs/kcl-samples/ball-bearing',
},
],
},
{
id: 'sweep',
onClick: () => console.error('Sweep not yet implemented'),
icon: 'sweep',
status: 'unavailable',
title: 'Sweep',
hotkey: 'W',
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
links: [
{
label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/498',
},
],
},
{
id: 'loft',
onClick: () => console.error('Loft not yet implemented'),
icon: 'loft',
status: 'unavailable',
title: 'Loft',
hotkey: 'L',
description:
'Create a 3D body by blending between two or more sketches.',
links: [
{
label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/613',
},
],
},
'break',
{
id: 'fillet3d',
onClick: ({ commandBarSend }) =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
}),
icon: 'fillet3d',
status: DEV ? 'available' : 'kcl-only',
disabled: (state) => !state.can('Fillet'),
title: 'Fillet',
hotkey: 'F',
description: 'Round the edges of a 3D solid.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/fillet' }],
},
{
id: 'chamfer',
onClick: () => console.error('Chamfer not yet implemented'),
icon: 'chamfer3d',
status: 'kcl-only',
title: 'Chamfer',
hotkey: 'C',
description: 'Bevel the edges of a 3D solid.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/chamfer' }],
},
{
id: 'shell',
onClick: () => console.error('Shell not yet implemented'),
icon: 'shell',
status: 'kcl-only',
title: 'Shell',
description: 'Hollow out a 3D solid.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/shell' }],
},
{
id: 'hole',
onClick: () => console.error('Hole not yet implemented'),
icon: 'hole',
status: 'unavailable',
title: 'Hole',
description: 'Create a hole in a 3D solid.',
links: [],
},
'break',
[
{
id: 'boolean-union',
onClick: () => console.error('Boolean union not yet implemented'),
icon: 'booleanUnion',
status: 'unavailable',
title: 'Union',
hotkey: 'Shift + B U',
description: 'Combine two or more solids into a single solid.',
links: [
{
label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/509',
},
],
},
{
id: 'boolean-subtract',
onClick: () => console.error('Boolean subtract not yet implemented'),
icon: 'booleanSubtract',
status: 'unavailable',
title: 'Subtract',
hotkey: 'Shift + B S',
description: 'Subtract one solid from another.',
links: [
{
label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/510',
},
],
},
{
id: 'boolean-intersect',
onClick: () => console.error('Boolean intersect not yet implemented'),
icon: 'booleanIntersect',
status: 'unavailable',
title: 'Intersect',
hotkey: 'Shift + B I',
description: 'Create a solid from the intersection of two solids.',
links: [
{
label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/511',
},
],
},
],
[
{
id: 'plane-offset',
onClick: () =>
console.error('Plane through normal not yet implemented'),
icon: 'plane',
status: 'unavailable',
title: 'Offset plane',
description: 'Create a plane parallel to an existing plane.',
links: [],
},
{
id: 'plane-points',
onClick: () =>
console.error('Plane through points not yet implemented'),
status: 'unavailable',
title: '3-point plane',
description: 'Create a plane from three points.',
links: [],
},
],
],
},
sketching: {
check: (state) =>
state.matches('Sketch') || state.matches('Sketch no face'),
items: [
{
id: 'sketch-exit',
onClick: ({ modelingSend }) =>
modelingSend({
type: 'Cancel',
}),
disableHotkey: (state) =>
!(
state.matches('Sketch.SketchIdle') ||
state.matches('Sketch no face')
),
icon: 'arrowLeft',
status: 'available',
title: 'Exit sketch',
showTitle: true,
hotkey: 'Esc',
description: 'Exit the current sketch',
links: [],
},
'break',
{
id: 'line',
onClick: ({ modelingStateMatches: matches, modelingSend }) =>
modelingSend({
type: 'change tool',
data: {
tool: !matches('Sketch.Line tool') ? 'line' : 'none',
},
}),
icon: 'line',
status: 'available',
disabled: (state) =>
state.matches('Sketch no face') ||
state.matches('Sketch.Rectangle tool.Awaiting second corner'),
title: 'Line',
hotkey: (state) =>
state.matches('Sketch.Line tool') ? ['Esc', 'L'] : 'L',
description: 'Start drawing straight lines',
links: [],
isActive: (state) => state.matches('Sketch.Line tool'),
},
[
{
id: 'tangential-arc',
onClick: ({ modelingStateMatches, modelingSend }) =>
modelingSend({
type: 'change tool',
data: {
tool: !modelingStateMatches('Sketch.Tangential arc to')
? 'tangentialArc'
: 'none',
},
}),
icon: 'arc',
status: 'available',
disabled: (state) =>
!isEditingExistingSketch(state.context) &&
!state.matches('Sketch.Tangential arc to'),
title: 'Tangential Arc',
hotkey: (state) =>
state.matches('Sketch.Tangential arc to') ? ['Esc', 'A'] : 'A',
description: 'Start drawing an arc tangent to the current segment',
links: [],
isActive: (state) => state.matches('Sketch.Tangential arc to'),
},
{
id: 'three-point-arc',
onClick: () => console.error('Three-point arc not yet implemented'),
icon: 'arc',
status: 'unavailable',
title: 'Three-point Arc',
showTitle: false,
description: 'Draw a circular arc defined by three points',
links: [
{
label: 'GitHub issue',
url: 'https://github.com/KittyCAD/modeling-app/issues/1659',
},
],
},
],
{
id: 'spline',
onClick: () => console.error('Spline not yet implemented'),
icon: 'spline',
status: 'unavailable',
title: 'Spline',
showTitle: false,
description: 'Draw a spline curve through a series of points',
links: [],
},
'break',
[
{
id: 'circle-center',
onClick: () => console.error('Center circle not yet implemented'),
icon: 'circle',
status: 'unavailable',
title: 'Center circle',
showTitle: false,
description: 'Start drawing a circle from its center',
links: [
{
label: 'GitHub issue',
url: 'https://github.com/KittyCAD/modeling-app/issues/1501',
},
],
},
{
id: 'circle-three-points',
onClick: () =>
console.error('Three-point circle not yet implemented'),
icon: 'circle',
status: 'unavailable',
disabled: () => true,
title: 'Three-point circle',
showTitle: false,
description: 'Draw a circle defined by three points',
links: [],
},
],
[
{
id: 'corner-rectangle',
onClick: ({ modelingStateMatches, modelingSend }) =>
modelingSend({
type: 'change tool',
data: {
tool: !modelingStateMatches('Sketch.Rectangle tool')
? 'rectangle'
: 'none',
},
}),
icon: 'rectangle',
status: 'available',
disabled: (state) =>
!canRectangleTool(state.context) &&
!state.matches('Sketch.Rectangle tool'),
title: 'Corner rectangle',
hotkey: (state) =>
state.matches('Sketch.Rectangle tool') ? ['Esc', 'R'] : 'R',
description: 'Start drawing a rectangle',
links: [],
isActive: (state) => state.matches('Sketch.Rectangle tool'),
},
{
id: 'center-rectangle',
onClick: () => console.error('Center rectangle not yet implemented'),
icon: 'rectangle',
status: 'unavailable',
title: 'Center rectangle',
showTitle: false,
description: 'Start drawing a rectangle from its center',
links: [],
},
],
{
id: 'polygon',
onClick: () => console.error('Polygon not yet implemented'),
icon: 'polygon',
status: 'unavailable',
title: 'Polygon',
showTitle: false,
description: 'Draw a polygon with a specified number of sides',
links: [],
},
{
id: 'text',
onClick: () => console.error('Text not yet implemented'),
icon: 'text',
status: 'unavailable',
title: 'Text',
showTitle: false,
description: 'Add text to your sketch as geometry.',
links: [],
},
'break',
{
id: 'mirror',
onClick: () => console.error('Mirror not yet implemented'),
icon: 'mirror',
status: 'unavailable',
title: 'Mirror',
showTitle: false,
description: 'Mirror sketch entities about a line or axis',
links: [],
},
[
{
id: 'constraint-length',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain length') &&
state.can('Constrain length')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain length' }),
icon: 'dimension',
status: 'available',
title: 'Length',
showTitle: false,
description: 'Constrain the length of a straight segment',
links: [],
},
{
id: 'constraint-angle',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain angle') &&
state.can('Constrain angle')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain angle' }),
status: 'available',
title: 'Angle',
showTitle: false,
description: 'Constrain the angle between two segments',
links: [],
},
{
id: 'constraint-vertical',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Make segment vertical') &&
state.can('Make segment vertical')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Make segment vertical' }),
status: 'available',
title: 'Vertical',
showTitle: false,
description:
'Constrain a straight segment to be vertical relative to the sketch',
links: [],
},
{
id: 'constraint-horizontal',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Make segment horizontal') &&
state.can('Make segment horizontal')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Make segment horizontal' }),
status: 'available',
title: 'Horizontal',
showTitle: false,
description:
'Constrain a straight segment to be horizontal relative to the sketch',
links: [],
},
{
id: 'constraint-parallel',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain parallel') &&
state.can('Constrain parallel')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain parallel' }),
status: 'available',
title: 'Parallel',
showTitle: false,
description: 'Constrain two segments to be parallel',
links: [],
},
{
id: 'constraint-equal-length',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain equal length') &&
state.can('Constrain equal length')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain equal length' }),
status: 'available',
title: 'Equal length',
showTitle: false,
description: 'Constrain two segments to be equal length',
links: [],
},
{
id: 'constraint-horizontal-distance',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain horizontal distance') &&
state.can('Constrain horizontal distance')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain horizontal distance' }),
status: 'available',
title: 'Horizontal distance',
showTitle: false,
description: 'Constrain the horizontal distance between two points',
links: [],
},
{
id: 'constraint-vertical-distance',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain vertical distance') &&
state.can('Constrain vertical distance')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain vertical distance' }),
status: 'available',
title: 'Vertical distance',
showTitle: false,
description: 'Constrain the vertical distance between two points',
links: [],
},
{
id: 'constraint-absolute-x',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain ABS X') &&
state.can('Constrain ABS X')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain ABS X' }),
status: 'available',
title: 'Absolute X',
showTitle: false,
description: 'Constrain the x-coordinate of a point',
links: [],
},
{
id: 'constraint-absolute-y',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain ABS Y') &&
state.can('Constrain ABS Y')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain ABS Y' }),
status: 'available',
title: 'Absolute Y',
showTitle: false,
description: 'Constrain the y-coordinate of a point',
links: [],
},
{
id: 'constraint-perpendicular-distance',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain perpendicular distance') &&
state.can('Constrain perpendicular distance')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain perpendicular distance' }),
status: 'available',
title: 'Perpendicular distance',
showTitle: false,
description:
'Constrain the perpendicular distance between two segments',
links: [],
},
{
id: 'constraint-align-horizontal',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain horizontally align') &&
state.can('Constrain horizontally align')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain horizontally align' }),
status: 'available',
title: 'Horizontally align',
showTitle: false,
description: 'Align the ends of two or more segments horizontally',
links: [],
},
{
id: 'constraint-align-vertical',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain vertically align') &&
state.can('Constrain vertically align')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain vertically align' }),
status: 'available',
title: 'Vertically align',
showTitle: false,
description: 'Align the ends of two or more segments vertically',
links: [],
},
{
id: 'snap-to-x',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain snap to X') &&
state.can('Constrain snap to X')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain snap to X' }),
status: 'available',
title: 'Snap to X',
showTitle: false,
description: 'Snap a point to an x-coordinate',
links: [],
},
{
id: 'snap-to-y',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain snap to Y') &&
state.can('Constrain snap to Y')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain snap to Y' }),
status: 'available',
title: 'Snap to Y',
showTitle: false,
description: 'Snap a point to a y-coordinate',
links: [],
},
{
id: 'constraint-remove',
disabled: (state) =>
!(
state.matches('Sketch.SketchIdle') &&
state.nextEvents.includes('Constrain remove constraints') &&
state.can('Constrain remove constraints')
),
onClick: ({ modelingSend }) =>
modelingSend({ type: 'Constrain remove constraints' }),
status: 'available',
title: 'Remove constraints',
showTitle: false,
description: 'Remove all constraints from the segment',
links: [],
},
],
],
},
}

View File

@ -56,7 +56,7 @@ function OnboardingWithNewFile() {
next={() => {
// We do want to update both the state and editor here.
codeManager.updateCodeEditor(bracket)
kclManager.executeCode(true)
kclManager.executeCode(true, true)
next()
}}
nextText="Overwrite code and continue"

View File

@ -14,7 +14,7 @@ export default function Sketching() {
codeManager.updateCodeEditor('')
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
// If the engine is ready, promptly execute the loaded code
kclManager.executeCode(true)
kclManager.executeCode(true, true)
}
}, [])

View File

@ -2880,6 +2880,30 @@ impl BinaryExpression {
pipe_info: &PipeInfo,
ctx: &ExecutorContext,
) -> Result<MemoryItem, KclError> {
// First check if we are doing short-circuiting logical operator.
if self.operator == BinaryOperator::LogicalOr {
let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let left = json_to_bool(&left_json_value);
if left {
// Short-circuit.
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(left),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let right = json_to_bool(&right_json_value);
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(right),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
@ -2909,6 +2933,9 @@ impl BinaryExpression {
BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::LogicalOr => {
unreachable!("LogicalOr should have been handled above")
}
};
Ok(MemoryItem::UserVal(UserVal {
@ -2950,6 +2977,27 @@ pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
}
}
pub fn json_to_bool(j: &serde_json::Value) -> bool {
match j {
JValue::Null => false,
JValue::Bool(b) => *b,
JValue::Number(n) => {
if let Some(n) = n.as_u64() {
n != 0
} else if let Some(n) = n.as_i64() {
n != 0
} else if let Some(x) = n.as_f64() {
x != 0.0 && !x.is_nan()
} else {
false
}
}
JValue::String(s) => !s.is_empty(),
JValue::Array(a) => !a.is_empty(),
JValue::Object(_) => false,
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
@ -2980,6 +3028,10 @@ pub enum BinaryOperator {
#[serde(rename = "^")]
#[display("^")]
Pow,
/// Logical OR.
#[serde(rename = "||")]
#[display("||")]
LogicalOr,
}
/// Mathematical associativity.
@ -3008,6 +3060,7 @@ impl BinaryOperator {
BinaryOperator::Div => *b"div",
BinaryOperator::Mod => *b"mod",
BinaryOperator::Pow => *b"pow",
BinaryOperator::LogicalOr => *b"lor",
}
}
@ -3018,6 +3071,7 @@ impl BinaryOperator {
BinaryOperator::Add | BinaryOperator::Sub => 11,
BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12,
BinaryOperator::Pow => 6,
BinaryOperator::LogicalOr => 3,
}
}
@ -3025,7 +3079,7 @@ impl BinaryOperator {
/// Taken from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table>
pub fn associativity(&self) -> Associativity {
match self {
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod => Associativity::Left,
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod | Self::LogicalOr => Associativity::Left,
Self::Pow => Associativity::Right,
}
}
@ -3089,6 +3143,21 @@ impl UnaryExpression {
pipe_info: &PipeInfo,
ctx: &ExecutorContext,
) -> Result<MemoryItem, KclError> {
if self.operator == UnaryOperator::Not {
let value = self
.argument
.get_result(memory, pipe_info, ctx)
.await?
.get_json_value()?;
let negated = !json_to_bool(&value);
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let num = parse_json_number_as_f64(
&self
.argument

View File

@ -2513,6 +2513,57 @@ let shape = layer() |> patternTransform(10, transform, %)"#;
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_ycombinator_is_even() {
let ast = r#"
// Heavily inspired by: https://raganwald.com/2018/09/10/why-y.html
fn why = (f) => {
fn inner = (maker) => {
fn inner2 = (x) => {
return f(maker(maker), x)
}
return inner2
}
return inner(
(maker) => {
fn inner2 = (x) => {
return f(maker(maker), x)
}
return inner2
}
)
}
fn innerIsEven = (self, n) => {
return !n || !self(n - 1)
}
const isEven = why(innerIsEven)
const two = isEven(2)
const three = isEven(3)
"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(true),
memory
.get("two", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(false),
memory
.get("three", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;

View File

@ -223,8 +223,7 @@ impl Backend {
#[cfg(not(test))]
let mut completion_list = vec![];
// if self.dev_mode
if false {
if self.dev_mode {
completion_list.push(
r#"fn cube = (pos, scale) => {
const sg = startSketchOn('XY')

View File

@ -299,6 +299,7 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
"*" => BinaryOperator::Mul,
"%" => BinaryOperator::Mod,
"^" => BinaryOperator::Pow,
"||" => BinaryOperator::LogicalOr,
_ => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
@ -1136,11 +1137,11 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
let (operator, op_token) = any
.try_map(|token: Token| match token.token_type {
TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)),
// TODO: negation. Original parser doesn't support `not` yet.
TokenType::Operator => Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)", token.value.as_str(),),
})),
TokenType::Bang => Ok((UnaryOperator::Not, token)),
other => Err(KclError::Syntax(KclErrorDetails { source_ranges: token.as_source_ranges(), message: format!("{EXPECTED} but found {} which is {}", token.value.as_str(), other,) })),
})
.context(expected("a unary expression, e.g. -x or -3"))

View File

@ -79,7 +79,7 @@ impl From<ParseError<&[Token], ContextError>> for KclError {
// See https://github.com/KittyCAD/modeling-app/issues/784
KclError::Syntax(KclErrorDetails {
source_ranges: bad_token.as_source_ranges(),
message: "Unexpected token".to_string(),
message: format!("Unexpected token: {}", bad_token.value),
})
}
}

View File

@ -90,7 +90,7 @@ fn word(i: &mut Located<&str>) -> PResult<Token> {
fn operator(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = alt((
">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^",
">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "||", "|", "^",
))
.with_span()
.parse_next(i)?;