Compare commits
12 Commits
v0.33.0
...
jtran/per-
Author | SHA1 | Date | |
---|---|---|---|
9b177b9cbd | |||
bb51646738 | |||
c02e31a530 | |||
1d06cc7845 | |||
e0c07eecfe | |||
c5d42500fa | |||
e6e47f77f0 | |||
662c2485ac | |||
9f891deebb | |||
d08a07a1f8 | |||
872b196a86 | |||
d535a2862d |
@ -149,7 +149,7 @@ test.describe('Basic sketch', () => {
|
||||
await doBasicSketch(page, homePage, ['code'])
|
||||
})
|
||||
|
||||
test.fixme('code pane closed at start', async ({ page, homePage }) => {
|
||||
test('code pane closed at start', async ({ page, homePage }) => {
|
||||
// Load the app with the code panes
|
||||
await page.addInitScript(async (persistModelingContext) => {
|
||||
localStorage.setItem(
|
||||
|
@ -76,7 +76,7 @@ test.describe('Editor tests', () => {
|
||||
await u.openDebugPanel()
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(2)
|
||||
).toHaveCount(1)
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(2)
|
||||
@ -100,7 +100,60 @@ test.describe('Editor tests', () => {
|
||||
).toHaveCount(3)
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('ensure we use the cache, and do not clear on append', async ({
|
||||
homePage,
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)`)
|
||||
|
||||
// Ensure we execute the first time.
|
||||
await u.openDebugPanel()
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(1)
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(2)
|
||||
|
||||
// Add whitespace to the end of the code.
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const x = 1')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await u.openDebugPanel()
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(3)
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('if you click the format button it formats your code', async ({
|
||||
|
127
e2e/playwright/feature-tree-pane.spec.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { test, expect } from './zoo-test'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
const FEATURE_TREE_EXAMPLE_CODE = `export fn timesFive(x) {
|
||||
return 5 * x
|
||||
}
|
||||
export fn triangle() {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(10, %)
|
||||
|> line([-10, -5], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
}
|
||||
|
||||
length001 = timesFive(1) * 5
|
||||
sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([20, 10], %)
|
||||
|> line([10, 10], %)
|
||||
|> angledLine([-45, length001], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
revolve001 = revolve({ axis = "X" }, sketch001)
|
||||
triangle()
|
||||
|> extrude(30, %)
|
||||
plane001 = offsetPlane('XY', 10)
|
||||
sketch002 = startSketchOn(plane001)
|
||||
|> startProfileAt([-20, 0], %)
|
||||
|> line([5, -15], %)
|
||||
|> xLine(-10, %)
|
||||
|> lineTo([-40, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(10, sketch002)
|
||||
`
|
||||
|
||||
test.describe('Feature Tree pane', () => {
|
||||
test(
|
||||
'User can go to definition and go to function definition',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, homePage, scene, editor, toolbar }) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const bracketDir = join(dir, 'test-sample')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.writeFile(
|
||||
join(bracketDir, 'main.kcl'),
|
||||
FEATURE_TREE_EXAMPLE_CODE,
|
||||
'utf-8'
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('setup test', async () => {
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: 'test-sample',
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await homePage.openProject('test-sample')
|
||||
await scene.waitForExecutionDone()
|
||||
await editor.closePane()
|
||||
await toolbar.openFeatureTreePane()
|
||||
})
|
||||
|
||||
async function testViewSource({
|
||||
operationName,
|
||||
operationIndex,
|
||||
expectedActiveLine,
|
||||
}: {
|
||||
operationName: string
|
||||
operationIndex: number
|
||||
expectedActiveLine: string
|
||||
}) {
|
||||
await test.step(`Go to definition of the ${operationName}`, async () => {
|
||||
await toolbar.viewSourceOnOperation(operationName, operationIndex)
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: [expectedActiveLine],
|
||||
})
|
||||
await expect(
|
||||
editor.activeLine.first(),
|
||||
`${operationName} code should be scrolled into view`
|
||||
).toBeVisible()
|
||||
})
|
||||
}
|
||||
|
||||
await testViewSource({
|
||||
operationName: 'Offset Plane',
|
||||
operationIndex: 0,
|
||||
expectedActiveLine: "plane001 = offsetPlane('XY', 10)",
|
||||
})
|
||||
await testViewSource({
|
||||
operationName: 'Extrude',
|
||||
operationIndex: 1,
|
||||
expectedActiveLine: 'extrude001 = extrude(10, sketch002)',
|
||||
})
|
||||
await testViewSource({
|
||||
operationName: 'Revolve',
|
||||
operationIndex: 0,
|
||||
expectedActiveLine: 'revolve001 = revolve({ axis = "X" }, sketch001)',
|
||||
})
|
||||
await testViewSource({
|
||||
operationName: 'Triangle',
|
||||
operationIndex: 0,
|
||||
expectedActiveLine: 'triangle()',
|
||||
})
|
||||
|
||||
await test.step('Go to definition on the triangle function', async () => {
|
||||
await toolbar.goToDefinitionOnOperation('Triangle', 0)
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: ['export fn triangle() {'],
|
||||
})
|
||||
await expect(
|
||||
editor.activeLine.first(),
|
||||
'Triangle function definition should be scrolled into view'
|
||||
).toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
type CmdBarSerialised =
|
||||
@ -26,9 +26,11 @@ type CmdBarSerialised =
|
||||
|
||||
export class CmdBarFixture {
|
||||
public page: Page
|
||||
cmdBarOpenBtn!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
this.cmdBarOpenBtn = page.getByTestId('command-bar-open-button')
|
||||
}
|
||||
reConstruct = (page: Page) => {
|
||||
this.page = page
|
||||
@ -116,4 +118,21 @@ export class CmdBarFixture {
|
||||
await this.page.keyboard.press('Enter')
|
||||
}
|
||||
}
|
||||
|
||||
openCmdBar = async (selectCmd?: 'promptToEdit') => {
|
||||
// TODO why does this button not work in electron tests?
|
||||
// await this.cmdBarOpenBtn.click()
|
||||
await this.page.keyboard.down('ControlOrMeta')
|
||||
await this.page.keyboard.press('KeyK')
|
||||
await this.page.keyboard.up('ControlOrMeta')
|
||||
await expect(this.page.getByPlaceholder('Search commands')).toBeVisible()
|
||||
if (selectCmd === 'promptToEdit') {
|
||||
const promptEditCommand = this.page.getByText(
|
||||
'Use Zoo AI to edit your kcl'
|
||||
)
|
||||
await expect(promptEditCommand.first()).toBeVisible()
|
||||
await promptEditCommand.first().scrollIntoViewIfNeeded()
|
||||
await promptEditCommand.first().click()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export class EditorFixture {
|
||||
private diagnosticsTooltip!: Locator
|
||||
private diagnosticsGutterIcon!: Locator
|
||||
private codeContent!: Locator
|
||||
private activeLine!: Locator
|
||||
public activeLine!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
|
@ -1,6 +1,13 @@
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import { expect } from '../zoo-test'
|
||||
import { doAndWaitForImageDiff } from '../test-utils'
|
||||
import {
|
||||
checkIfPaneIsOpen,
|
||||
closePane,
|
||||
doAndWaitForImageDiff,
|
||||
openPane,
|
||||
} from '../test-utils'
|
||||
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
|
||||
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
||||
|
||||
export class ToolbarFixture {
|
||||
public page: Page
|
||||
@ -20,6 +27,10 @@ export class ToolbarFixture {
|
||||
filePane!: Locator
|
||||
exeIndicator!: Locator
|
||||
treeInputField!: Locator
|
||||
/** The sidebar button for the Feature Tree pane */
|
||||
featureTreeId = 'feature-tree' as const
|
||||
/** The pane element for the Feature Tree */
|
||||
featureTreePane!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
@ -41,6 +52,7 @@ export class ToolbarFixture {
|
||||
this.treeInputField = page.getByTestId('tree-input-field')
|
||||
|
||||
this.filePane = page.locator('#files-pane')
|
||||
this.featureTreePane = page.locator('#feature-tree-pane')
|
||||
this.fileCreateToast = page.getByText('Successfully created')
|
||||
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
|
||||
}
|
||||
@ -91,4 +103,76 @@ export class ToolbarFixture {
|
||||
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
|
||||
}
|
||||
}
|
||||
|
||||
async closePane(paneId: SidebarType) {
|
||||
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)
|
||||
}
|
||||
async openPane(paneId: SidebarType) {
|
||||
return openPane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)
|
||||
}
|
||||
async checkIfPaneIsOpen(paneId: SidebarType) {
|
||||
return checkIfPaneIsOpen(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)
|
||||
}
|
||||
|
||||
async openFeatureTreePane() {
|
||||
return this.openPane(this.featureTreeId)
|
||||
}
|
||||
async closeFeatureTreePane() {
|
||||
await this.closePane(this.featureTreeId)
|
||||
}
|
||||
async checkIfFeatureTreePaneIsOpen() {
|
||||
return this.checkIfPaneIsOpen(this.featureTreeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific operation button from the Feature Tree pane
|
||||
*/
|
||||
async getFeatureTreeOperation(operationName: string, operationIndex: number) {
|
||||
await this.openFeatureTreePane()
|
||||
await expect(this.featureTreePane).toBeVisible()
|
||||
return this.featureTreePane
|
||||
.getByRole('button', {
|
||||
name: operationName,
|
||||
})
|
||||
.nth(operationIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* View source on a specific operation in the Feature Tree pane.
|
||||
* @param operationName The name of the operation type
|
||||
* @param operationIndex The index out of operations of this type
|
||||
*/
|
||||
async viewSourceOnOperation(operationName: string, operationIndex: number) {
|
||||
const operationButton = await this.getFeatureTreeOperation(
|
||||
operationName,
|
||||
operationIndex
|
||||
)
|
||||
const viewSourceMenuButton = this.page.getByRole('button', {
|
||||
name: 'View KCL source code',
|
||||
})
|
||||
|
||||
await operationButton.click({ button: 'right' })
|
||||
await expect(viewSourceMenuButton).toBeVisible()
|
||||
await viewSourceMenuButton.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to definition on a specific operation in the Feature Tree pane
|
||||
*/
|
||||
async goToDefinitionOnOperation(
|
||||
operationName: string,
|
||||
operationIndex: number
|
||||
) {
|
||||
const operationButton = await this.getFeatureTreeOperation(
|
||||
operationName,
|
||||
operationIndex
|
||||
)
|
||||
const goToDefinitionMenuButton = this.page.getByRole('button', {
|
||||
name: 'View function definition',
|
||||
})
|
||||
|
||||
await operationButton.click({ button: 'right' })
|
||||
await expect(goToDefinitionMenuButton).toBeVisible()
|
||||
await goToDefinitionMenuButton.click()
|
||||
}
|
||||
}
|
||||
|
190
e2e/playwright/prompt-to-edit.spec.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import { test, expect } from './zoo-test'
|
||||
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
|
||||
const file = `sketch001 = startSketchOn('XZ')
|
||||
profile001 = startProfileAt([57.81, 250.51], sketch001)
|
||||
|> line([121.13, 56.63], %, $seg02)
|
||||
|> line([83.37, -34.61], %, $seg01)
|
||||
|> line([19.66, -116.4], %)
|
||||
|> line([-221.8, -41.69], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(200, profile001)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([-73.64, -42.89], %)
|
||||
|> xLine(173.71, %)
|
||||
|> line([-22.12, -94.4], %)
|
||||
|> xLine(-156.98, %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude002 = extrude(50, sketch002)
|
||||
sketch003 = startSketchOn('XY')
|
||||
|> startProfileAt([52.92, 157.81], %)
|
||||
|> angledLine([0, 176.4], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
53.4
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude003 = extrude(20, sketch003)
|
||||
`
|
||||
|
||||
test.describe('Check the happy path, for basic changing color', () => {
|
||||
const cases = [
|
||||
{
|
||||
desc: 'User accepts change',
|
||||
shouldReject: false,
|
||||
},
|
||||
{
|
||||
desc: 'User rejects change',
|
||||
shouldReject: true,
|
||||
},
|
||||
] as const
|
||||
for (const { desc, shouldReject } of cases) {
|
||||
test(`${desc}`, async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
page,
|
||||
scene,
|
||||
}) => {
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
const greenCheckCoords = { x: 565, y: 345 }
|
||||
const body2WallCoords = { x: 609, y: 153 }
|
||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||
body1CapCoords.x,
|
||||
body1CapCoords.y
|
||||
)
|
||||
const yellow: [number, number, number] = [179, 179, 131]
|
||||
const green: [number, number, number] = [108, 152, 75]
|
||||
const notGreen: [number, number, number] = [132, 132, 132]
|
||||
const body2NotGreen: [number, number, number] = [88, 88, 88]
|
||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||
const successToast = page.getByText('Prompt to edit successful')
|
||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
||||
const rejectBtn = page.getByRole('button', { name: 'close Reject' })
|
||||
|
||||
await test.step('wait for scene to load select body and check selection came through', async () => {
|
||||
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
||||
await clickBody1Cap()
|
||||
await scene.expectPixelColor(yellow, body1CapCoords, 20)
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('fire off edit prompt', async () => {
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
// being specific about the color with a hex means asserting pixel color is more stable
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('make this neon green please, use #39FF14')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(submittingToast).toBeVisible()
|
||||
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while
|
||||
await expect(successToast).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('verify initial change', async () => {
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
|
||||
await editor.expectEditor.toContain('appearance({')
|
||||
})
|
||||
|
||||
if (!shouldReject) {
|
||||
await test.step('check accept works and can be "undo"ed', async () => {
|
||||
await acceptBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||
await editor.expectEditor.toContain('appearance({')
|
||||
|
||||
// ctrl-z works after accepting
|
||||
await page.keyboard.down('ControlOrMeta')
|
||||
await page.keyboard.press('KeyZ')
|
||||
await page.keyboard.up('ControlOrMeta')
|
||||
await editor.expectEditor.not.toContain('appearance({')
|
||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
||||
})
|
||||
} else {
|
||||
await test.step('check reject works', async () => {
|
||||
await rejectBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
|
||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
||||
await editor.expectEditor.not.toContain('appearance({')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('bad path', () => {
|
||||
test(`bad edit prompt`, async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
toolbar,
|
||||
page,
|
||||
scene,
|
||||
}) => {
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||
body1CapCoords.x,
|
||||
body1CapCoords.y
|
||||
)
|
||||
const yellow: [number, number, number] = [179, 179, 131]
|
||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||
const failToast = page.getByText(
|
||||
'Failed to edit your KCL code, please try again with a different prompt or selection'
|
||||
)
|
||||
|
||||
await test.step('wait for scene to load and select body', async () => {
|
||||
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
||||
|
||||
await clickBody1Cap()
|
||||
await scene.expectPixelColor(yellow, body1CapCoords, 20)
|
||||
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('fire of bad prompt', async () => {
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('ansheusha asnthuatshoeuhtaoetuhthaeu laughs in dvorak')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(submittingToast).toBeVisible()
|
||||
})
|
||||
await test.step('check fail toast appeared', async () => {
|
||||
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while
|
||||
await expect(failToast).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
@ -82,19 +82,16 @@ test.describe('Sketch tests', () => {
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.getByText(selectionsSnippets.startProfileAt1).click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
|
||||
await page.getByText(selectionsSnippets.startProfileAt2).click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
|
||||
await page.getByText(selectionsSnippets.startProfileAt3).click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
|
@ -375,6 +375,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => {
|
||||
await u.closeKclCodePanel()
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
})
|
||||
await u.openKclCodePanel()
|
||||
}
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
@ -486,7 +486,7 @@ test.describe('Testing selections', () => {
|
||||
await u.clearCommandLogs()
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
await expect(page.getByText('Unable to delete part')).toBeVisible()
|
||||
await expect(page.getByText('Unable to delete selection')).toBeVisible()
|
||||
})
|
||||
test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({
|
||||
page,
|
||||
@ -874,17 +874,15 @@ test.describe('Testing selections', () => {
|
||||
}
|
||||
const clickEmpty = () => page.mouse.click(700, 460)
|
||||
await selectUnExtrudable()
|
||||
// expect extrude button to be disabled
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
// expect extrude button to be enabled, since we don't guard
|
||||
// until the extrude button is clicked
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeEnabled()
|
||||
|
||||
await clickEmpty()
|
||||
|
||||
// expect active line to contain nothing
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText('')
|
||||
|
||||
// and extrude to still be disabled
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
|
||||
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
||||
sketch002 = startSketchOn(extrude001, $seg01)
|
||||
|> startProfileAt([-12.94, 6.6], %)
|
||||
@ -896,8 +894,9 @@ test.describe('Testing selections', () => {
|
||||
await u.codeLocator.fill(codeToAdd)
|
||||
|
||||
await selectUnExtrudable()
|
||||
// expect extrude button to be disabled
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
// expect extrude button to be enabled, since we don't guard
|
||||
// until the extrude button is clicked
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeEnabled()
|
||||
|
||||
await clickEmpty()
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText('')
|
||||
@ -932,11 +931,14 @@ test.describe('Testing selections', () => {
|
||||
const selectClose = () => page.getByText(`close(%)`).click()
|
||||
const clickEmpty = () => page.mouse.click(950, 100)
|
||||
|
||||
// expect fillet button without any bodies in the scene
|
||||
// Now that we don't disable toolbar buttons based on selection,
|
||||
// but rather based on a "selection" step in the command palette,
|
||||
// the fillet button should always be enabled with a good network connection.
|
||||
// I'm not sure if this test is actually useful anymore.
|
||||
await selectSegment()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
await clickEmpty()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
|
||||
// test fillet button with the body in the scene
|
||||
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
||||
@ -946,7 +948,7 @@ test.describe('Testing selections', () => {
|
||||
await selectSegment()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
await selectClose()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
await clickEmpty()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
})
|
||||
@ -1201,7 +1203,9 @@ test.describe('Testing selections', () => {
|
||||
).not.toBeDisabled()
|
||||
|
||||
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
// expect extrude button to be enabled, since we don't guard
|
||||
// until the extrude button is clicked
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeEnabled()
|
||||
|
||||
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
|
||||
await expect(
|
||||
@ -1212,7 +1216,9 @@ test.describe('Testing selections', () => {
|
||||
).not.toBeDisabled()
|
||||
|
||||
await page.getByText(selectionsSnippets.editOnly).click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
// expect extrude button to be enabled, since we don't guard
|
||||
// until the extrude button is clicked
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeEnabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).not.toBeDisabled()
|
||||
@ -1220,7 +1226,9 @@ test.describe('Testing selections', () => {
|
||||
await page
|
||||
.getByText(selectionsSnippets.extrudeAndEditBlockedInFunction)
|
||||
.click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
// expect extrude button to be enabled, since we don't guard
|
||||
// until the extrude button is clicked
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeEnabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).not.toBeVisible()
|
||||
|
@ -39,6 +39,7 @@
|
||||
"chokidar": "^4.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "6.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
@ -154,6 +155,7 @@
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/isomorphic-fetch": "^0.0.39",
|
||||
"@types/minimist": "^1.2.5",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import re
|
||||
import os
|
||||
import requests
|
||||
import textwrap
|
||||
|
||||
webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
|
||||
release_version = os.getenv('RELEASE_VERSION')
|
||||
@ -25,12 +26,11 @@ if len(modified_release_body) > max_length:
|
||||
|
||||
# Message to send to Discord
|
||||
data = {
|
||||
"content":
|
||||
f'''
|
||||
"content": textwrap.dedent(f'''
|
||||
**{release_version}** is now available! Check out the latest features and improvements here: <https://zoo.dev/modeling-app/download>
|
||||
|
||||
{modified_release_body}
|
||||
''',
|
||||
'''),
|
||||
"username": "Modeling App Release Updates",
|
||||
"avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png"
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ import {
|
||||
DoubleSide,
|
||||
Group,
|
||||
Intersection,
|
||||
Line,
|
||||
LineDashedMaterial,
|
||||
BufferGeometry,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
@ -13,6 +16,7 @@ import {
|
||||
Points,
|
||||
Quaternion,
|
||||
Scene,
|
||||
SphereGeometry,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from 'three'
|
||||
@ -31,6 +35,8 @@ import {
|
||||
SKETCH_LAYER,
|
||||
X_AXIS,
|
||||
Y_AXIS,
|
||||
CIRCLE_3_POINT_DRAFT_POINT,
|
||||
CIRCLE_3_POINT_DRAFT_CIRCLE,
|
||||
} from './sceneInfra'
|
||||
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
|
||||
import {
|
||||
@ -64,6 +70,7 @@ import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { executeAst, ToolTip } from 'lang/langHelpers'
|
||||
import {
|
||||
createProfileStartHandle,
|
||||
createArcGeometry,
|
||||
SegmentUtils,
|
||||
segmentUtils,
|
||||
} from './segments'
|
||||
@ -1219,6 +1226,228 @@ export class SceneEntities {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// lee: Well, it appears all our code in sceneEntities each act as their own
|
||||
// kind of classes. In this case, I'll keep utility functions pertaining to
|
||||
// circle3Point here. Feel free to extract as needed.
|
||||
entryDraftCircle3Point = async (
|
||||
startSketchOnASTNodePath: PathToNode,
|
||||
forward: Vector3,
|
||||
up: Vector3,
|
||||
sketchOrigin: Vector3
|
||||
) => {
|
||||
// lee: Not a fan we need to re-iterate this dummy object all over the place
|
||||
// just to get the scale but okie dokie.
|
||||
const dummy = new Mesh()
|
||||
dummy.position.set(0, 0, 0)
|
||||
const scale = sceneInfra.getClientSceneScaleFactor(dummy)
|
||||
|
||||
const orientation = quaternionFromUpNForward(up, forward)
|
||||
|
||||
// Reminder: the intersection plane is the primary way to derive a XY
|
||||
// position from a mouse click in ThreeJS.
|
||||
// Here, we position and orient so it's facing the viewer.
|
||||
this.intersectionPlane!.setRotationFromQuaternion(orientation)
|
||||
this.intersectionPlane!.position.copy(sketchOrigin)
|
||||
|
||||
// Keep track of points in the scene with their ThreeJS ids.
|
||||
const points: Map<number, Vector2> = new Map()
|
||||
|
||||
// Keep a reference so we can destroy and recreate as needed.
|
||||
let groupCircle: Group | undefined
|
||||
|
||||
// Add our new group to the list of groups to render
|
||||
const groupOfDrafts = new Group()
|
||||
groupOfDrafts.name = 'circle-3-point-group'
|
||||
groupOfDrafts.position.copy(sketchOrigin)
|
||||
// lee: I'm keeping this here as a developer gotchya:
|
||||
// Do not reorient your surfaces to the intersection plane. Your points are
|
||||
// already in 3D space, not 2D. If you intersect say XZ, you want the points
|
||||
// to continue to live at the 3D intersection point, not be rotated to end
|
||||
// up elsewhere!
|
||||
// groupOfDrafts.setRotationFromQuaternion(orientation)
|
||||
this.scene.add(groupOfDrafts)
|
||||
|
||||
const DRAFT_POINT_RADIUS = 6
|
||||
|
||||
const createPoint = (center: Vector3): number => {
|
||||
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
|
||||
const color = getThemeColorForThreeJs(sceneInfra._theme)
|
||||
const material = new MeshBasicMaterial({ color })
|
||||
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.userData = { type: CIRCLE_3_POINT_DRAFT_POINT }
|
||||
mesh.layers.set(SKETCH_LAYER)
|
||||
mesh.position.copy(center)
|
||||
mesh.scale.set(scale, scale, scale)
|
||||
mesh.renderOrder = 100
|
||||
|
||||
groupOfDrafts.add(mesh)
|
||||
|
||||
return mesh.id
|
||||
}
|
||||
|
||||
const circle3Point = (
|
||||
points: Vector2[]
|
||||
): undefined | { center: Vector3; radius: number } => {
|
||||
// A 3-point circle is undefined if it doesn't have 3 points :)
|
||||
if (points.length !== 3) return undefined
|
||||
|
||||
// y = (i/j)(x-h) + b
|
||||
// i and j variables for the slopes
|
||||
const i = [points[1].x - points[0].x, points[2].x - points[1].x]
|
||||
const j = [points[1].y - points[0].y, points[2].y - points[1].y]
|
||||
|
||||
// Our / threejs coordinate system affects this a lot. If you take this
|
||||
// code into a different code base, you may have to adjust a/b to being
|
||||
// -1/a/b, b/a, etc! In this case, a/-b did the trick.
|
||||
const m = [i[0] / -j[0], i[1] / -j[1]]
|
||||
|
||||
const h = [
|
||||
(points[0].x + points[1].x) / 2,
|
||||
(points[1].x + points[2].x) / 2,
|
||||
]
|
||||
const b = [
|
||||
(points[0].y + points[1].y) / 2,
|
||||
(points[1].y + points[2].y) / 2,
|
||||
]
|
||||
|
||||
// Algebraically derived
|
||||
const x = (-m[0] * h[0] + b[0] - b[1] + m[1] * h[1]) / (m[1] - m[0])
|
||||
const y = m[0] * (x - h[0]) + b[0]
|
||||
|
||||
const center = new Vector3(x, y, 0)
|
||||
const radius = Math.sqrt((points[1].x - x) ** 2 + (points[1].y - y) ** 2)
|
||||
|
||||
return {
|
||||
center,
|
||||
radius,
|
||||
}
|
||||
}
|
||||
|
||||
// TO BE SHORT LIVED: unused function to draw the circle and lines.
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const createCircle3Point = (points: Vector2[]) => {
|
||||
const circleParams = circle3Point(points)
|
||||
|
||||
// A circle cannot be created for these points.
|
||||
if (!circleParams) return
|
||||
|
||||
const color = getThemeColorForThreeJs(sceneInfra._theme)
|
||||
const geometryCircle = createArcGeometry({
|
||||
center: [circleParams.center.x, circleParams.center.y],
|
||||
radius: circleParams.radius,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
const materialCircle = new MeshBasicMaterial({ color })
|
||||
|
||||
if (groupCircle) groupOfDrafts.remove(groupCircle)
|
||||
groupCircle = new Group()
|
||||
groupCircle.renderOrder = 1
|
||||
|
||||
const meshCircle = new Mesh(geometryCircle, materialCircle)
|
||||
meshCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
|
||||
meshCircle.layers.set(SKETCH_LAYER)
|
||||
meshCircle.position.set(circleParams.center.x, circleParams.center.y, 0)
|
||||
meshCircle.scale.set(scale, scale, scale)
|
||||
groupCircle.add(meshCircle)
|
||||
|
||||
const geometryPolyLine = new BufferGeometry().setFromPoints([
|
||||
...points,
|
||||
points[0],
|
||||
])
|
||||
const materialPolyLine = new LineDashedMaterial({
|
||||
color,
|
||||
scale,
|
||||
dashSize: 6,
|
||||
gapSize: 6,
|
||||
})
|
||||
const meshPolyLine = new Line(geometryPolyLine, materialPolyLine)
|
||||
meshPolyLine.computeLineDistances()
|
||||
groupCircle.add(meshPolyLine)
|
||||
|
||||
groupOfDrafts.add(groupCircle)
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
this.scene.remove(groupOfDrafts)
|
||||
}
|
||||
|
||||
// The target of our dragging
|
||||
let target: Object3D | undefined = undefined
|
||||
|
||||
sceneInfra.setCallbacks({
|
||||
async onDrag(args) {
|
||||
const draftPointsIntersected = args.intersects.filter(
|
||||
(intersected) =>
|
||||
intersected.object.userData.type === CIRCLE_3_POINT_DRAFT_POINT
|
||||
)
|
||||
|
||||
const firstPoint = draftPointsIntersected[0]
|
||||
if (firstPoint && !target) {
|
||||
target = firstPoint.object
|
||||
}
|
||||
|
||||
// The user was off their mark! Missed the object to select.
|
||||
if (!target) return
|
||||
|
||||
target.position.copy(args.intersectionPoint.threeD)
|
||||
points.set(target.id, args.intersectionPoint.twoD)
|
||||
},
|
||||
async onDragEnd(_args) {
|
||||
target = undefined
|
||||
},
|
||||
async onClick(args) {
|
||||
if (points.size >= 3) return
|
||||
if (!args.intersectionPoint) return
|
||||
|
||||
const id = createPoint(args.intersectionPoint.threeD)
|
||||
points.set(id, args.intersectionPoint.twoD)
|
||||
|
||||
if (points.size < 2) return
|
||||
|
||||
// We've now got 3 points, let's create our circle!
|
||||
const astSnapshot = structuredClone(kclManager.ast)
|
||||
let nodeQueryResult
|
||||
nodeQueryResult = getNodeFromPath<VariableDeclaration>(
|
||||
astSnapshot,
|
||||
startSketchOnASTNodePath,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(nodeQueryResult)) return Promise.reject(nodeQueryResult)
|
||||
const startSketchOnASTNode = nodeQueryResult
|
||||
|
||||
const circleParams = circle3Point(Array.from(points.values()))
|
||||
|
||||
if (!circleParams) return
|
||||
|
||||
const kclCircle3Point = parse(`circle({
|
||||
center = [${circleParams.center.x}, ${circleParams.center.y}],
|
||||
radius = ${circleParams.radius},
|
||||
}, %)`)
|
||||
|
||||
if (err(kclCircle3Point) || kclCircle3Point.program === null) return
|
||||
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
|
||||
return
|
||||
|
||||
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
|
||||
startSketchOnASTNode.node.declaration.init = createPipeExpression([
|
||||
clonedStartSketchOnASTNode.node.declaration.init,
|
||||
kclCircle3Point.program.body[0].expression,
|
||||
])
|
||||
|
||||
await kclManager.executeAstMock(astSnapshot)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(astSnapshot)
|
||||
|
||||
sceneInfra.modelingSend({ type: 'circle3PointsFinished', cleanup })
|
||||
},
|
||||
})
|
||||
}
|
||||
setupDraftCircle = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
forward: [number, number, number],
|
||||
|
@ -62,6 +62,8 @@ export const ARROWHEAD = 'arrowhead'
|
||||
export const SEGMENT_LENGTH_LABEL = 'segment-length-label'
|
||||
export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text'
|
||||
export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30
|
||||
export const CIRCLE_3_POINT_DRAFT_POINT = 'circle-3-point-draft-point'
|
||||
export const CIRCLE_3_POINT_DRAFT_CIRCLE = 'circle-3-point-draft-circle'
|
||||
|
||||
export interface OnMouseEnterLeaveArgs {
|
||||
selected: Object3D<Object3DEventMap>
|
||||
|
@ -21,7 +21,8 @@ export function AstExplorer() {
|
||||
const node = _node
|
||||
|
||||
return (
|
||||
<div id="ast-explorer" className="relative">
|
||||
<details id="ast-explorer" className="relative">
|
||||
<summary>AST Explorer</summary>
|
||||
<div className="">
|
||||
filter out keys:<div className="w-2 inline-block"></div>
|
||||
{['start', 'end', 'type'].map((key) => {
|
||||
@ -58,7 +59,7 @@ export function AstExplorer() {
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ function CommandBarSelectionInput({
|
||||
return () => {
|
||||
toSync(() => {
|
||||
const promises = [
|
||||
new Promise(() => kclManager.defaultSelectionFilter()),
|
||||
new Promise(() => kclManager.defaultSelectionFilter(selection)),
|
||||
]
|
||||
if (!kclManager._isAstEmpty(kclManager.ast)) {
|
||||
promises.push(kclManager.hidePlanes())
|
||||
|
@ -392,6 +392,26 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
eyeOpen: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 14.5C5.58172 14.5 3 10.5 3 10.5C3 10.5 5.58172 5.5 10 5.5C14.4183 5.5 17 10.5 17 10.5C17 10.5 14.4183 14.5 10 14.5ZM4.24209 10.4865L4.19946 10.4348C4.23234 10.3833 4.26774 10.3288 4.30565 10.2717C4.59304 9.83862 5.01753 9.26342 5.56519 8.69184C6.67884 7.52958 8.18459 6.5 10 6.5C11.8154 6.5 13.3212 7.52958 14.4348 8.69184C14.9825 9.26342 15.407 9.83862 15.6944 10.2717C15.7323 10.3288 15.7677 10.3833 15.8005 10.4348L15.7579 10.4865C15.4766 10.8257 15.0582 11.2796 14.516 11.7324C13.4249 12.6433 11.8946 13.5 10 13.5C8.10539 13.5 6.57507 12.6433 5.48405 11.7324C4.9418 11.2796 4.52342 10.8257 4.24209 10.4865ZM12 10C12 11.1046 11.1046 12 10 12C8.89543 12 8 11.1046 8 10C8 8.89543 8.89543 8 10 8C11.1046 8 12 8.89543 12 10ZM13 10C13 11.6569 11.6569 13 10 13C8.34315 13 7 11.6569 7 10C7 8.34315 8.34315 7 10 7C11.6569 7 13 8.34315 13 10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
eyeCrossedOut: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.35352 5.39647L14.253 15.296L14.9601 14.5889L5.06062 4.68936L4.35352 5.39647ZM10.9303 13.4303L11.7785 14.2785C11.2246 14.4184 10.631 14.5 10 14.5C5.58172 14.5 3 10.5 3 10.5C3 10.5 3.76379 9.02078 5.17147 7.67148L5.8787 8.37872C5.771 8.48155 5.66647 8.58616 5.56519 8.69186C5.01753 9.26343 4.59304 9.83863 4.30565 10.2717C4.26774 10.3288 4.23234 10.3833 4.19946 10.4348L4.24209 10.4866C4.52342 10.8257 4.9418 11.2797 5.48405 11.7324C6.57507 12.6433 8.10539 13.5 10 13.5C10.3206 13.5 10.6309 13.4755 10.9303 13.4303ZM10 5.50001C9.16896 5.50001 8.4029 5.6769 7.70677 5.96414L8.48545 6.74282C8.96231 6.58848 9.46785 6.50001 10 6.50001C11.8154 6.50001 13.3212 7.52959 14.4348 8.69186C14.9825 9.26343 15.407 9.83863 15.6944 10.2717C15.7323 10.3288 15.7677 10.3833 15.8005 10.4348L15.7579 10.4866C15.4766 10.8257 15.0582 11.2797 14.516 11.7324C14.3321 11.8859 14.1357 12.0379 13.9272 12.1845L14.6438 12.9011C16.1692 11.7871 17 10.5 17 10.5C17 10.5 14.4183 5.50001 10 5.50001ZM10 7.00001C9.62554 7.00001 9.2671 7.06862 8.93658 7.19395L9.75723 8.0146C9.8368 8.00497 9.91782 8.00001 10 8.00001C11.1046 8.00001 12 8.89544 12 10C12 10.0822 11.995 10.1632 11.9854 10.2428L12.8061 11.0634C12.9314 10.7329 13 10.3745 13 10C13 8.34316 11.6569 7.00001 10 7.00001ZM7 10C7 9.8421 7.0122 9.68704 7.03571 9.53572L8.08776 10.5878C8.28175 11.2197 8.78035 11.7183 9.41224 11.9123L10.4643 12.9643C10.313 12.9878 10.1579 13 10 13C8.34315 13 7 11.6569 7 10Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
fillet: (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
@ -533,6 +553,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
hollow: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.67443 5.38863L7.24585 3.87359L6.55986 3.15721L4.51827 5.12555L4.5391 5.14731L4.51129 5.14139V6.14139V13.9073V14.9073L5.48944 15.1152L12.5048 16.6064L13.4829 16.8143V16.7896L13.5043 16.8119L15.5459 14.8436L14.8599 14.1272L13.4829 15.4548V8.04838V7.63961L13.5043 7.66193L15.5459 5.69359L14.8599 4.97721L12.851 6.91405L12.5048 6.84046L5.67443 5.38863ZM12.5048 7.84046L5.48944 6.34931V14.1152L12.5048 15.6064V7.84046ZM6.21381 8.04101V8.51384V8.54101L7.1075 8.73098V8.7038L7.19195 8.72175V7.74893L7.1075 7.73098L6.70288 7.64497L6.21381 7.54101V8.04101ZM6.21381 12.4051V12.3779L7.1075 12.5679V12.5951L7.19195 12.613V13.5859L7.1075 13.5679L6.70288 13.4819L6.21381 13.3779V12.8779V12.4051ZM10.6823 14.3277L10.5978 14.3098V13.337L10.6823 13.3549V13.3277L11.576 13.5177V13.5449V14.0177V14.5177L11.0869 14.4138L10.6823 14.3277ZM11.576 9.6536V9.68078L10.6823 9.49082V9.46364L10.5978 9.44569V8.47287L10.6823 8.49082L11.0869 8.57683L11.576 8.68078V9.18078V9.6536ZM8.0012 8.42094V7.92094L9.7886 8.30086V8.80086V9.30086L8.0012 8.92094V8.42094ZM11.0869 10.5225L11.576 10.6264V12.5721L11.0869 12.4681L10.5978 12.3642V10.4185L11.0869 10.5225ZM9.7886 13.6378V14.1378L8.0012 13.7579V13.2579V12.7579L9.7886 13.1378V13.6378ZM6.70288 11.5363L6.21381 11.4323V9.48666L6.70288 9.59061L7.19195 9.69457V11.6402L6.70288 11.5363Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
horizontal: (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
@ -563,6 +593,22 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
import: (
|
||||
<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="M9.64645 12.3535L10 12.7071L10.3536 12.3535L13.8536 8.85352L13.1464 8.14642L10.5 10.7929L10.5 3H9.5L9.5 10.7929L6.85355 8.14642L6.14645 8.85352L9.64645 12.3535ZM15 5H12.4999V4H15H16V5V15V16H15H5H4V15V5V4H5H7.49988V5H5V15H15V5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'intersection-offset': (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
@ -741,6 +787,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
model: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.91773 10L10 6.89417L15.0823 10L10 13.1058L4.91773 10ZM10 5.72222L16.0411 9.41403L17 10L17 12.0541H16V10.6111L10.5 13.9722V16.0541H9.5V13.9722L4 10.6111V12.0541H3V10L3.95886 9.41403L10 5.72222Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
move: (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
@ -801,6 +857,58 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
patternCircular2d: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11 4C11 4.55228 10.5523 5 10 5C9.44772 5 9 4.55228 9 4C9 3.44772 9.44772 3 10 3C10.5523 3 11 3.44772 11 4ZM12 4C12 5.10457 11.1046 6 10 6C8.89543 6 8 5.10457 8 4C8 2.89543 8.89543 2 10 2C11.1046 2 12 2.89543 12 4ZM16 7C16 7.55228 15.5523 8 15 8C14.4477 8 14 7.55228 14 7C14 6.44772 14.4477 6 15 6C15.5523 6 16 6.44772 16 7ZM17 7C17 8.10457 16.1046 9 15 9C13.8954 9 13 8.10457 13 7C13 5.89543 13.8954 5 15 5C16.1046 5 17 5.89543 17 7ZM15 14C15.5523 14 16 13.5523 16 13C16 12.4477 15.5523 12 15 12C14.4477 12 14 12.4477 14 13C14 13.5523 14.4477 14 15 14ZM15 15C16.1046 15 17 14.1046 17 13C17 11.8954 16.1046 11 15 11C13.8954 11 13 11.8954 13 13C13 14.1046 13.8954 15 15 15ZM11 16C11 16.5523 10.5523 17 10 17C9.44772 17 9 16.5523 9 16C9 15.4477 9.44772 15 10 15C10.5523 15 11 15.4477 11 16ZM12 16C12 17.1046 11.1046 18 10 18C8.89543 18 8 17.1046 8 16C8 14.8954 8.89543 14 10 14C11.1046 14 12 14.8954 12 16ZM5 14C5.55228 14 6 13.5523 6 13C6 12.4477 5.55228 12 5 12C4.44772 12 4 12.4477 4 13C4 13.5523 4.44772 14 5 14ZM5 15C6.10457 15 7 14.1046 7 13C7 11.8954 6.10457 11 5 11C3.89543 11 3 11.8954 3 13C3 14.1046 3.89543 15 5 15ZM6 7C6 7.55228 5.55228 8 5 8C4.44772 8 4 7.55228 4 7C4 6.44772 4.44772 6 5 6C5.55228 6 6 6.44772 6 7ZM7 7C7 8.10457 6.10457 9 5 9C3.89543 9 3 8.10457 3 7C3 5.89543 3.89543 5 5 5C6.10457 5 7 5.89543 7 7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
patternCircular3d: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11 4C11 4.55228 10.5523 5 10 5C9.44772 5 9 4.55228 9 4C9 3.44772 9.44772 3 10 3C10.5523 3 11 3.44772 11 4ZM12 4C12 5.10457 11.1046 6 10 6C8.89543 6 8 5.10457 8 4C8 2.89543 8.89543 2 10 2C11.1046 2 12 2.89543 12 4ZM16 7C16 7.55228 15.5523 8 15 8C14.4477 8 14 7.55228 14 7C14 6.44772 14.4477 6 15 6C15.5523 6 16 6.44772 16 7ZM17 7C17 8.10457 16.1046 9 15 9C13.8954 9 13 8.10457 13 7C13 5.89543 13.8954 5 15 5C16.1046 5 17 5.89543 17 7ZM15 14C15.5523 14 16 13.5523 16 13C16 12.4477 15.5523 12 15 12C14.4477 12 14 12.4477 14 13C14 13.5523 14.4477 14 15 14ZM15 15C16.1046 15 17 14.1046 17 13C17 11.8954 16.1046 11 15 11C13.8954 11 13 11.8954 13 13C13 14.1046 13.8954 15 15 15ZM11 16C11 16.5523 10.5523 17 10 17C9.44772 17 9 16.5523 9 16C9 15.4477 9.44772 15 10 15C10.5523 15 11 15.4477 11 16ZM12 16C12 17.1046 11.1046 18 10 18C8.89543 18 8 17.1046 8 16C8 14.8954 8.89543 14 10 14C11.1046 14 12 14.8954 12 16ZM5 14C5.55228 14 6 13.5523 6 13C6 12.4477 5.55228 12 5 12C4.44772 12 4 12.4477 4 13C4 13.5523 4.44772 14 5 14ZM5 15C6.10457 15 7 14.1046 7 13C7 11.8954 6.10457 11 5 11C3.89543 11 3 11.8954 3 13C3 14.1046 3.89543 15 5 15ZM6 7C6 7.55228 5.55228 8 5 8C4.44772 8 4 7.55228 4 7C4 6.44772 4.44772 6 5 6C5.55228 6 6 6.44772 6 7ZM7 7C7 8.10457 6.10457 9 5 9C3.89543 9 3 8.10457 3 7C3 5.89543 3.89543 5 5 5C6.10457 5 7 5.89543 7 7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.642 5.1421C12.1606 4.78075 12.5 4.18001 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.82004 1.5 9.21933 1.83933 8.85797 2.35789C9.18176 2.13228 9.57543 1.99999 9.99999 1.99999C11.1046 1.99999 12 2.89542 12 3.99999C12 4.42459 11.8677 4.81829 11.642 5.1421ZM16.642 8.1421C17.1606 7.78075 17.5 7.18001 17.5 6.5C17.5 5.39543 16.6046 4.5 15.5 4.5C14.82 4.5 14.2193 4.83933 13.858 5.35789C14.1818 5.13228 14.5754 4.99999 15 4.99999C16.1046 4.99999 17 5.89542 17 6.99999C17 7.42459 16.8677 7.81829 16.642 8.1421ZM13.858 11.3579C14.1818 11.1323 14.5754 11 15 11C16.1046 11 17 11.8954 17 13C17 13.4246 16.8677 13.8183 16.642 14.1421C17.1606 13.7808 17.5 13.18 17.5 12.5C17.5 11.3954 16.6046 10.5 15.5 10.5C14.82 10.5 14.2193 10.8393 13.858 11.3579ZM11.642 17.1421C12.1606 16.7808 12.5 16.18 12.5 15.5C12.5 14.3954 11.6046 13.5 10.5 13.5C9.82004 13.5 9.21933 13.8393 8.85797 14.3579C9.18176 14.1323 9.57543 14 9.99999 14C11.1046 14 12 14.8954 12 16C12 16.4246 11.8677 16.8183 11.642 17.1421ZM6.64202 14.1421C7.16064 13.7808 7.50001 13.18 7.50001 12.5C7.50001 11.3954 6.60458 10.5 5.50001 10.5C4.82004 10.5 4.21933 10.8393 3.85797 11.3579C4.18176 11.1323 4.57543 11 4.99999 11C6.10456 11 6.99999 11.8954 6.99999 13C6.99999 13.4246 6.86767 13.8183 6.64202 14.1421ZM6.64202 8.1421C7.16064 7.78075 7.50001 7.18001 7.50001 6.5C7.50001 5.39543 6.60458 4.5 5.50001 4.5C4.82004 4.5 4.21933 4.83933 3.85797 5.35789C4.18176 5.13228 4.57543 4.99999 4.99999 4.99999C6.10456 4.99999 6.99999 5.89542 6.99999 6.99999C6.99999 7.42459 6.86767 7.81829 6.64202 8.1421Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
patternLinear2d: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 4H6V6H4V4ZM3 3H4H6H7V4V6V7H6H4H3V6V4V3ZM4 9H6V11H4V9ZM3 8H4H6H7V9V11V12H6H4H3V11V9V8ZM6 14H4V16H6V14ZM4 13H3V14V16V17H4H6H7V16V14V13H6H4ZM9 4H11V6H9V4ZM8 3H9H11H12V4V6V7H11H9H8V6V4V3ZM11 9H9V11H11V9ZM9 8H8V9V11V12H9H11H12V11V9V8H11H9ZM14 4H16V6H14V4ZM13 3H14H16H17V4V6V7H16H14H13V6V4V3Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
patternLinear3d: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 4H6V6H4V4ZM3 3H4H6H7V4V6V7H6H4H3V6V4V3ZM4 9H6V11H4V9ZM3 8H4H6H7V9V11V12H6H4H3V11V9V8ZM6 14H4V16H6V14ZM4 13H3V14V16V17H4H6H7V16V14V13H6H4ZM9 4H11V6H9V4ZM8 3H9H11H12V4V6V7H11H9H8V6V4V3ZM11 9H9V11H11V9ZM9 8H8V9V11V12H9H11H12V11V9V8H11H9ZM14 4H16V6H14V4ZM13 3H14H16H17V4V6V7H16H14H13V6V4V3Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.5 2.5H3.5V3H7V6.5H7.5V5.5V3.5V2.5H6.5H4.5ZM4.5 7.5H3.5V8H7V11.5H7.5V10.5V8.5V7.5H6.5H4.5ZM3.5 12.5H4.5H6.5H7.5V13.5V15.5V16.5H7V13H3.5V12.5ZM9.5 2.5H8.5V3H12V6.5H12.5V5.5V3.5V2.5H11.5H9.5ZM8.5 7.5H9.5H11.5H12.5V8.5V10.5V11.5H12V8H8.5V7.5ZM14.5 2.5H13.5V3H17V6.5H17.5V5.5V3.5V2.5H16.5H14.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
person: (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
|
@ -13,7 +13,11 @@ import { engineCommandManager } from '../lib/singletons'
|
||||
|
||||
import { Spinner } from './Spinner'
|
||||
|
||||
const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
interface LoadingProps extends React.PropsWithChildren {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Loading = ({ children, className }: LoadingProps) => {
|
||||
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
|
||||
|
||||
useEffect(() => {
|
||||
@ -64,7 +68,7 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="body-bg flex flex-col items-center justify-center h-screen"
|
||||
className={`body-bg flex flex-col items-center justify-center h-screen ${className}`}
|
||||
data-testid="loading"
|
||||
>
|
||||
<Spinner />
|
||||
|
@ -46,16 +46,9 @@ import {
|
||||
applyConstraintLength,
|
||||
} from './Toolbar/setAngleLength'
|
||||
import {
|
||||
canSweepSelection,
|
||||
handleSelectionBatch,
|
||||
isSelectionLastLine,
|
||||
isRangeBetweenCharacters,
|
||||
isSketchPipe,
|
||||
Selections,
|
||||
updateSelections,
|
||||
canLoftSelection,
|
||||
canRevolveSelection,
|
||||
canShellSelection,
|
||||
} from 'lib/selections'
|
||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
@ -75,21 +68,17 @@ import {
|
||||
} from 'lang/modifyAst'
|
||||
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
|
||||
import {
|
||||
doesSceneHaveExtrudedSketch,
|
||||
doesSceneHaveSweepableSketch,
|
||||
artifactIsPlaneWithPaths,
|
||||
getNodePathFromSourceRange,
|
||||
isSingleCursorInPipe,
|
||||
} from 'lang/queryAst'
|
||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import toast from 'react-hot-toast'
|
||||
import { EditorSelection, Transaction } from '@codemirror/state'
|
||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { modelingMachineEvent } from 'editor/manager'
|
||||
import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment'
|
||||
import {
|
||||
ExportIntent,
|
||||
EngineConnectionStateType,
|
||||
@ -100,6 +89,8 @@ import { useFileContext } from 'hooks/useFileContext'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { promptToEditFlow } from 'lib/promptToEdit'
|
||||
import { kclEditorActor } from 'machines/kclEditorMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -309,22 +300,6 @@ export const ModelingMachineProvider = ({
|
||||
null
|
||||
if (!setSelections) return {}
|
||||
|
||||
const dispatchSelection = (selection?: EditorSelection) => {
|
||||
if (!selection) return // TODO less of hack for the below please
|
||||
if (!editorManager.editorView) return
|
||||
|
||||
setTimeout(() => {
|
||||
if (!editorManager.editorView) return
|
||||
editorManager.editorView.dispatch({
|
||||
selection,
|
||||
annotations: [
|
||||
modelingMachineEvent,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let selections: Selections = {
|
||||
graphSelections: [],
|
||||
otherSelections: [],
|
||||
@ -364,7 +339,15 @@ export const ModelingMachineProvider = ({
|
||||
} = handleSelectionBatch({
|
||||
selections,
|
||||
})
|
||||
codeMirrorSelection && dispatchSelection(codeMirrorSelection)
|
||||
if (codeMirrorSelection) {
|
||||
kclEditorActor.send({
|
||||
type: 'setLastSelectionEvent',
|
||||
data: {
|
||||
codeMirrorSelection,
|
||||
scrollIntoView: setSelections.scrollIntoView ?? false,
|
||||
},
|
||||
})
|
||||
}
|
||||
engineEvents &&
|
||||
engineEvents.forEach((event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@ -556,78 +539,6 @@ export const ModelingMachineProvider = ({
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
'has valid sweep selection': ({ context: { selectionRanges } }) => {
|
||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
isRangeBetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
|
||||
if (hasNoSelection) {
|
||||
// they have no selection, we should enable the button
|
||||
// so they can select the face through the cmdbar
|
||||
// BUT only if there's extrudable geometry
|
||||
return doesSceneHaveSweepableSketch(kclManager.ast)
|
||||
}
|
||||
if (!isSketchPipe(selectionRanges)) return false
|
||||
|
||||
const canSweep = canSweepSelection(selectionRanges)
|
||||
if (err(canSweep)) return false
|
||||
return canSweep
|
||||
},
|
||||
'has valid revolve selection': ({ context: { selectionRanges } }) => {
|
||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
isRangeBetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
|
||||
if (hasNoSelection) {
|
||||
// they have no selection, we should enable the button
|
||||
// so they can select the face through the cmdbar
|
||||
// BUT only if there's extrudable geometry
|
||||
return doesSceneHaveSweepableSketch(kclManager.ast)
|
||||
}
|
||||
if (!isSketchPipe(selectionRanges)) return false
|
||||
|
||||
const canSweep = canRevolveSelection(selectionRanges)
|
||||
if (err(canSweep)) return false
|
||||
return canSweep
|
||||
},
|
||||
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
isRangeBetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
|
||||
if (hasNoSelection) {
|
||||
const count = 2
|
||||
return doesSceneHaveSweepableSketch(kclManager.ast, count)
|
||||
}
|
||||
|
||||
const canLoft = canLoftSelection(selectionRanges)
|
||||
if (err(canLoft)) return false
|
||||
return canLoft
|
||||
},
|
||||
'has valid shell selection': ({
|
||||
context: { selectionRanges },
|
||||
event,
|
||||
}) => {
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
isRangeBetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
|
||||
if (hasNoSelection) {
|
||||
return doesSceneHaveExtrudedSketch(kclManager.ast)
|
||||
}
|
||||
|
||||
const canShell = canShellSelection(selectionRanges)
|
||||
if (err(canShell)) return false
|
||||
return canShell
|
||||
},
|
||||
'has valid selection for deletion': ({
|
||||
context: { selectionRanges },
|
||||
}) => {
|
||||
@ -635,18 +546,12 @@ export const ModelingMachineProvider = ({
|
||||
if (selectionRanges.graphSelections.length <= 0) return false
|
||||
return true
|
||||
},
|
||||
'has valid edge treatment selection': ({
|
||||
context: { selectionRanges },
|
||||
}) => {
|
||||
return hasValidEdgeTreatmentSelection({
|
||||
selectionRanges,
|
||||
ast: kclManager.ast,
|
||||
code: codeManager.code,
|
||||
})
|
||||
},
|
||||
'Selection is on face': ({ context: { selectionRanges }, event }) => {
|
||||
if (event.type !== 'Enter sketch') return false
|
||||
if (event.data?.forceNewSketch) return false
|
||||
if (artifactIsPlaneWithPaths(selectionRanges)) {
|
||||
return true
|
||||
}
|
||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
||||
return false
|
||||
return !!isCursorInSketchCommandRange(
|
||||
@ -1198,6 +1103,15 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
}
|
||||
),
|
||||
'submit-prompt-edit': fromPromise(async ({ input }) => {
|
||||
return await promptToEditFlow({
|
||||
code: codeManager.code,
|
||||
prompt: input.prompt,
|
||||
selections: input.selection,
|
||||
token,
|
||||
artifactGraph: engineCommandManager.artifactGraph,
|
||||
})
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{
|
||||
|
381
src/components/ModelingSidebar/ModelingPanes/FeatureTreePane.tsx
Normal file
@ -0,0 +1,381 @@
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { useMachine, useSelector } from '@xstate/react'
|
||||
import { ContextMenu, ContextMenuItem } from 'components/ContextMenu'
|
||||
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
|
||||
import Loading from 'components/Loading'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { codeRefFromRange, getArtifactFromRange } from 'lang/std/artifactGraph'
|
||||
import { sourceRangeFromRust } from 'lang/wasm'
|
||||
import {
|
||||
filterOperations,
|
||||
getOperationIcon,
|
||||
getOperationLabel,
|
||||
} from 'lib/operations'
|
||||
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { Actor, Prop } from 'xstate'
|
||||
import { featureTreeMachine } from 'machines/featureTreeMachine'
|
||||
import {
|
||||
editorIsMountedSelector,
|
||||
kclEditorActor,
|
||||
selectionEventSelector,
|
||||
} from 'machines/kclEditorMachine'
|
||||
|
||||
export const FeatureTreePane = () => {
|
||||
const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector)
|
||||
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
|
||||
const { send: modelingSend, state: modelingState } = useModelingContext()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_featureTreeState, featureTreeSend] = useMachine(
|
||||
featureTreeMachine.provide({
|
||||
guards: {
|
||||
codePaneIsOpen: () =>
|
||||
modelingState.context.store.openPanes.includes('code') &&
|
||||
editorManager.editorView !== null,
|
||||
},
|
||||
actions: {
|
||||
openCodePane: () => {
|
||||
modelingSend({
|
||||
type: 'Set context',
|
||||
data: {
|
||||
openPanes: [...modelingState.context.store.openPanes, 'code'],
|
||||
},
|
||||
})
|
||||
},
|
||||
sendEditFlowStart: () => {
|
||||
modelingSend({ type: 'Enter sketch' })
|
||||
},
|
||||
scrollToError: () => {
|
||||
editorManager.scrollToFirstErrorDiagnosticIfExists()
|
||||
},
|
||||
sendSelectionEvent: ({ context }) => {
|
||||
if (!context.targetSourceRange) {
|
||||
return
|
||||
}
|
||||
const artifact = context.targetSourceRange
|
||||
? getArtifactFromRange(
|
||||
context.targetSourceRange,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
: null
|
||||
if (!artifact || !('codeRef' in artifact)) {
|
||||
modelingSend({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: {
|
||||
codeRef: codeRefFromRange(
|
||||
context.targetSourceRange,
|
||||
kclManager.ast
|
||||
),
|
||||
},
|
||||
scrollIntoView: true,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
modelingSend({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: {
|
||||
artifact: artifact,
|
||||
codeRef: codeRefFromRange(
|
||||
context.targetSourceRange,
|
||||
kclManager.ast
|
||||
),
|
||||
},
|
||||
scrollIntoView: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
// If there are parse errors we show the last successful operations
|
||||
// and overlay a message on top of the pane
|
||||
const parseErrors = kclManager.errors.filter((e) => e.kind !== 'engine')
|
||||
|
||||
// If there are engine errors we show the successful operations
|
||||
// Errors return an operation list, so use the longest one if there are multiple
|
||||
const longestErrorOperationList = kclManager.errors.reduce((acc, error) => {
|
||||
return error.operations && error.operations.length > acc.length
|
||||
? error.operations
|
||||
: acc
|
||||
}, [] as Operation[])
|
||||
|
||||
const unfilteredOperationList = !parseErrors.length
|
||||
? !kclManager.errors.length
|
||||
? kclManager.execState.operations
|
||||
: longestErrorOperationList
|
||||
: kclManager.lastSuccessfulOperations
|
||||
|
||||
// We filter out operations that are not useful to show in the feature tree
|
||||
const operationList = filterOperations(unfilteredOperationList)
|
||||
|
||||
// Watch for changes in the open panes and send an event to the feature tree machine
|
||||
useEffect(() => {
|
||||
const codeOpen = modelingState.context.store.openPanes.includes('code')
|
||||
if (codeOpen && isEditorMounted) {
|
||||
featureTreeSend({ type: 'codePaneOpened' })
|
||||
}
|
||||
}, [modelingState.context.store.openPanes, isEditorMounted])
|
||||
|
||||
// Watch for changes in the selection and send an event to the feature tree machine
|
||||
useEffect(() => {
|
||||
featureTreeSend({ type: 'selected' })
|
||||
}, [lastSelectionEvent])
|
||||
|
||||
function goToError() {
|
||||
featureTreeSend({ type: 'goToError' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<section
|
||||
data-testid="debug-panel"
|
||||
className="absolute inset-0 p-1 box-border overflow-auto"
|
||||
>
|
||||
{kclManager.isExecuting ? (
|
||||
<Loading className="h-full">Building feature tree...</Loading>
|
||||
) : (
|
||||
<>
|
||||
{parseErrors.length > 0 && (
|
||||
<div
|
||||
className={`absolute inset-0 rounded-lg p-2 ${
|
||||
operationList.length &&
|
||||
`bg-destroy-10/40 dark:bg-destroy-80/40`
|
||||
}`}
|
||||
>
|
||||
<div className="text-sm bg-destroy-80 text-chalkboard-10 py-1 px-2 rounded flex gap-2 items-center">
|
||||
<p className="flex-1">
|
||||
Errors found in KCL code.
|
||||
<br />
|
||||
Please fix them before continuing.
|
||||
</p>
|
||||
<button
|
||||
onClick={goToError}
|
||||
className="bg-chalkboard-10 text-destroy-80 p-1 rounded-sm flex-none hover:bg-chalkboard-10 hover:border-destroy-70 hover:text-destroy-80 border-transparent"
|
||||
>
|
||||
View error
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{operationList.map((operation) => {
|
||||
const key = `${operation.type}-${
|
||||
'name' in operation ? operation.name : 'anonymous'
|
||||
}-${
|
||||
'sourceRange' in operation ? operation.sourceRange[0] : 'start'
|
||||
}`
|
||||
|
||||
return (
|
||||
<OperationItem
|
||||
key={key}
|
||||
item={operation}
|
||||
send={featureTreeSend}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const visibilityMap = new Map<string, boolean>()
|
||||
|
||||
interface VisibilityToggleProps {
|
||||
entityId: string
|
||||
initialVisibility: boolean
|
||||
onVisibilityChange?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A button that toggles the visibility of an entity
|
||||
* tied to an artifact in the feature tree.
|
||||
* TODO: this is unimplemented and will be used for
|
||||
* default planes after we fix them and add them to the artifact graph / feature tree
|
||||
*/
|
||||
const VisibilityToggle = (props: VisibilityToggleProps) => {
|
||||
const [visible, setVisible] = useState(props.initialVisibility)
|
||||
|
||||
function handleToggleVisible() {
|
||||
setVisible(!visible)
|
||||
visibilityMap.set(props.entityId, !visible)
|
||||
props.onVisibilityChange?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleToggleVisible}
|
||||
className="border-transparent p-0 m-0"
|
||||
>
|
||||
<CustomIcon
|
||||
name={visible ? 'eyeOpen' : 'eyeCrossedOut'}
|
||||
className={`w-5 h-5 ${
|
||||
visible
|
||||
? 'hidden group-hover/item:block group-focus-within/item:block'
|
||||
: 'text-chalkboard-50'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* More generic version of OperationListItem,
|
||||
* to be used for default planes after we fix them and
|
||||
* add them to the artifact graph / feature tree
|
||||
*/
|
||||
const OperationItemWrapper = ({
|
||||
icon,
|
||||
name,
|
||||
visibilityToggle,
|
||||
menuItems,
|
||||
errors,
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLButtonElement> & {
|
||||
icon: CustomIconName
|
||||
name: string
|
||||
visibilityToggle?: VisibilityToggleProps
|
||||
menuItems?: ComponentProps<typeof ContextMenu>['items']
|
||||
errors?: Diagnostic[]
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="flex select-none items-center group/item my-0 py-0.5 px-1 focus-within:bg-primary/10 hover:bg-primary/5"
|
||||
>
|
||||
<button
|
||||
{...props}
|
||||
className={`reset flex-1 flex items-center gap-2 border-transparent dark:border-transparent text-left text-base ${className}`}
|
||||
>
|
||||
<CustomIcon name={icon} className="w-5 h-5 block" />
|
||||
{name}
|
||||
</button>
|
||||
{errors && errors.length > 0 && (
|
||||
<em className="text-destroy-80 text-xs">has error</em>
|
||||
)}
|
||||
{visibilityToggle && <VisibilityToggle {...visibilityToggle} />}
|
||||
{menuItems && (
|
||||
<ContextMenu menuTargetElement={menuRef} items={menuItems} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A button with an icon, name, and context menu
|
||||
* for an operation in the feature tree.
|
||||
*/
|
||||
const OperationItem = (props: {
|
||||
item: Operation
|
||||
send: Prop<Actor<typeof featureTreeMachine>, 'send'>
|
||||
}) => {
|
||||
const kclContext = useKclContext()
|
||||
const name =
|
||||
'name' in props.item && props.item.name !== null
|
||||
? getOperationLabel(props.item)
|
||||
: 'anonymous'
|
||||
const errors = useMemo(() => {
|
||||
return kclContext.diagnostics.filter(
|
||||
(diag) =>
|
||||
diag.severity === 'error' &&
|
||||
'sourceRange' in props.item &&
|
||||
diag.from >= props.item.sourceRange[0] &&
|
||||
diag.to <= props.item.sourceRange[1]
|
||||
)
|
||||
}, [kclContext.diagnostics.length])
|
||||
|
||||
function selectOperation() {
|
||||
if (props.item.type === 'UserDefinedFunctionReturn') {
|
||||
return
|
||||
}
|
||||
props.send({
|
||||
type: 'selectOperation',
|
||||
data: {
|
||||
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* For now we can only enter the "edit" flow for the startSketchOn operation.
|
||||
* TODO: https://github.com/KittyCAD/modeling-app/issues/4442
|
||||
*/
|
||||
function enterEditFlow() {
|
||||
if (
|
||||
props.item.type === 'StdLibCall' &&
|
||||
props.item.name === 'startSketchOn'
|
||||
) {
|
||||
props.send({
|
||||
type: 'enterEditFlow',
|
||||
data: {
|
||||
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
if (props.item.type === 'UserDefinedFunctionReturn') {
|
||||
return
|
||||
}
|
||||
props.send({
|
||||
type: 'goToKclSource',
|
||||
data: {
|
||||
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
View KCL source code
|
||||
</ContextMenuItem>,
|
||||
...(props.item.type === 'UserDefinedFunctionCall'
|
||||
? [
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
if (props.item.type !== 'UserDefinedFunctionCall') {
|
||||
return
|
||||
}
|
||||
const functionRange = props.item.functionSourceRange
|
||||
// For some reason, the cursor goes to the end of the source
|
||||
// range we select. So set the end equal to the beginning.
|
||||
functionRange[1] = functionRange[0]
|
||||
props.send({
|
||||
type: 'goToKclSource',
|
||||
data: {
|
||||
targetSourceRange: sourceRangeFromRust(functionRange),
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
View function definition
|
||||
</ContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[props.item, props.send]
|
||||
)
|
||||
|
||||
return (
|
||||
<OperationItemWrapper
|
||||
icon={getOperationIcon(props.item)}
|
||||
name={name}
|
||||
menuItems={menuItems}
|
||||
onClick={selectOperation}
|
||||
onDoubleClick={enterEditFlow}
|
||||
errors={errors}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { TEST } from 'env'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||
import { lineHighlightField } from 'editor/highlightextension'
|
||||
import { onMouseDragMakeANewNumber, onMouseDragRegex } from 'lib/utils'
|
||||
@ -36,7 +36,7 @@ import interact from '@replit/codemirror-interact'
|
||||
import { kclManager, editorManager, codeManager } from 'lib/singletons'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
import { Prec, EditorState, Extension } from '@codemirror/state'
|
||||
import { Prec, EditorState, Extension, Transaction } from '@codemirror/state'
|
||||
import {
|
||||
closeBrackets,
|
||||
closeBracketsKeymap,
|
||||
@ -44,6 +44,13 @@ import {
|
||||
} from '@codemirror/autocomplete'
|
||||
import CodeEditor from './CodeEditor'
|
||||
import { codeManagerHistoryCompartment } from 'lang/codeManager'
|
||||
import {
|
||||
editorIsMountedSelector,
|
||||
kclEditorActor,
|
||||
selectionEventSelector,
|
||||
} from 'machines/kclEditorMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { modelingMachineEvent } from 'editor/manager'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
@ -59,6 +66,8 @@ export const KclEditorPane = () => {
|
||||
const {
|
||||
settings: { context },
|
||||
} = useSettingsAuthContext()
|
||||
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
|
||||
const editorIsMounted = useSelector(kclEditorActor, editorIsMountedSelector)
|
||||
const theme =
|
||||
context.app.theme.current === Themes.System
|
||||
? getSystemTheme()
|
||||
@ -76,6 +85,25 @@ export const KclEditorPane = () => {
|
||||
editorManager.redo()
|
||||
})
|
||||
|
||||
// When this component unmounts, we need to tell the machine that the editor
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
kclEditorActor.send({ type: 'setKclEditorMounted', data: false })
|
||||
kclEditorActor.send({ type: 'setLastSelectionEvent', data: undefined })
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorIsMounted || !lastSelectionEvent || !editorManager.editorView) {
|
||||
return
|
||||
}
|
||||
editorManager.editorView.dispatch({
|
||||
selection: lastSelectionEvent.codeMirrorSelection,
|
||||
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
|
||||
scrollIntoView: lastSelectionEvent.scrollIntoView,
|
||||
})
|
||||
}, [editorIsMounted, lastSelectionEvent])
|
||||
|
||||
const textWrapping = context.textEditor.textWrapping
|
||||
const cursorBlinking = context.textEditor.blinkingCursor
|
||||
// DO NOT ADD THE CODEMIRROR HOTKEYS HERE TO THE DEPENDENCY ARRAY
|
||||
@ -174,6 +202,7 @@ export const KclEditorPane = () => {
|
||||
if (_editorView === null) return
|
||||
|
||||
editorManager.setEditorView(_editorView)
|
||||
kclEditorActor.send({ type: 'setKclEditorMounted', data: true })
|
||||
|
||||
// On first load of this component, ensure we show the current errors
|
||||
// in the editor.
|
||||
|
@ -17,12 +17,14 @@ import { useKclContext } from 'lang/KclProvider'
|
||||
import { editorManager } from 'lib/singletons'
|
||||
import { ContextFrom } from 'xstate'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { FeatureTreePane } from './FeatureTreePane'
|
||||
|
||||
export type SidebarType =
|
||||
| 'code'
|
||||
| 'debug'
|
||||
| 'export'
|
||||
| 'files'
|
||||
| 'feature-tree'
|
||||
| 'logs'
|
||||
| 'lspMessages'
|
||||
| 'variables'
|
||||
@ -69,6 +71,23 @@ export type SidebarAction = {
|
||||
// changes to be a spinning loader on loading.
|
||||
|
||||
export const sidebarPanes: SidebarPane[] = [
|
||||
{
|
||||
id: 'feature-tree',
|
||||
icon: 'model',
|
||||
keybinding: 'Shift + T',
|
||||
sidebarName: 'Feature Tree',
|
||||
Content: (props) => (
|
||||
<>
|
||||
<ModelingPaneHeader
|
||||
id={props.id}
|
||||
icon="model"
|
||||
title="Feature Tree"
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
<FeatureTreePane />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
icon: 'code',
|
||||
|
@ -20,6 +20,7 @@ import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
@ -302,7 +303,7 @@ function ModelingPaneButton({
|
||||
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
||||
onClick={onClick}
|
||||
name={paneConfig.sidebarName}
|
||||
data-testid={paneConfig.id + '-pane-button'}
|
||||
data-testid={paneConfig.id + SIDEBAR_BUTTON_SUFFIX}
|
||||
disabled={disabledText !== undefined}
|
||||
aria-disabled={disabledText !== undefined}
|
||||
{...props}
|
||||
|
@ -4,7 +4,10 @@ import { useFileContext } from 'hooks/useFileContext'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import toast from 'react-hot-toast'
|
||||
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import {
|
||||
TextToCad_type,
|
||||
TextToCadIteration_type,
|
||||
} from '@kittycad/lib/dist/types/src/models'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Box3,
|
||||
@ -29,6 +32,7 @@ import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { EventFrom } from 'xstate'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
|
||||
const CANVAS_SIZE = 128
|
||||
const PROMPT_TRUNCATE_LENGTH = 128
|
||||
@ -411,3 +415,67 @@ function traverseSceneToStyleObjects({
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function ToastPromptToEditCadSuccess({
|
||||
toastId,
|
||||
token,
|
||||
data,
|
||||
oldCode,
|
||||
}: {
|
||||
toastId: string
|
||||
oldCode: string
|
||||
data: TextToCadIteration_type
|
||||
token?: string
|
||||
}) {
|
||||
const modelId = data.id
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 min-w-80">
|
||||
<div className="flex flex-col justify-between gap-6">
|
||||
<section>
|
||||
<h2>Prompt to edit successful</h2>
|
||||
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
|
||||
Prompt: "
|
||||
{data?.prompt && data?.prompt?.length > PROMPT_TRUNCATE_LENGTH
|
||||
? data.prompt.slice(0, PROMPT_TRUNCATE_LENGTH) + '...'
|
||||
: data.prompt}
|
||||
"
|
||||
</p>
|
||||
<p>Do you want to keep the change?</p>
|
||||
</section>
|
||||
<div className="flex justify-between gap-8">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'close',
|
||||
}}
|
||||
data-negative-button={'reject'}
|
||||
name={'Reject'}
|
||||
onClick={() => {
|
||||
sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
|
||||
codeManager.updateCodeEditor(oldCode)
|
||||
kclManager.executeCode().catch(reportRejection)
|
||||
toast.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
{'Reject'}
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'checkmark',
|
||||
}}
|
||||
name="Accept"
|
||||
onClick={() => {
|
||||
sendTelemetry(modelId, 'accepted', token).catch(reportRejection)
|
||||
toast.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
} from '@codemirror/lint'
|
||||
import { StateFrom } from 'xstate'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { kclEditorActor } from 'machines/kclEditorMachine'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -70,6 +71,7 @@ export default class EditorManager {
|
||||
|
||||
setEditorView(editorView: EditorView) {
|
||||
this._editorView = editorView
|
||||
kclEditorActor.send({ type: 'setKclEditorMounted', data: true })
|
||||
this.overrideTreeHighlighterUpdateForPerformanceTracking()
|
||||
}
|
||||
|
||||
@ -207,6 +209,32 @@ export default class EditorManager {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the first selection in the editor.
|
||||
*/
|
||||
scrollToSelection() {
|
||||
if (!this._editorView || !this._selectionRanges.graphSelections[0]) return
|
||||
|
||||
const firstSelection = this._selectionRanges.graphSelections[0]
|
||||
|
||||
this._editorView.focus()
|
||||
this._editorView.dispatch({
|
||||
effects: [
|
||||
EditorView.scrollIntoView(
|
||||
EditorSelection.range(
|
||||
firstSelection.codeRef.range[0],
|
||||
firstSelection.codeRef.range[1]
|
||||
),
|
||||
{ y: 'center' }
|
||||
),
|
||||
],
|
||||
annotations: [
|
||||
updateOutsideEditorEvent,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
scrollToFirstErrorDiagnosticIfExists() {
|
||||
if (!this._editorView) return
|
||||
|
||||
|
@ -311,6 +311,14 @@ code {
|
||||
@apply bg-chalkboard-20 text-chalkboard-80;
|
||||
@apply dark:bg-chalkboard-80 dark:text-chalkboard-30;
|
||||
}
|
||||
|
||||
button.reset {
|
||||
@apply bg-transparent border-transparent m-0 p-0 rounded-none text-base;
|
||||
}
|
||||
|
||||
button.reset:hover {
|
||||
@apply bg-transparent border-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-scroller,
|
||||
|
@ -3,6 +3,7 @@ import { type IndexLoaderData } from 'lib/types'
|
||||
import { useLoaderData } from 'react-router-dom'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { KCLError } from './errors'
|
||||
|
||||
const KclContext = createContext({
|
||||
code: codeManager?.code || '',
|
||||
@ -11,6 +12,7 @@ const KclContext = createContext({
|
||||
isExecuting: kclManager?.isExecuting,
|
||||
diagnostics: kclManager?.diagnostics,
|
||||
logs: kclManager?.logs,
|
||||
errors: kclManager?.errors,
|
||||
wasmInitFailed: kclManager?.wasmInitFailed,
|
||||
})
|
||||
|
||||
@ -32,7 +34,8 @@ export function KclContextProvider({
|
||||
const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
|
||||
const [ast, setAst] = useState(kclManager.ast)
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const [diagnostics, setErrors] = useState<Diagnostic[]>([])
|
||||
const [diagnostics, setDiagnostics] = useState<Diagnostic[]>([])
|
||||
const [errors, setErrors] = useState<KCLError[]>([])
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const [wasmInitFailed, setWasmInitFailed] = useState(false)
|
||||
|
||||
@ -44,7 +47,8 @@ export function KclContextProvider({
|
||||
setProgramMemory,
|
||||
setAst,
|
||||
setLogs,
|
||||
setKclErrors: setErrors,
|
||||
setErrors,
|
||||
setDiagnostics,
|
||||
setIsExecuting,
|
||||
setWasmInitFailed,
|
||||
})
|
||||
@ -59,6 +63,7 @@ export function KclContextProvider({
|
||||
isExecuting,
|
||||
diagnostics,
|
||||
logs,
|
||||
errors,
|
||||
wasmInitFailed,
|
||||
}}
|
||||
>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { executeAst, lintAst } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { handleSelectionBatch, Selections } from 'lib/selections'
|
||||
import {
|
||||
KCLError,
|
||||
complilationErrorsToDiagnostics,
|
||||
@ -28,7 +28,11 @@ import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { EntityType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import {
|
||||
EntityType_type,
|
||||
ModelingCmdReq_type,
|
||||
} from '@kittycad/lib/dist/types/src/models'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
|
||||
interface ExecuteArgs {
|
||||
ast?: Node<Program>
|
||||
@ -55,7 +59,9 @@ export class KclManager {
|
||||
private _execState: ExecState = emptyExecState()
|
||||
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
||||
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
|
||||
lastSuccessfulOperations: Operation[] = []
|
||||
private _logs: string[] = []
|
||||
private _errors: KCLError[] = []
|
||||
private _diagnostics: Diagnostic[] = []
|
||||
private _isExecuting = false
|
||||
private _executeIsStale: ExecuteArgs | null = null
|
||||
@ -69,7 +75,8 @@ export class KclManager {
|
||||
private _astCallBack: (arg: Node<Program>) => void = () => {}
|
||||
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
|
||||
private _logsCallBack: (arg: string[]) => void = () => {}
|
||||
private _kclErrorsCallBack: (errors: Diagnostic[]) => void = () => {}
|
||||
private _kclErrorsCallBack: (errors: KCLError[]) => void = () => {}
|
||||
private _diagnosticsCallback: (errors: Diagnostic[]) => void = () => {}
|
||||
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
|
||||
private _executeCallback: () => void = () => {}
|
||||
|
||||
@ -103,6 +110,13 @@ export class KclManager {
|
||||
return this._execState
|
||||
}
|
||||
|
||||
get errors() {
|
||||
return this._errors
|
||||
}
|
||||
set errors(errors) {
|
||||
this._errors = errors
|
||||
this._kclErrorsCallBack(errors)
|
||||
}
|
||||
get logs() {
|
||||
return this._logs
|
||||
}
|
||||
@ -132,7 +146,7 @@ export class KclManager {
|
||||
|
||||
setDiagnosticsForCurrentErrors() {
|
||||
editorManager?.setDiagnostics(this.diagnostics)
|
||||
this._kclErrorsCallBack(this.diagnostics)
|
||||
this._diagnosticsCallback(this.diagnostics)
|
||||
}
|
||||
|
||||
get isExecuting() {
|
||||
@ -185,21 +199,24 @@ export class KclManager {
|
||||
setProgramMemory,
|
||||
setAst,
|
||||
setLogs,
|
||||
setKclErrors,
|
||||
setErrors,
|
||||
setDiagnostics,
|
||||
setIsExecuting,
|
||||
setWasmInitFailed,
|
||||
}: {
|
||||
setProgramMemory: (arg: ProgramMemory) => void
|
||||
setAst: (arg: Node<Program>) => void
|
||||
setLogs: (arg: string[]) => void
|
||||
setKclErrors: (errors: Diagnostic[]) => void
|
||||
setErrors: (errors: KCLError[]) => void
|
||||
setDiagnostics: (errors: Diagnostic[]) => void
|
||||
setIsExecuting: (arg: boolean) => void
|
||||
setWasmInitFailed: (arg: boolean) => void
|
||||
}) {
|
||||
this._programMemoryCallBack = setProgramMemory
|
||||
this._astCallBack = setAst
|
||||
this._logsCallBack = setLogs
|
||||
this._kclErrorsCallBack = setKclErrors
|
||||
this._kclErrorsCallBack = setErrors
|
||||
this._diagnosticsCallback = setDiagnostics
|
||||
this._isExecutingCallback = setIsExecuting
|
||||
this._wasmInitFailedCallback = setWasmInitFailed
|
||||
}
|
||||
@ -311,7 +328,7 @@ export class KclManager {
|
||||
// Do not send send scene commands if the program was interrupted, go to clean up
|
||||
if (!isInterrupted) {
|
||||
this.addDiagnostics(await lintAst({ ast: ast }))
|
||||
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
|
||||
setSelectionFilterToDefault(this.engineCommandManager)
|
||||
|
||||
if (args.zoomToFit) {
|
||||
let zoomObjectId: string | undefined = ''
|
||||
@ -349,11 +366,13 @@ export class KclManager {
|
||||
}
|
||||
|
||||
this.logs = logs
|
||||
this.errors = errors
|
||||
// Do not add the errors since the program was interrupted and the error is not a real KCL error
|
||||
this.addDiagnostics(isInterrupted ? [] : kclErrorsToDiagnostics(errors))
|
||||
this.execState = execState
|
||||
if (!errors.length) {
|
||||
this.lastSuccessfulProgramMemory = execState.memory
|
||||
this.lastSuccessfulOperations = execState.operations
|
||||
}
|
||||
this.ast = { ...ast }
|
||||
// updateArtifactGraph relies on updated executeState/programMemory
|
||||
@ -408,6 +427,7 @@ export class KclManager {
|
||||
this._programMemory = execState.memory
|
||||
if (!errors.length) {
|
||||
this.lastSuccessfulProgramMemory = execState.memory
|
||||
this.lastSuccessfulOperations = execState.operations
|
||||
}
|
||||
if (updates !== 'artifactRanges') return
|
||||
|
||||
@ -603,8 +623,8 @@ export class KclManager {
|
||||
return Promise.all(thePromises)
|
||||
}
|
||||
/** TODO: this function is hiding unawaited asynchronous work */
|
||||
defaultSelectionFilter() {
|
||||
setSelectionFilterToDefault(this.programMemory, this.engineCommandManager)
|
||||
defaultSelectionFilter(selectionsToRestore?: Selections) {
|
||||
setSelectionFilterToDefault(this.engineCommandManager, selectionsToRestore)
|
||||
}
|
||||
/** TODO: this function is hiding unawaited asynchronous work */
|
||||
setSelectionFilter(filter: EntityType_type[]) {
|
||||
@ -640,25 +660,65 @@ const defaultSelectionFilter: EntityType_type[] = [
|
||||
|
||||
/** TODO: This function is not synchronous but is currently treated as such */
|
||||
function setSelectionFilterToDefault(
|
||||
programMemory: ProgramMemory,
|
||||
engineCommandManager: EngineCommandManager
|
||||
engineCommandManager: EngineCommandManager,
|
||||
selectionsToRestore?: Selections
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
setSelectionFilter(defaultSelectionFilter, engineCommandManager)
|
||||
setSelectionFilter(
|
||||
defaultSelectionFilter,
|
||||
engineCommandManager,
|
||||
selectionsToRestore
|
||||
)
|
||||
}
|
||||
|
||||
/** TODO: This function is not synchronous but is currently treated as such */
|
||||
function setSelectionFilter(
|
||||
filter: EntityType_type[],
|
||||
engineCommandManager: EngineCommandManager
|
||||
engineCommandManager: EngineCommandManager,
|
||||
selectionsToRestore?: Selections
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_selection_filter',
|
||||
filter,
|
||||
},
|
||||
const { engineEvents } = selectionsToRestore
|
||||
? handleSelectionBatch({
|
||||
selections: selectionsToRestore,
|
||||
})
|
||||
: { engineEvents: undefined }
|
||||
if (!selectionsToRestore || !engineEvents) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_selection_filter',
|
||||
filter,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const modelingCmd: ModelingCmdReq_type[] = []
|
||||
engineEvents.forEach((event) => {
|
||||
if (event.type === 'modeling_cmd_req') {
|
||||
modelingCmd.push({
|
||||
cmd_id: uuidv4(),
|
||||
cmd: event.cmd,
|
||||
})
|
||||
}
|
||||
})
|
||||
// batch is needed other wise the selection flickers.
|
||||
engineCommandManager
|
||||
.sendSceneCommand({
|
||||
type: 'modeling_cmd_batch_req',
|
||||
batch_id: uuidv4(),
|
||||
requests: [
|
||||
{
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_selection_filter',
|
||||
filter,
|
||||
},
|
||||
},
|
||||
...modelingCmd,
|
||||
],
|
||||
responses: false,
|
||||
})
|
||||
.catch(reportError)
|
||||
}
|
||||
|
@ -1146,31 +1146,43 @@ export async function deleteFromSelection(
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
if (
|
||||
(selection?.artifact?.type === 'wall' ||
|
||||
((selection?.artifact?.type === 'wall' ||
|
||||
selection?.artifact?.type === 'cap') &&
|
||||
varDec.node.init.type === 'PipeExpression'
|
||||
varDec.node.init.type === 'PipeExpression') ||
|
||||
selection.artifact?.type === 'sweep'
|
||||
) {
|
||||
const varDecName = varDec.node.id.name
|
||||
let pathToNode: PathToNode | null = null
|
||||
let extrudeNameToDelete = ''
|
||||
traverse(astClone, {
|
||||
enter: (node, path) => {
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
const dec = node.declaration
|
||||
if (
|
||||
dec.init.type === 'CallExpression' &&
|
||||
(dec.init.callee.name === 'extrude' ||
|
||||
dec.init.callee.name === 'revolve') &&
|
||||
dec.init.arguments?.[1].type === 'Identifier' &&
|
||||
dec.init.arguments?.[1].name === varDecName
|
||||
) {
|
||||
pathToNode = path
|
||||
extrudeNameToDelete = dec.id.name
|
||||
let pathToNode: PathToNode | null = null
|
||||
if (selection.artifact?.type !== 'sweep') {
|
||||
const varDecName = varDec.node.id.name
|
||||
traverse(astClone, {
|
||||
enter: (node, path) => {
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
const dec = node.declaration
|
||||
if (
|
||||
dec.init.type === 'CallExpression' &&
|
||||
(dec.init.callee.name === 'extrude' ||
|
||||
dec.init.callee.name === 'revolve') &&
|
||||
dec.init.arguments?.[1].type === 'Identifier' &&
|
||||
dec.init.arguments?.[1].name === varDecName
|
||||
) {
|
||||
pathToNode = path
|
||||
extrudeNameToDelete = dec.id.name
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
if (!pathToNode) return new Error('Could not find extrude variable')
|
||||
},
|
||||
})
|
||||
if (!pathToNode) return new Error('Could not find extrude variable')
|
||||
} else {
|
||||
pathToNode = selection.codeRef.pathToNode
|
||||
const extrudeVarDec = getNodeFromPath<VariableDeclarator>(
|
||||
astClone,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(extrudeVarDec)) return extrudeVarDec
|
||||
extrudeNameToDelete = extrudeVarDec.node.id.name
|
||||
}
|
||||
|
||||
const expressionIndex = pathToNode[1][0] as number
|
||||
astClone.body.splice(expressionIndex, 1)
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
isNodeSafeToReplace,
|
||||
isTypeInValue,
|
||||
getNodePathFromSourceRange,
|
||||
doesPipeHaveCallExp,
|
||||
hasExtrudeSketch,
|
||||
findUsesOfTagInPipe,
|
||||
hasSketchPipeBeenExtruded,
|
||||
@ -362,82 +361,6 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing doesPipeHave', () => {
|
||||
it('finds close', () => {
|
||||
const exampleCode = `length001 = 2
|
||||
part001 = startSketchAt([-1.41, 3.46])
|
||||
|> line([19.49, 1.16], %, $seg01)
|
||||
|> angledLine([-35, length001], %)
|
||||
|> line([-3.22, -7.36], %)
|
||||
|> angledLine([-175, segLen(seg01)], %)
|
||||
|> close(%)
|
||||
`
|
||||
const ast = assertParse(exampleCode)
|
||||
|
||||
const result = doesPipeHaveCallExp({
|
||||
calleeName: 'close',
|
||||
ast,
|
||||
selection: {
|
||||
codeRef: codeRefFromRange([100, 101, true], ast),
|
||||
},
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('finds extrude', () => {
|
||||
const exampleCode = `length001 = 2
|
||||
part001 = startSketchAt([-1.41, 3.46])
|
||||
|> line([19.49, 1.16], %, $seg01)
|
||||
|> angledLine([-35, length001], %)
|
||||
|> line([-3.22, -7.36], %)
|
||||
|> angledLine([-175, segLen(seg01)], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
`
|
||||
const ast = assertParse(exampleCode)
|
||||
|
||||
const result = doesPipeHaveCallExp({
|
||||
calleeName: 'extrude',
|
||||
ast,
|
||||
selection: {
|
||||
codeRef: codeRefFromRange([100, 101, true], ast),
|
||||
},
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('does NOT find close', () => {
|
||||
const exampleCode = `length001 = 2
|
||||
part001 = startSketchAt([-1.41, 3.46])
|
||||
|> line([19.49, 1.16], %, $seg01)
|
||||
|> angledLine([-35, length001], %)
|
||||
|> line([-3.22, -7.36], %)
|
||||
|> angledLine([-175, segLen(seg01)], %)
|
||||
`
|
||||
const ast = assertParse(exampleCode)
|
||||
|
||||
const result = doesPipeHaveCallExp({
|
||||
calleeName: 'close',
|
||||
ast,
|
||||
selection: {
|
||||
codeRef: codeRefFromRange([100, 101, true], ast),
|
||||
},
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns false if not a pipe', () => {
|
||||
const exampleCode = `length001 = 2`
|
||||
const ast = assertParse(exampleCode)
|
||||
|
||||
const result = doesPipeHaveCallExp({
|
||||
calleeName: 'close',
|
||||
ast,
|
||||
selection: {
|
||||
codeRef: codeRefFromRange([9, 10, true], ast),
|
||||
},
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing hasExtrudeSketch', () => {
|
||||
it('find sketch', async () => {
|
||||
const exampleCode = `length001 = 2
|
||||
|
@ -831,33 +831,6 @@ export function isLinesParallelAndConstrained(
|
||||
}
|
||||
}
|
||||
|
||||
export function doesPipeHaveCallExp({
|
||||
ast,
|
||||
selection,
|
||||
calleeName,
|
||||
}: {
|
||||
calleeName: string
|
||||
ast: Program
|
||||
selection: Selection
|
||||
}): boolean {
|
||||
const pipeExpressionMeta = getNodeFromPath<PipeExpression>(
|
||||
ast,
|
||||
selection?.codeRef?.pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipeExpressionMeta)) {
|
||||
console.error(pipeExpressionMeta)
|
||||
return false
|
||||
}
|
||||
const pipeExpression = pipeExpressionMeta.node
|
||||
if (pipeExpression.type !== 'PipeExpression') return false
|
||||
return pipeExpression.body.some(
|
||||
(expression) =>
|
||||
expression.type === 'CallExpression' &&
|
||||
expression.callee.name === calleeName
|
||||
)
|
||||
}
|
||||
|
||||
export function hasExtrudeSketch({
|
||||
ast,
|
||||
selection,
|
||||
@ -886,6 +859,14 @@ export function hasExtrudeSketch({
|
||||
)
|
||||
}
|
||||
|
||||
export function artifactIsPlaneWithPaths(selectionRanges: Selections) {
|
||||
return (
|
||||
selectionRanges.graphSelections.length &&
|
||||
selectionRanges.graphSelections[0].artifact?.type === 'plane' &&
|
||||
selectionRanges.graphSelections[0].artifact.pathIds.length
|
||||
)
|
||||
}
|
||||
|
||||
export function isSingleCursorInPipe(
|
||||
selectionRanges: Selections,
|
||||
ast: Program
|
||||
|
@ -882,7 +882,7 @@ export function getArtifactFromRange(
|
||||
for (const artifact of artifactGraph.values()) {
|
||||
if ('codeRef' in artifact) {
|
||||
const match =
|
||||
artifact.codeRef.range[0] === range[0] &&
|
||||
artifact.codeRef?.range[0] === range[0] &&
|
||||
artifact.codeRef.range[1] === range[1]
|
||||
if (match) return artifact
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
|
||||
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
|
||||
|
||||
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
@ -245,6 +246,7 @@ export const isPathToNodeNumber = (
|
||||
|
||||
export interface ExecState {
|
||||
memory: ProgramMemory
|
||||
operations: Operation[]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -254,12 +256,14 @@ export interface ExecState {
|
||||
export function emptyExecState(): ExecState {
|
||||
return {
|
||||
memory: ProgramMemory.empty(),
|
||||
operations: [],
|
||||
}
|
||||
}
|
||||
|
||||
function execStateFromRaw(raw: RawExecState): ExecState {
|
||||
return {
|
||||
memory: ProgramMemory.fromRaw(raw.modLocal.memory),
|
||||
operations: raw.modLocal.operations,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,10 @@ export type ModelingCommandSchema = {
|
||||
'Text-to-CAD': {
|
||||
prompt: string
|
||||
}
|
||||
'Prompt-to-edit': {
|
||||
prompt: string
|
||||
selection: Selections
|
||||
}
|
||||
}
|
||||
|
||||
export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
@ -479,4 +483,29 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
},
|
||||
},
|
||||
},
|
||||
'Prompt-to-edit': {
|
||||
description: 'Use Zoo AI to edit your kcl',
|
||||
icon: 'chat',
|
||||
args: {
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: [
|
||||
'solid2D',
|
||||
'segment',
|
||||
'sweepEdge',
|
||||
'cap',
|
||||
'wall',
|
||||
'edgeCut',
|
||||
'edgeCutEdge',
|
||||
],
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: true,
|
||||
},
|
||||
prompt: {
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -136,3 +136,5 @@ export const VIEW_NAMES_SEMANTIC = {
|
||||
[AxisNames.NEG_Y]: 'Front',
|
||||
[AxisNames.NEG_Z]: 'Bottom',
|
||||
} as const
|
||||
/** The modeling sidebar buttons' IDs get a suffix to prevent collisions */
|
||||
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
|
||||
|
122
src/lib/operations.test.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { defaultRustSourceRange } from 'lang/wasm'
|
||||
import { filterOperations } from './operations'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
|
||||
function stdlib(name: string): Operation {
|
||||
return {
|
||||
type: 'StdLibCall',
|
||||
name,
|
||||
unlabeledArg: null,
|
||||
labeledArgs: {},
|
||||
sourceRange: defaultRustSourceRange(),
|
||||
isError: false,
|
||||
}
|
||||
}
|
||||
|
||||
function userCall(name: string): Operation {
|
||||
return {
|
||||
type: 'UserDefinedFunctionCall',
|
||||
name,
|
||||
functionSourceRange: defaultRustSourceRange(),
|
||||
unlabeledArg: null,
|
||||
labeledArgs: {},
|
||||
sourceRange: defaultRustSourceRange(),
|
||||
}
|
||||
}
|
||||
function userReturn(): Operation {
|
||||
return {
|
||||
type: 'UserDefinedFunctionReturn',
|
||||
}
|
||||
}
|
||||
|
||||
describe('operations filtering', () => {
|
||||
it('drops stdlib operations inside a user-defined function call', async () => {
|
||||
const operations = [
|
||||
stdlib('std1'),
|
||||
userCall('foo'),
|
||||
stdlib('std2'),
|
||||
stdlib('std3'),
|
||||
userReturn(),
|
||||
stdlib('std4'),
|
||||
stdlib('std5'),
|
||||
]
|
||||
const actual = filterOperations(operations)
|
||||
expect(actual).toEqual([
|
||||
stdlib('std1'),
|
||||
userCall('foo'),
|
||||
stdlib('std4'),
|
||||
stdlib('std5'),
|
||||
])
|
||||
})
|
||||
it('drops user-defined function calls that contain no stdlib operations', async () => {
|
||||
const operations = [
|
||||
stdlib('std1'),
|
||||
userCall('foo'),
|
||||
userReturn(),
|
||||
stdlib('std2'),
|
||||
userCall('bar'),
|
||||
userReturn(),
|
||||
stdlib('std3'),
|
||||
]
|
||||
const actual = filterOperations(operations)
|
||||
expect(actual).toEqual([stdlib('std1'), stdlib('std2'), stdlib('std3')])
|
||||
})
|
||||
it('preserves user-defined function calls at the end of the list', async () => {
|
||||
const operations = [stdlib('std1'), userCall('foo')]
|
||||
const actual = filterOperations(operations)
|
||||
expect(actual).toEqual([stdlib('std1'), userCall('foo')])
|
||||
})
|
||||
it('drops all user-defined function return operations', async () => {
|
||||
// The returns allow us to group operations with the call, but we never
|
||||
// display the returns.
|
||||
const operations = [
|
||||
stdlib('std1'),
|
||||
userCall('foo'),
|
||||
stdlib('std2'),
|
||||
userReturn(),
|
||||
stdlib('std3'),
|
||||
stdlib('std4'),
|
||||
userCall('foo2'),
|
||||
stdlib('std5'),
|
||||
stdlib('std6'),
|
||||
userReturn(),
|
||||
stdlib('std7'),
|
||||
]
|
||||
const actual = filterOperations(operations)
|
||||
expect(actual).toEqual([
|
||||
stdlib('std1'),
|
||||
userCall('foo'),
|
||||
stdlib('std3'),
|
||||
stdlib('std4'),
|
||||
userCall('foo2'),
|
||||
stdlib('std7'),
|
||||
])
|
||||
})
|
||||
it('correctly filters with nested function calls', async () => {
|
||||
const operations = [
|
||||
stdlib('std1'),
|
||||
userCall('foo'),
|
||||
stdlib('std2'),
|
||||
userReturn(),
|
||||
stdlib('std3'),
|
||||
stdlib('std4'),
|
||||
userCall('foo2'),
|
||||
stdlib('std5'),
|
||||
userCall('foo3-nested'),
|
||||
stdlib('std6'),
|
||||
userReturn(),
|
||||
stdlib('std7'),
|
||||
userReturn(),
|
||||
stdlib('std8'),
|
||||
]
|
||||
const actual = filterOperations(operations)
|
||||
expect(actual).toEqual([
|
||||
stdlib('std1'),
|
||||
userCall('foo'),
|
||||
stdlib('std3'),
|
||||
stdlib('std4'),
|
||||
userCall('foo2'),
|
||||
stdlib('std8'),
|
||||
])
|
||||
})
|
||||
})
|
180
src/lib/operations.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
|
||||
interface StdLibCallInfo {
|
||||
label: string
|
||||
icon: CustomIconName
|
||||
}
|
||||
|
||||
const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
chamfer: {
|
||||
label: 'Chamfer',
|
||||
icon: 'chamfer3d',
|
||||
},
|
||||
extrude: {
|
||||
label: 'Extrude',
|
||||
icon: 'extrude',
|
||||
},
|
||||
fillet: {
|
||||
label: 'Fillet',
|
||||
icon: 'fillet3d',
|
||||
},
|
||||
hole: {
|
||||
label: 'Hole',
|
||||
icon: 'hole',
|
||||
},
|
||||
hollow: {
|
||||
label: 'Hollow',
|
||||
icon: 'hollow',
|
||||
},
|
||||
import: {
|
||||
label: 'Import',
|
||||
icon: 'import',
|
||||
},
|
||||
loft: {
|
||||
label: 'Loft',
|
||||
icon: 'loft',
|
||||
},
|
||||
offsetPlane: {
|
||||
label: 'Offset Plane',
|
||||
icon: 'plane',
|
||||
},
|
||||
patternCircular2d: {
|
||||
label: 'Circular Pattern',
|
||||
icon: 'patternCircular2d',
|
||||
},
|
||||
patternCircular3d: {
|
||||
label: 'Circular Pattern',
|
||||
icon: 'patternCircular3d',
|
||||
},
|
||||
patternLinear2d: {
|
||||
label: 'Linear Pattern',
|
||||
icon: 'patternLinear2d',
|
||||
},
|
||||
patternLinear3d: {
|
||||
label: 'Linear Pattern',
|
||||
icon: 'patternLinear3d',
|
||||
},
|
||||
revolve: {
|
||||
label: 'Revolve',
|
||||
icon: 'revolve',
|
||||
},
|
||||
shell: {
|
||||
label: 'Shell',
|
||||
icon: 'shell',
|
||||
},
|
||||
startSketchOn: {
|
||||
label: 'Sketch',
|
||||
icon: 'sketch',
|
||||
},
|
||||
sweep: {
|
||||
label: 'Sweep',
|
||||
icon: 'sweep',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label of the operation
|
||||
*/
|
||||
export function getOperationLabel(op: Operation): string {
|
||||
switch (op.type) {
|
||||
case 'StdLibCall':
|
||||
return stdLibMap[op.name]?.label ?? op.name
|
||||
case 'UserDefinedFunctionCall':
|
||||
return op.name ?? 'Anonymous custom function'
|
||||
case 'UserDefinedFunctionReturn':
|
||||
return 'User function return'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the icon of the operation
|
||||
*/
|
||||
export function getOperationIcon(op: Operation): CustomIconName {
|
||||
switch (op.type) {
|
||||
case 'StdLibCall':
|
||||
return stdLibMap[op.name]?.icon ?? 'questionMark'
|
||||
default:
|
||||
return 'make-variable'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all filters to a list of operations.
|
||||
*/
|
||||
export function filterOperations(operations: Operation[]): Operation[] {
|
||||
return operationFilters.reduce((ops, filterFn) => filterFn(ops), operations)
|
||||
}
|
||||
|
||||
/**
|
||||
* The filters to apply to a list of operations
|
||||
* for use in the feature tree UI
|
||||
*/
|
||||
const operationFilters = [
|
||||
isNotUserFunctionWithNoOperations,
|
||||
isNotInsideUserFunction,
|
||||
isNotUserFunctionReturn,
|
||||
]
|
||||
|
||||
/**
|
||||
* A filter to exclude everything that occurs inside a UserDefinedFunctionCall
|
||||
* and its corresponding UserDefinedFunctionReturn from a list of operations.
|
||||
* This works even when there are nested function calls.
|
||||
*/
|
||||
function isNotInsideUserFunction(operations: Operation[]): Operation[] {
|
||||
const ops: Operation[] = []
|
||||
let depth = 0
|
||||
for (const op of operations) {
|
||||
if (depth === 0) {
|
||||
ops.push(op)
|
||||
}
|
||||
if (op.type === 'UserDefinedFunctionCall') {
|
||||
depth++
|
||||
}
|
||||
if (op.type === 'UserDefinedFunctionReturn') {
|
||||
depth--
|
||||
console.assert(
|
||||
depth >= 0,
|
||||
'Unbalanced UserDefinedFunctionCall and UserDefinedFunctionReturn; too many returns'
|
||||
)
|
||||
}
|
||||
}
|
||||
// Depth could be non-zero here if there was an error in execution.
|
||||
return ops
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter to exclude UserDefinedFunctionCall operations and their
|
||||
* corresponding UserDefinedFunctionReturn that don't have any operations inside
|
||||
* them from a list of operations.
|
||||
*/
|
||||
function isNotUserFunctionWithNoOperations(
|
||||
operations: Operation[]
|
||||
): Operation[] {
|
||||
return operations.filter((op, index) => {
|
||||
if (
|
||||
op.type === 'UserDefinedFunctionCall' &&
|
||||
// If this is a call at the end of the array, it's preserved.
|
||||
index < operations.length - 1 &&
|
||||
operations[index + 1].type === 'UserDefinedFunctionReturn'
|
||||
)
|
||||
return false
|
||||
if (
|
||||
op.type === 'UserDefinedFunctionReturn' &&
|
||||
// If this return is at the beginning of the array, it's preserved.
|
||||
index > 0 &&
|
||||
operations[index - 1].type === 'UserDefinedFunctionCall'
|
||||
)
|
||||
return false
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter to exclude UserDefinedFunctionReturn operations from a list of
|
||||
* operations.
|
||||
*/
|
||||
function isNotUserFunctionReturn(ops: Operation[]): Operation[] {
|
||||
return ops.filter((op) => op.type !== 'UserDefinedFunctionReturn')
|
||||
}
|
345
src/lib/promptToEdit.ts
Normal file
@ -0,0 +1,345 @@
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
import crossPlatformFetch from './crossPlatformFetch'
|
||||
import { err, reportRejection } from './trap'
|
||||
import { Selections } from './selections'
|
||||
import { ArtifactGraph, getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import toast from 'react-hot-toast'
|
||||
import { codeManager, editorManager, kclManager } from './singletons'
|
||||
import { ToastPromptToEditCadSuccess } from 'components/ToastTextToCad'
|
||||
import { uuidv4 } from './utils'
|
||||
import { diffLines } from 'diff'
|
||||
import { Transaction, EditorSelection, SelectionRange } from '@codemirror/state'
|
||||
import { modelingMachineEvent } from 'editor/manager'
|
||||
|
||||
function sourceIndexToLineColumn(
|
||||
code: string,
|
||||
index: number
|
||||
): { line: number; column: number } {
|
||||
const codeStart = code.slice(0, index)
|
||||
const lines = codeStart.split('\n')
|
||||
const line = lines.length
|
||||
const column = lines[lines.length - 1].length
|
||||
return { line, column }
|
||||
}
|
||||
|
||||
function convertAppRangeToApiRange(
|
||||
range: SourceRange,
|
||||
code: string
|
||||
): Models['SourceRange_type'] {
|
||||
return {
|
||||
start: sourceIndexToLineColumn(code, range[0]),
|
||||
end: sourceIndexToLineColumn(code, range[1]),
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitPromptToEditToQueue({
|
||||
prompt,
|
||||
selections,
|
||||
code,
|
||||
token,
|
||||
artifactGraph,
|
||||
}: {
|
||||
prompt: string
|
||||
selections: Selections
|
||||
code: string
|
||||
token?: string
|
||||
artifactGraph: ArtifactGraph
|
||||
}): Promise<Models['TextToCadIteration_type'] | Error> {
|
||||
const ranges: Models['TextToCadIterationBody_type']['source_ranges'] =
|
||||
selections.graphSelections.flatMap((selection) => {
|
||||
const artifact = selection.artifact
|
||||
const prompts: Models['TextToCadIterationBody_type']['source_ranges'] = []
|
||||
|
||||
if (artifact?.type === 'cap') {
|
||||
prompts.push({
|
||||
prompt: `The users main selection is the end cap of a general-sweep (that is an extrusion, revolve, sweep or loft).
|
||||
The source range most likely refers to "startProfileAt" simply because this is the start of the profile that was swept.
|
||||
If you need to operate on this cap, for example for sketching on the face, you can use the special string ${
|
||||
artifact.subType === 'end' ? 'END' : 'START'
|
||||
} i.e. \`startSketchOn(someSweepVariable, ${
|
||||
artifact.subType === 'end' ? 'END' : 'START'
|
||||
})\`
|
||||
When they made this selection they main have intended this surface directly or meant something more general like the sweep body.
|
||||
See later source ranges for more context.`,
|
||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||
})
|
||||
let sweep = getArtifactOfTypes(
|
||||
{ key: artifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (!err(sweep)) {
|
||||
prompts.push({
|
||||
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
||||
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
||||
})
|
||||
}
|
||||
}
|
||||
if (artifact?.type === 'wall') {
|
||||
prompts.push({
|
||||
prompt: `The users main selection is the wall of a general-sweep (that is an extrusion, revolve, sweep or loft).
|
||||
The source range though is for the original segment before it was extruded, you can add a tag to that segment in order to refer to this wall, for example "startSketchOn(someSweepVariable, segmentTag)"
|
||||
But it's also worth bearing in mind that the user may have intended to select the sweep itself, not this individual wall, see later source ranges for more context. about the sweep`,
|
||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||
})
|
||||
let sweep = getArtifactOfTypes(
|
||||
{ key: artifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (!err(sweep)) {
|
||||
prompts.push({
|
||||
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
||||
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
||||
})
|
||||
}
|
||||
}
|
||||
if (artifact?.type === 'sweepEdge') {
|
||||
prompts.push({
|
||||
prompt: `The users main selection is the edge of a general-sweep (that is an extrusion, revolve, sweep or loft).
|
||||
it is an ${
|
||||
artifact.subType
|
||||
} edge, in order to refer to this edge you should add a tag to the segment function in this source range,
|
||||
and then use the function ${
|
||||
artifact.subType === 'adjacent'
|
||||
? 'getAdjacentEdge'
|
||||
: 'getOppositeEdge'
|
||||
}
|
||||
See later source ranges for more context. about the sweep`,
|
||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||
})
|
||||
let sweep = getArtifactOfTypes(
|
||||
{ key: artifact.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (!err(sweep)) {
|
||||
prompts.push({
|
||||
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
||||
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
||||
})
|
||||
}
|
||||
}
|
||||
if (artifact?.type === 'segment') {
|
||||
if (!artifact.surfaceId) {
|
||||
prompts.push({
|
||||
prompt: `This selection is of a segment, likely an individual part of a profile. Segments are often "constrained" by the use of variables and relationships with other segments. Adding tags to segments helps refer to their length, angle or other properties`,
|
||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||
})
|
||||
} else {
|
||||
prompts.push({
|
||||
prompt: `This selection is for a segment (line, xLine, angledLine etc) that has been swept (a general-sweep, either an extrusion, revolve, sweep or loft).
|
||||
Because it now refers to an edge the way to refer to this edge is to add a tag to the segment, and then use that tag directly.
|
||||
i.e. \`fillet({ radius = someInteger, tags = [newTag] }, %)\` will work in the case of filleting this edge
|
||||
See later source ranges for more context. about the sweep`,
|
||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||
})
|
||||
let path = getArtifactOfTypes(
|
||||
{ key: artifact.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (!err(path)) {
|
||||
const sweep = getArtifactOfTypes(
|
||||
{ key: path.sweepId, types: ['sweep'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (!err(sweep)) {
|
||||
prompts.push({
|
||||
prompt: `This is the sweep's source range from the user's main selection of the edge.`,
|
||||
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return prompts
|
||||
})
|
||||
const body: Models['TextToCadIterationBody_type'] = {
|
||||
original_source_code: code,
|
||||
prompt,
|
||||
source_ranges: ranges,
|
||||
}
|
||||
const url = VITE_KC_API_BASE_URL + '/ml/text-to-cad/iteration'
|
||||
const data: Models['TextToCadIteration_type'] | Error =
|
||||
await crossPlatformFetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
token
|
||||
)
|
||||
|
||||
// Make sure we have an id.
|
||||
if (data instanceof Error) {
|
||||
return data
|
||||
}
|
||||
|
||||
if (!data.id) {
|
||||
return new Error('No id returned from Text-to-CAD API')
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getPromptToEditResult(
|
||||
id: string,
|
||||
token?: string
|
||||
): Promise<Models['TextToCadIteration_type'] | Error> {
|
||||
const url = VITE_KC_API_BASE_URL + '/async/operations/' + id
|
||||
const data: Models['TextToCadIteration_type'] | Error =
|
||||
await crossPlatformFetch(
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
token
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export async function doPromptEdit({
|
||||
prompt,
|
||||
selections,
|
||||
code,
|
||||
token,
|
||||
artifactGraph,
|
||||
}: {
|
||||
prompt: string
|
||||
selections: Selections
|
||||
code: string
|
||||
token?: string
|
||||
artifactGraph: ArtifactGraph
|
||||
}): Promise<Models['TextToCadIteration_type'] | Error> {
|
||||
const toastId = toast.loading('Submitting to Text-to-CAD API...')
|
||||
const submitResult = await submitPromptToEditToQueue({
|
||||
prompt,
|
||||
selections,
|
||||
code,
|
||||
token,
|
||||
artifactGraph,
|
||||
})
|
||||
if (err(submitResult)) return submitResult
|
||||
|
||||
const textToCadComplete = new Promise<Models['TextToCadIteration_type']>(
|
||||
(resolve, reject) => {
|
||||
;(async () => {
|
||||
const MAX_CHECK_TIMEOUT = 3 * 60_000
|
||||
const CHECK_DELAY = 200
|
||||
|
||||
let timeElapsed = 0
|
||||
|
||||
while (timeElapsed < MAX_CHECK_TIMEOUT) {
|
||||
const check = await getPromptToEditResult(submitResult.id, token)
|
||||
if (check instanceof Error || check.status === 'failed') {
|
||||
reject(check)
|
||||
return
|
||||
} else if (check.status === 'completed') {
|
||||
resolve(check)
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, CHECK_DELAY))
|
||||
timeElapsed += CHECK_DELAY
|
||||
}
|
||||
|
||||
reject(new Error('Text-to-CAD API timed out'))
|
||||
})().catch(reportRejection)
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
const result = await textToCadComplete
|
||||
toast.dismiss(toastId)
|
||||
return result
|
||||
} catch (e) {
|
||||
toast.dismiss(toastId)
|
||||
toast.error(
|
||||
'Failed to edit your KCL code, please try again with a different prompt or selection'
|
||||
)
|
||||
console.error('textToCadComplete', e)
|
||||
}
|
||||
|
||||
return textToCadComplete
|
||||
}
|
||||
|
||||
/** takes care of the whole submit prompt to endpoint flow including the accept-reject toast once the result is back */
|
||||
export async function promptToEditFlow({
|
||||
prompt,
|
||||
selections,
|
||||
code,
|
||||
token,
|
||||
artifactGraph,
|
||||
}: {
|
||||
prompt: string
|
||||
selections: Selections
|
||||
code: string
|
||||
token?: string
|
||||
artifactGraph: ArtifactGraph
|
||||
}) {
|
||||
const result = await doPromptEdit({
|
||||
prompt,
|
||||
selections,
|
||||
code,
|
||||
token,
|
||||
artifactGraph,
|
||||
})
|
||||
if (err(result)) return Promise.reject(result)
|
||||
const oldCode = codeManager.code
|
||||
const { code: newCode } = result
|
||||
codeManager.updateCodeEditor(newCode)
|
||||
const diff = reBuildNewCodeWithRanges(oldCode, newCode)
|
||||
const ranges: SelectionRange[] = diff.insertRanges.map((range) =>
|
||||
EditorSelection.range(range[0], range[1])
|
||||
)
|
||||
editorManager?.editorView?.dispatch({
|
||||
selection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.graphSelections.length - 1
|
||||
),
|
||||
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
|
||||
})
|
||||
await kclManager.executeCode()
|
||||
const toastId = uuidv4()
|
||||
|
||||
toast.success(
|
||||
() =>
|
||||
ToastPromptToEditCadSuccess({
|
||||
toastId,
|
||||
data: result,
|
||||
token,
|
||||
oldCode,
|
||||
}),
|
||||
{
|
||||
id: toastId,
|
||||
duration: Infinity,
|
||||
icon: null,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const reBuildNewCodeWithRanges = (
|
||||
oldCode: string,
|
||||
newCode: string
|
||||
): {
|
||||
newCode: string
|
||||
insertRanges: SourceRange[]
|
||||
} => {
|
||||
let insertRanges: SourceRange[] = []
|
||||
const changes = diffLines(oldCode, newCode)
|
||||
let newCodeWithRanges = ''
|
||||
for (const change of changes) {
|
||||
if (!change.added && !change.removed) {
|
||||
// no change add it to newCodeWithRanges
|
||||
newCodeWithRanges += change.value
|
||||
} else if (change.added && !change.removed) {
|
||||
const start = newCodeWithRanges.length
|
||||
const end = start + change.value.length
|
||||
insertRanges.push([start, end, true])
|
||||
newCodeWithRanges += change.value
|
||||
}
|
||||
}
|
||||
return {
|
||||
newCode: newCodeWithRanges,
|
||||
insertRanges,
|
||||
}
|
||||
}
|
@ -125,7 +125,9 @@ export const fileLoader: LoaderFunction = async (
|
||||
|
||||
// If persistCode in localStorage is present, it'll persist that code
|
||||
// through *anything*. INTENDED FOR TESTS.
|
||||
code = codeManager.localStoragePersistCode() || code
|
||||
if (window.electron.process.env.IS_PLAYWRIGHT) {
|
||||
code = codeManager.localStoragePersistCode() || code
|
||||
}
|
||||
|
||||
// Update both the state and the editor's code.
|
||||
// We explicitly do not write to the file here since we are loading from
|
||||
|
@ -18,10 +18,8 @@ import { getNormalisedCoordinates, isOverlap } from 'lib/utils'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { Program } from 'lang/wasm'
|
||||
import {
|
||||
doesPipeHaveCallExp,
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
hasSketchPipeBeenExtruded,
|
||||
isSingleCursorInPipe,
|
||||
} from 'lang/queryAst'
|
||||
import { CommandArgument } from './commandTypes'
|
||||
@ -490,6 +488,9 @@ function resetAndSetEngineEntitySelectionCmds(
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the selection a single cursor in a sketch pipe expression chain?
|
||||
*/
|
||||
export function isSketchPipe(selectionRanges: Selections) {
|
||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) return false
|
||||
return isCursorInSketchCommandRange(
|
||||
@ -498,115 +499,6 @@ export function isSketchPipe(selectionRanges: Selections) {
|
||||
)
|
||||
}
|
||||
|
||||
export function isSelectionLastLine(
|
||||
selectionRanges: Selections,
|
||||
code: string,
|
||||
i = 0
|
||||
) {
|
||||
return selectionRanges.graphSelections[i]?.codeRef?.range[1] === code.length
|
||||
}
|
||||
|
||||
export function isRangeBetweenCharacters(selectionRanges: Selections) {
|
||||
return (
|
||||
selectionRanges.graphSelections.length === 1 &&
|
||||
selectionRanges.graphSelections[0]?.codeRef?.range[0] === 0 &&
|
||||
selectionRanges.graphSelections[0]?.codeRef?.range[1] === 0
|
||||
)
|
||||
}
|
||||
|
||||
export type CommonASTNode = {
|
||||
selection: Selection
|
||||
ast: Program
|
||||
}
|
||||
|
||||
function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) {
|
||||
return {
|
||||
selection: selectionRanges.graphSelections[i],
|
||||
ast: kclManager.ast,
|
||||
}
|
||||
}
|
||||
|
||||
function nodeHasExtrude(node: CommonASTNode) {
|
||||
return (
|
||||
doesPipeHaveCallExp({
|
||||
calleeName: 'extrude',
|
||||
...node,
|
||||
}) ||
|
||||
doesPipeHaveCallExp({
|
||||
calleeName: 'revolve',
|
||||
...node,
|
||||
}) ||
|
||||
doesPipeHaveCallExp({
|
||||
calleeName: 'loft',
|
||||
...node,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function nodeHasClose(node: CommonASTNode) {
|
||||
return doesPipeHaveCallExp({
|
||||
calleeName: 'close',
|
||||
...node,
|
||||
})
|
||||
}
|
||||
function nodeHasCircle(node: CommonASTNode) {
|
||||
return doesPipeHaveCallExp({
|
||||
calleeName: 'circle',
|
||||
...node,
|
||||
})
|
||||
}
|
||||
|
||||
export function canSweepSelection(selection: Selections) {
|
||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||
buildCommonNodeFromSelection(selection, i)
|
||||
)
|
||||
return (
|
||||
!!isSketchPipe(selection) &&
|
||||
commonNodes.every((n) => !hasSketchPipeBeenExtruded(n.selection, n.ast)) &&
|
||||
(commonNodes.every((n) => nodeHasClose(n)) ||
|
||||
commonNodes.every((n) => nodeHasCircle(n))) &&
|
||||
commonNodes.every((n) => !nodeHasExtrude(n))
|
||||
)
|
||||
}
|
||||
|
||||
export function canRevolveSelection(selection: Selections) {
|
||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||
buildCommonNodeFromSelection(selection, i)
|
||||
)
|
||||
return (
|
||||
!!isSketchPipe(selection) &&
|
||||
(commonNodes.every((n) => nodeHasClose(n)) ||
|
||||
commonNodes.every((n) => nodeHasCircle(n)))
|
||||
)
|
||||
}
|
||||
|
||||
export function canLoftSelection(selection: Selections) {
|
||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||
buildCommonNodeFromSelection(selection, i)
|
||||
)
|
||||
return (
|
||||
!!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactGraph,
|
||||
selection
|
||||
) &&
|
||||
commonNodes.length > 1 &&
|
||||
commonNodes.every((n) => !hasSketchPipeBeenExtruded(n.selection, n.ast)) &&
|
||||
commonNodes.every((n) => nodeHasClose(n) || nodeHasCircle(n)) &&
|
||||
commonNodes.every((n) => !nodeHasExtrude(n))
|
||||
)
|
||||
}
|
||||
|
||||
export function canShellSelection(selection: Selections) {
|
||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||
buildCommonNodeFromSelection(selection, i)
|
||||
)
|
||||
return commonNodes.every(
|
||||
(n) =>
|
||||
n.selection.artifact?.type === 'cap' ||
|
||||
n.selection.artifact?.type === 'wall'
|
||||
)
|
||||
}
|
||||
|
||||
// This accounts for non-geometry selections under "other"
|
||||
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
||||
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
||||
@ -889,6 +781,14 @@ export function codeToIdSelections(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.artifact.type === 'sweep') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (bestCandidate) {
|
||||
|
@ -17,7 +17,7 @@ import { getNextFileName } from './desktopFS'
|
||||
import { reportRejection } from './trap'
|
||||
import { toSync } from './utils'
|
||||
|
||||
export async function submitTextToCadPrompt(
|
||||
async function submitTextToCadPrompt(
|
||||
prompt: string,
|
||||
token?: string
|
||||
): Promise<Models['TextToCad_type'] | Error> {
|
||||
@ -45,7 +45,7 @@ export async function submitTextToCadPrompt(
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getTextToCadResult(
|
||||
async function getTextToCadResult(
|
||||
id: string,
|
||||
token?: string
|
||||
): Promise<Models['TextToCad_type'] | Error> {
|
||||
|
@ -71,7 +71,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
: modelingSend({ type: 'Enter sketch' }),
|
||||
icon: 'sketch',
|
||||
status: 'available',
|
||||
disabled: (state) => !state.matches('idle'),
|
||||
title: ({ sketchPathId }) =>
|
||||
`${sketchPathId ? 'Edit' : 'Start'} Sketch`,
|
||||
showTitle: true,
|
||||
@ -89,7 +88,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Extrude', groupId: 'modeling' },
|
||||
}),
|
||||
disabled: (state) => !state.can({ type: 'Extrude' }),
|
||||
icon: 'extrude',
|
||||
status: 'available',
|
||||
title: 'Extrude',
|
||||
@ -104,9 +102,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Revolve', groupId: 'modeling' },
|
||||
}),
|
||||
// TODO: disabled
|
||||
// Who's state is this?
|
||||
disabled: (state) => !state.can({ type: 'Revolve' }),
|
||||
icon: 'revolve',
|
||||
status: DEV ? 'available' : 'kcl-only',
|
||||
title: 'Revolve',
|
||||
@ -144,7 +139,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Loft', groupId: 'modeling' },
|
||||
}),
|
||||
disabled: (state) => !state.can({ type: 'Loft' }),
|
||||
icon: 'loft',
|
||||
status: 'available',
|
||||
title: 'Loft',
|
||||
@ -172,7 +166,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
}),
|
||||
icon: 'fillet3d',
|
||||
status: DEV ? 'available' : 'kcl-only',
|
||||
disabled: (state) => !state.can({ type: 'Fillet' }),
|
||||
title: 'Fillet',
|
||||
hotkey: 'F',
|
||||
description: 'Round the edges of a 3D solid.',
|
||||
@ -196,7 +189,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
data: { name: 'Shell', groupId: 'modeling' },
|
||||
})
|
||||
},
|
||||
disabled: (state) => !state.can({ type: 'Shell' }),
|
||||
icon: 'shell',
|
||||
status: 'available',
|
||||
title: 'Shell',
|
||||
@ -440,10 +432,19 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
},
|
||||
{
|
||||
id: 'circle-three-points',
|
||||
onClick: () =>
|
||||
console.error('Three-point circle not yet implemented'),
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({
|
||||
Sketch: 'circle3PointToolSelect',
|
||||
})
|
||||
? 'circle3Points'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
icon: 'circle',
|
||||
status: 'unavailable',
|
||||
status: 'available',
|
||||
title: 'Three-point circle',
|
||||
showTitle: false,
|
||||
description: 'Draw a circle defined by three points',
|
||||
|
157
src/machines/featureTreeMachine.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { assign, setup } from 'xstate'
|
||||
|
||||
type FeatureTreeEvent =
|
||||
| {
|
||||
type: 'goToKclSource'
|
||||
data: { targetSourceRange: SourceRange }
|
||||
}
|
||||
| {
|
||||
type: 'selectOperation'
|
||||
data: { targetSourceRange: SourceRange }
|
||||
}
|
||||
| {
|
||||
type: 'enterEditFlow'
|
||||
data: { targetSourceRange: SourceRange }
|
||||
}
|
||||
| { type: 'goToError' }
|
||||
| { type: 'codePaneOpened' }
|
||||
| { type: 'selected' }
|
||||
| { type: 'done' }
|
||||
|
||||
export const featureTreeMachine = setup({
|
||||
types: {
|
||||
context: {} as { targetSourceRange?: SourceRange },
|
||||
events: {} as FeatureTreeEvent,
|
||||
},
|
||||
guards: {
|
||||
codePaneIsOpen: () => false,
|
||||
},
|
||||
actions: {
|
||||
saveTargetSourceRange: assign({
|
||||
targetSourceRange: ({ event }) =>
|
||||
'data' in event ? event.data.targetSourceRange : undefined,
|
||||
}),
|
||||
clearTargetSourceRange: assign({
|
||||
targetSourceRange: undefined,
|
||||
}),
|
||||
sendSelectionEvent: () => {},
|
||||
openCodePane: () => {},
|
||||
sendEditFlowStart: () => {},
|
||||
scrollToError: () => {},
|
||||
},
|
||||
}).createMachine({
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QDMwEMAuBXATmAKnmAHQCWEANmAMRQD2+dA0gMYUDKduLYA2gAwBdRKAAOdWKQyk6AOxEgAHogCMAFn7EAbAFYAHAHYAnGoOm9OgMw6tAGhABPRBsvF+a6wYBM-A-0uWepYGAL4h9qiYuAREZJQ0sGBULBgA8qJgOJgysgLCSCDiktJyCsoIKipabmo6-Co2Wip+-DoG9k4IWpp+Ae6W-N16KpZeYRHo2HiEYCTkVNRgshiZAKIQUgBiFHQA7nkKRVI5ZapergN6el46515GVdYdiN4GbjpGXiNGV2pan+MQJEpjFZsR6KRZFBGKwOFwcDxiIlktIodRkWAUpADgUjiV5AVyipWipiGZupYVCY1D5+F5ngg-jpiJ8DFo1OoDA13GNwkDJtEZiQIVCYWxONwSBA5DQcWIJMdSoTVG0yTodGoPAZKfwjP52o5EHVNIYvNd9M0zJZAcDBbERdDmOL4Yi6BlZJCoABhOgQMAABTQshoLF9AaDYHSS2xQkOCvxpy6lWIbS0Bm8JkM+ksDKZLK8bL0-S8NOCNoF01iGJSnqRSUxqKg6PrWIgcsK8ZOyq6aj0avTbU1Hg0KgZzVcrU+Xn+9Q+dPLUUrYOrjeI0uD1HbeK7oCJBj7xkafhMX28agZJeqHz0RhstTUD21WgXIKFxCWKxwnvWWx2uzrKKes2KIxvk8rFDuShGpY1RaKMRiHr4va9gyegPsQQRmiYVK+GYL52mCH6ZN+GwYNsexrjKm6xrinZKruqhaLBmr8PUvj6F4nGoeoxDEpYCFMVSowfPhS7CnQnqMKsOA4HQODEG6Syej6fqBhuoaqRGUbBm2NHgYqBIMV0-EproxLpoE7hWGOnFeGSxgaPwei6EYmaiaCczxLQDB0NJsk4FudGGVBFRpsQwRGGmgx6n4cH0oaFSVH21i6LqD5mtYejuW+DpSTJcmURugUQfRIX6MQfwcpqDxpUETwJSoXxqMQ04PjoSXspYtRhHyshhvABS2mJcYlcF5QALR2Al43TtoVJDD46ZfJS1p8kNHlxFQI0GYmNJjn8c21PuIzjuq2X2hJopOnCkrbQm3ZdXZNhsl1fzOSW1kJdYzKeK5VQqJh+7nWCuXXRKCIkCunp3ZB5RFq46ofOq6geDeo4JZqdlaEWt66l8jXfcD4mSWDLpSjKMOlUSzSwXS2qztVfy5jS2g43UnyVOcZ1rRWG2g7C4Ouu6ylhmpYCU2NiD8Zoz1wZq2NaB9OYYyz2O6gE3TtR8XVEwBDbQ7Ro2JtYT1pnLb2K7UyudIrFU3h8ZoamafhZTzi4bVDUJ6zWUIS7trGmS98vvVb+1GBhjUPtqer3KxOi657UCFeLhs7d2FmHe1ARmFr54NdqLUFrZnLBFUutEV+UI-mRf5+w9NgYZSNyBHB6rxTbcHhTY5wmDVMGrRM7tvhXJG-hRid10ZGjNUEjVWM533t4gaHh8aFgaOc+7XOXyzEVXpHkf+64p-p91T744VBEE7h0oEGpTZ0Dx2Sbhi9r4ozNLroN+XJk8hTBV51SRQGE7JiS9ErJjgnSRWC0bCu0Hq+C6JMf7yUUh6KEKlwzBj-uUKqKYgFQNAYrMcVI+yVFcqxfwFhAhf0uo6FByccHLyaC1JygRp5FkagaTo5CyFUj1KxO+gReRhCAA */
|
||||
id: 'featureTree',
|
||||
description: 'Workflows for interacting with the feature tree pane',
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
goToKclSource: {
|
||||
target: 'goingToKclSource',
|
||||
actions: 'saveTargetSourceRange',
|
||||
},
|
||||
|
||||
selectOperation: {
|
||||
target: 'selecting',
|
||||
actions: 'saveTargetSourceRange',
|
||||
},
|
||||
|
||||
enterEditFlow: {
|
||||
target: 'enteringEditFlow',
|
||||
actions: 'saveTargetSourceRange',
|
||||
},
|
||||
|
||||
goToError: 'goingToError',
|
||||
},
|
||||
},
|
||||
|
||||
goingToKclSource: {
|
||||
states: {
|
||||
selecting: {
|
||||
on: {
|
||||
selected: {
|
||||
target: 'done',
|
||||
},
|
||||
},
|
||||
|
||||
entry: ['sendSelectionEvent'],
|
||||
},
|
||||
|
||||
done: {
|
||||
entry: ['clearTargetSourceRange'],
|
||||
always: '#featureTree.idle',
|
||||
},
|
||||
|
||||
openingCodePane: {
|
||||
on: {
|
||||
codePaneOpened: 'selecting',
|
||||
},
|
||||
|
||||
entry: 'openCodePane',
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'openingCodePane',
|
||||
},
|
||||
|
||||
selecting: {
|
||||
states: {
|
||||
selecting: {
|
||||
on: {
|
||||
selected: 'done',
|
||||
},
|
||||
|
||||
entry: 'sendSelectionEvent',
|
||||
},
|
||||
|
||||
done: {
|
||||
always: '#featureTree.idle',
|
||||
entry: 'clearTargetSourceRange',
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'selecting',
|
||||
},
|
||||
|
||||
enteringEditFlow: {
|
||||
states: {
|
||||
selecting: {
|
||||
on: {
|
||||
selected: 'done',
|
||||
},
|
||||
},
|
||||
|
||||
done: {
|
||||
always: '#featureTree.idle',
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'selecting',
|
||||
entry: 'sendSelectionEvent',
|
||||
exit: ['clearTargetSourceRange', 'sendEditFlowStart'],
|
||||
},
|
||||
|
||||
goingToError: {
|
||||
states: {
|
||||
openingCodePane: {
|
||||
entry: 'openCodePane',
|
||||
|
||||
on: {
|
||||
codePaneOpened: 'done',
|
||||
},
|
||||
},
|
||||
|
||||
done: {
|
||||
entry: 'scrollToError',
|
||||
|
||||
always: '#featureTree.idle',
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'openingCodePane',
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'idle',
|
||||
})
|
67
src/machines/kclEditorMachine.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { assign, createActor, setup, StateFrom } from 'xstate'
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
|
||||
type SelectionEvent = {
|
||||
codeMirrorSelection: EditorSelection
|
||||
scrollIntoView: boolean
|
||||
}
|
||||
type KclEditorMachineEvent =
|
||||
| { type: 'setKclEditorMounted'; data: boolean }
|
||||
| { type: 'setLastSelectionEvent'; data?: SelectionEvent }
|
||||
|
||||
interface KclEditorMachineContext {
|
||||
isKclEditorMounted: boolean
|
||||
lastSelectionEvent?: SelectionEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a one-off XState machine not tied to React, so that we can publish
|
||||
* state to it from singletons and other parts of the app.
|
||||
*/
|
||||
export const kclEditorMachine = setup({
|
||||
types: {
|
||||
events: {} as KclEditorMachineEvent,
|
||||
context: {} as KclEditorMachineContext,
|
||||
},
|
||||
actions: {
|
||||
setKclEditorMounted: assign({
|
||||
isKclEditorMounted: ({ context, event }) =>
|
||||
event.type === 'setKclEditorMounted'
|
||||
? event.data
|
||||
: context.isKclEditorMounted,
|
||||
}),
|
||||
setLastSelectionEvent: assign({
|
||||
lastSelectionEvent: ({ context, event }) =>
|
||||
event.type === 'setLastSelectionEvent'
|
||||
? event.data
|
||||
: context.lastSelectionEvent,
|
||||
}),
|
||||
},
|
||||
}).createMachine({
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGsDGAbAohAlgFwHsAnAWQENUALHAOzAGJYw8BpDbfYkggVxr0gBtAAwBdRKAAOBWPhwEaEkAA9EARmEB2AHTCALACYAzGs0BWMwDYAHNZOWANCACeiALRrL2tQesGzamZ+FgYAnJqaAL7RTjQEEHBKaFi4hKQU1HRK0rJ48opIKupq2tZmwgZ6RpZ6msJGeqaOLupG1tq+bQFalZqWZtHRQA */
|
||||
id: 'kclEditorMachine',
|
||||
context: {
|
||||
isKclEditorMounted: false,
|
||||
lastSelectionEvent: undefined,
|
||||
},
|
||||
on: {
|
||||
setKclEditorMounted: {
|
||||
actions: 'setKclEditorMounted',
|
||||
},
|
||||
setLastSelectionEvent: {
|
||||
actions: 'setLastSelectionEvent',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const kclEditorActor = createActor(kclEditorMachine).start()
|
||||
|
||||
/** Watch for changes to `lastSelectionEvent` */
|
||||
export const selectionEventSelector = (
|
||||
snapshot?: StateFrom<typeof kclEditorMachine>
|
||||
) => snapshot?.context?.lastSelectionEvent
|
||||
|
||||
/** Watch for the editorView to be mounted */
|
||||
export const editorIsMountedSelector = (
|
||||
snapshot?: StateFrom<typeof kclEditorMachine>
|
||||
) => snapshot?.context?.isKclEditorMounted
|
@ -21,7 +21,7 @@ type Point2D = kcmc::shared::Point2d<f64>;
|
||||
type Point3D = kcmc::shared::Point3d<f64>;
|
||||
|
||||
pub use function_param::FunctionParam;
|
||||
pub use kcl_value::{KclObjectFields, KclValue};
|
||||
pub use kcl_value::{KclObjectFields, KclValue, UnitLen};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod annotations;
|
||||
@ -43,6 +43,7 @@ use crate::{
|
||||
settings::types::UnitLength,
|
||||
source_range::{ModuleId, SourceRange},
|
||||
std::{args::Arg, StdLib},
|
||||
walk::Node as WalkNode,
|
||||
ExecError, Program,
|
||||
};
|
||||
|
||||
@ -2002,9 +2003,12 @@ impl ExecutorContext {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut old_ast = old.ast.inner;
|
||||
let mut old_ast = old.ast;
|
||||
let mut new_ast = info.new_ast;
|
||||
|
||||
// The digests should already be computed, but just in case we don't
|
||||
// want to compare against none.
|
||||
old_ast.compute_digest();
|
||||
let mut new_ast = info.new_ast.inner.clone();
|
||||
new_ast.compute_digest();
|
||||
|
||||
// Check if the digest is the same.
|
||||
@ -2013,12 +2017,84 @@ impl ExecutorContext {
|
||||
}
|
||||
|
||||
// Check if the changes were only to Non-code areas, like comments or whitespace.
|
||||
Some(self.generate_changed_program(old_ast, new_ast))
|
||||
}
|
||||
|
||||
// For any unhandled cases just re-execute the whole thing.
|
||||
Some(CacheResult {
|
||||
clear_scene: true,
|
||||
program: info.new_ast,
|
||||
})
|
||||
/// Force-generate a new CacheResult, even if one shouldn't be made. The
|
||||
/// way in which this gets invoked should always be through
|
||||
/// [Self::get_changed_program]. This is purely to contain the logic on
|
||||
/// how we construct a new [CacheResult].
|
||||
pub fn generate_changed_program(&self, old_ast: Node<AstProgram>, new_ast: Node<AstProgram>) -> CacheResult {
|
||||
let mut generated_program = new_ast.clone();
|
||||
generated_program.body = vec![];
|
||||
|
||||
if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
|
||||
let old_node: WalkNode = old.into();
|
||||
let new_node: WalkNode = new.into();
|
||||
old_node.digest() == new_node.digest()
|
||||
}) {
|
||||
// If any of the nodes are different in the stretch of body that
|
||||
// overlaps, we have to bust cache and rebuild the scene. This
|
||||
// means a single insertion or deletion will result in a cache
|
||||
// bust.
|
||||
|
||||
return CacheResult {
|
||||
clear_scene: true,
|
||||
program: new_ast,
|
||||
};
|
||||
}
|
||||
|
||||
// otherwise the overlapping section of the ast bodies matches.
|
||||
// Let's see what the rest of the slice looks like.
|
||||
|
||||
match new_ast.body.len().cmp(&old_ast.body.len()) {
|
||||
std::cmp::Ordering::Less => {
|
||||
// the new AST is shorter than the old AST -- statements
|
||||
// were removed from the "current" code in the "new" code.
|
||||
//
|
||||
// Statements up until now match which means this is a
|
||||
// "pure delete" of the remaining slice, when we get to
|
||||
// supporting that.
|
||||
|
||||
// Cache bust time.
|
||||
CacheResult {
|
||||
clear_scene: true,
|
||||
program: new_ast,
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// the new AST is longer than the old AST, which means
|
||||
// statements were added to the new code we haven't previously
|
||||
// seen.
|
||||
//
|
||||
// Statements up until now are the same, which means this
|
||||
// is a "pure addition" of the remaining slice.
|
||||
|
||||
generated_program
|
||||
.body
|
||||
.extend_from_slice(&new_ast.body[old_ast.body.len()..]);
|
||||
|
||||
CacheResult {
|
||||
clear_scene: false,
|
||||
program: generated_program,
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
// currently unreachable, but lets pretend like the code
|
||||
// above can do something meaningful here for when we get
|
||||
// to diffing and yanking chunks of the program apart.
|
||||
|
||||
// We don't actually want to do anything here; so we're going
|
||||
// to not clear and do nothing. Is this wrong? I don't think
|
||||
// so but i think many things. This def needs to change
|
||||
// when the code above changes.
|
||||
|
||||
CacheResult {
|
||||
clear_scene: false,
|
||||
program: generated_program,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform the execution of a program.
|
||||
|
@ -92,25 +92,6 @@ impl Args {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn new_test_args() -> Result<Self> {
|
||||
use std::sync::Arc;
|
||||
|
||||
Ok(Self {
|
||||
args: Vec::new(),
|
||||
kw_args: Default::default(),
|
||||
source_range: SourceRange::default(),
|
||||
ctx: ExecutorContext {
|
||||
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
|
||||
fs: Arc::new(crate::fs::FileManager::new()),
|
||||
stdlib: Arc::new(crate::std::StdLib::new()),
|
||||
settings: Default::default(),
|
||||
context_type: crate::execution::ContextType::Mock,
|
||||
},
|
||||
pipe_value: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a keyword argument. If not set, returns None.
|
||||
pub(crate) fn get_kw_arg_opt<'a, T>(&'a self, label: &str) -> Option<T>
|
||||
where
|
||||
|
@ -5,14 +5,13 @@ use derive_docs::stdlib;
|
||||
|
||||
use crate::{
|
||||
errors::KclError,
|
||||
execution::{ExecState, KclValue},
|
||||
settings::types::UnitLength,
|
||||
execution::{ExecState, KclValue, UnitLen},
|
||||
std::Args,
|
||||
};
|
||||
|
||||
/// Millimeters conversion factor for current projects units.
|
||||
pub async fn mm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_mm(&args)?;
|
||||
pub async fn mm(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_mm(exec_state)?;
|
||||
|
||||
Ok(args.make_user_val_from_f64(result))
|
||||
}
|
||||
@ -40,20 +39,20 @@ pub async fn mm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
||||
name = "mm",
|
||||
tags = ["units"],
|
||||
}]
|
||||
fn inner_mm(args: &Args) -> Result<f64, KclError> {
|
||||
match args.ctx.settings.units {
|
||||
UnitLength::Mm => Ok(1.0),
|
||||
UnitLength::In => Ok(measurements::Length::from_millimeters(1.0).as_inches()),
|
||||
UnitLength::Ft => Ok(measurements::Length::from_millimeters(1.0).as_feet()),
|
||||
UnitLength::M => Ok(measurements::Length::from_millimeters(1.0).as_meters()),
|
||||
UnitLength::Cm => Ok(measurements::Length::from_millimeters(1.0).as_centimeters()),
|
||||
UnitLength::Yd => Ok(measurements::Length::from_millimeters(1.0).as_yards()),
|
||||
fn inner_mm(exec_state: &ExecState) -> Result<f64, KclError> {
|
||||
match exec_state.mod_local.settings.default_length_units {
|
||||
UnitLen::Mm => Ok(1.0),
|
||||
UnitLen::Inches => Ok(measurements::Length::from_millimeters(1.0).as_inches()),
|
||||
UnitLen::Feet => Ok(measurements::Length::from_millimeters(1.0).as_feet()),
|
||||
UnitLen::M => Ok(measurements::Length::from_millimeters(1.0).as_meters()),
|
||||
UnitLen::Cm => Ok(measurements::Length::from_millimeters(1.0).as_centimeters()),
|
||||
UnitLen::Yards => Ok(measurements::Length::from_millimeters(1.0).as_yards()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inches conversion factor for current projects units.
|
||||
pub async fn inch(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_inch(&args)?;
|
||||
pub async fn inch(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_inch(exec_state)?;
|
||||
|
||||
Ok(args.make_user_val_from_f64(result))
|
||||
}
|
||||
@ -81,20 +80,20 @@ pub async fn inch(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
||||
name = "inch",
|
||||
tags = ["units"],
|
||||
}]
|
||||
fn inner_inch(args: &Args) -> Result<f64, KclError> {
|
||||
match args.ctx.settings.units {
|
||||
UnitLength::Mm => Ok(measurements::Length::from_inches(1.0).as_millimeters()),
|
||||
UnitLength::In => Ok(1.0),
|
||||
UnitLength::Ft => Ok(measurements::Length::from_inches(1.0).as_feet()),
|
||||
UnitLength::M => Ok(measurements::Length::from_inches(1.0).as_meters()),
|
||||
UnitLength::Cm => Ok(measurements::Length::from_inches(1.0).as_centimeters()),
|
||||
UnitLength::Yd => Ok(measurements::Length::from_inches(1.0).as_yards()),
|
||||
fn inner_inch(exec_state: &ExecState) -> Result<f64, KclError> {
|
||||
match exec_state.mod_local.settings.default_length_units {
|
||||
UnitLen::Mm => Ok(measurements::Length::from_inches(1.0).as_millimeters()),
|
||||
UnitLen::Inches => Ok(1.0),
|
||||
UnitLen::Feet => Ok(measurements::Length::from_inches(1.0).as_feet()),
|
||||
UnitLen::M => Ok(measurements::Length::from_inches(1.0).as_meters()),
|
||||
UnitLen::Cm => Ok(measurements::Length::from_inches(1.0).as_centimeters()),
|
||||
UnitLen::Yards => Ok(measurements::Length::from_inches(1.0).as_yards()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Feet conversion factor for current projects units.
|
||||
pub async fn ft(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_ft(&args)?;
|
||||
pub async fn ft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_ft(exec_state)?;
|
||||
|
||||
Ok(args.make_user_val_from_f64(result))
|
||||
}
|
||||
@ -123,20 +122,20 @@ pub async fn ft(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
||||
name = "ft",
|
||||
tags = ["units"],
|
||||
}]
|
||||
fn inner_ft(args: &Args) -> Result<f64, KclError> {
|
||||
match args.ctx.settings.units {
|
||||
UnitLength::Mm => Ok(measurements::Length::from_feet(1.0).as_millimeters()),
|
||||
UnitLength::In => Ok(measurements::Length::from_feet(1.0).as_inches()),
|
||||
UnitLength::Ft => Ok(1.0),
|
||||
UnitLength::M => Ok(measurements::Length::from_feet(1.0).as_meters()),
|
||||
UnitLength::Cm => Ok(measurements::Length::from_feet(1.0).as_centimeters()),
|
||||
UnitLength::Yd => Ok(measurements::Length::from_feet(1.0).as_yards()),
|
||||
fn inner_ft(exec_state: &ExecState) -> Result<f64, KclError> {
|
||||
match exec_state.mod_local.settings.default_length_units {
|
||||
UnitLen::Mm => Ok(measurements::Length::from_feet(1.0).as_millimeters()),
|
||||
UnitLen::Inches => Ok(measurements::Length::from_feet(1.0).as_inches()),
|
||||
UnitLen::Feet => Ok(1.0),
|
||||
UnitLen::M => Ok(measurements::Length::from_feet(1.0).as_meters()),
|
||||
UnitLen::Cm => Ok(measurements::Length::from_feet(1.0).as_centimeters()),
|
||||
UnitLen::Yards => Ok(measurements::Length::from_feet(1.0).as_yards()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Meters conversion factor for current projects units.
|
||||
pub async fn m(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_m(&args)?;
|
||||
pub async fn m(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_m(exec_state)?;
|
||||
|
||||
Ok(args.make_user_val_from_f64(result))
|
||||
}
|
||||
@ -165,20 +164,20 @@ pub async fn m(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclE
|
||||
name = "m",
|
||||
tags = ["units"],
|
||||
}]
|
||||
fn inner_m(args: &Args) -> Result<f64, KclError> {
|
||||
match args.ctx.settings.units {
|
||||
UnitLength::Mm => Ok(measurements::Length::from_meters(1.0).as_millimeters()),
|
||||
UnitLength::In => Ok(measurements::Length::from_meters(1.0).as_inches()),
|
||||
UnitLength::Ft => Ok(measurements::Length::from_meters(1.0).as_feet()),
|
||||
UnitLength::M => Ok(1.0),
|
||||
UnitLength::Cm => Ok(measurements::Length::from_meters(1.0).as_centimeters()),
|
||||
UnitLength::Yd => Ok(measurements::Length::from_meters(1.0).as_yards()),
|
||||
fn inner_m(exec_state: &ExecState) -> Result<f64, KclError> {
|
||||
match exec_state.mod_local.settings.default_length_units {
|
||||
UnitLen::Mm => Ok(measurements::Length::from_meters(1.0).as_millimeters()),
|
||||
UnitLen::Inches => Ok(measurements::Length::from_meters(1.0).as_inches()),
|
||||
UnitLen::Feet => Ok(measurements::Length::from_meters(1.0).as_feet()),
|
||||
UnitLen::M => Ok(1.0),
|
||||
UnitLen::Cm => Ok(measurements::Length::from_meters(1.0).as_centimeters()),
|
||||
UnitLen::Yards => Ok(measurements::Length::from_meters(1.0).as_yards()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Centimeters conversion factor for current projects units.
|
||||
pub async fn cm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_cm(&args)?;
|
||||
pub async fn cm(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_cm(exec_state)?;
|
||||
|
||||
Ok(args.make_user_val_from_f64(result))
|
||||
}
|
||||
@ -207,20 +206,20 @@ pub async fn cm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
||||
name = "cm",
|
||||
tags = ["units"],
|
||||
}]
|
||||
fn inner_cm(args: &Args) -> Result<f64, KclError> {
|
||||
match args.ctx.settings.units {
|
||||
UnitLength::Mm => Ok(measurements::Length::from_centimeters(1.0).as_millimeters()),
|
||||
UnitLength::In => Ok(measurements::Length::from_centimeters(1.0).as_inches()),
|
||||
UnitLength::Ft => Ok(measurements::Length::from_centimeters(1.0).as_feet()),
|
||||
UnitLength::M => Ok(measurements::Length::from_centimeters(1.0).as_meters()),
|
||||
UnitLength::Cm => Ok(1.0),
|
||||
UnitLength::Yd => Ok(measurements::Length::from_centimeters(1.0).as_yards()),
|
||||
fn inner_cm(exec_state: &ExecState) -> Result<f64, KclError> {
|
||||
match exec_state.mod_local.settings.default_length_units {
|
||||
UnitLen::Mm => Ok(measurements::Length::from_centimeters(1.0).as_millimeters()),
|
||||
UnitLen::Inches => Ok(measurements::Length::from_centimeters(1.0).as_inches()),
|
||||
UnitLen::Feet => Ok(measurements::Length::from_centimeters(1.0).as_feet()),
|
||||
UnitLen::M => Ok(measurements::Length::from_centimeters(1.0).as_meters()),
|
||||
UnitLen::Cm => Ok(1.0),
|
||||
UnitLen::Yards => Ok(measurements::Length::from_centimeters(1.0).as_yards()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Yards conversion factor for current projects units.
|
||||
pub async fn yd(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_yd(&args)?;
|
||||
pub async fn yd(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let result = inner_yd(exec_state)?;
|
||||
|
||||
Ok(args.make_user_val_from_f64(result))
|
||||
}
|
||||
@ -249,14 +248,14 @@ pub async fn yd(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
||||
name = "yd",
|
||||
tags = ["units"],
|
||||
}]
|
||||
fn inner_yd(args: &Args) -> Result<f64, KclError> {
|
||||
match args.ctx.settings.units {
|
||||
UnitLength::Mm => Ok(measurements::Length::from_yards(1.0).as_millimeters()),
|
||||
UnitLength::In => Ok(measurements::Length::from_yards(1.0).as_inches()),
|
||||
UnitLength::Ft => Ok(measurements::Length::from_yards(1.0).as_feet()),
|
||||
UnitLength::M => Ok(measurements::Length::from_yards(1.0).as_meters()),
|
||||
UnitLength::Cm => Ok(measurements::Length::from_yards(1.0).as_centimeters()),
|
||||
UnitLength::Yd => Ok(1.0),
|
||||
fn inner_yd(exec_state: &ExecState) -> Result<f64, KclError> {
|
||||
match exec_state.mod_local.settings.default_length_units {
|
||||
UnitLen::Mm => Ok(measurements::Length::from_yards(1.0).as_millimeters()),
|
||||
UnitLen::Inches => Ok(measurements::Length::from_yards(1.0).as_inches()),
|
||||
UnitLen::Feet => Ok(measurements::Length::from_yards(1.0).as_feet()),
|
||||
UnitLen::M => Ok(measurements::Length::from_yards(1.0).as_meters()),
|
||||
UnitLen::Cm => Ok(measurements::Length::from_yards(1.0).as_centimeters()),
|
||||
UnitLen::Yards => Ok(1.0),
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,75 +265,82 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn exec_state_with_len_units(units: UnitLen) -> ExecState {
|
||||
ExecState {
|
||||
mod_local: crate::execution::ModuleState {
|
||||
settings: crate::execution::MetaSettings {
|
||||
default_length_units: units,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_units_inner_mm() {
|
||||
let mut args = Args::new_test_args().await.unwrap();
|
||||
args.ctx.settings.units = UnitLength::Mm;
|
||||
let result = inner_mm(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Mm);
|
||||
let result = inner_mm(&exec_state).unwrap();
|
||||
assert_eq!(result, 1.0);
|
||||
|
||||
args.ctx.settings.units = UnitLength::In;
|
||||
let result = inner_mm(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Inches);
|
||||
let result = inner_mm(&exec_state).unwrap();
|
||||
assert_eq!(result, 1.0 / 25.4);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_units_inner_inch() {
|
||||
let mut args = Args::new_test_args().await.unwrap();
|
||||
args.ctx.settings.units = UnitLength::In;
|
||||
let result = inner_inch(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Inches);
|
||||
let result = inner_inch(&exec_state).unwrap();
|
||||
assert_eq!(result, 1.0);
|
||||
|
||||
args.ctx.settings.units = UnitLength::Mm;
|
||||
let result = inner_inch(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Mm);
|
||||
let result = inner_inch(&exec_state).unwrap();
|
||||
assert_eq!(result, 25.4);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_units_inner_ft() {
|
||||
let mut args = Args::new_test_args().await.unwrap();
|
||||
args.ctx.settings.units = UnitLength::Ft;
|
||||
let result = inner_ft(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Feet);
|
||||
let result = inner_ft(&exec_state).unwrap();
|
||||
assert_eq!(result, 1.0);
|
||||
|
||||
args.ctx.settings.units = UnitLength::Mm;
|
||||
let result = inner_ft(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Mm);
|
||||
let result = inner_ft(&exec_state).unwrap();
|
||||
assert_eq!(result, 304.8);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_units_inner_m() {
|
||||
let mut args = Args::new_test_args().await.unwrap();
|
||||
args.ctx.settings.units = UnitLength::M;
|
||||
let result = inner_m(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::M);
|
||||
let result = inner_m(&exec_state).unwrap();
|
||||
assert_eq!(result, 1.0);
|
||||
|
||||
args.ctx.settings.units = UnitLength::Mm;
|
||||
let result = inner_m(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Mm);
|
||||
let result = inner_m(&exec_state).unwrap();
|
||||
assert_eq!(result, 1000.0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_units_inner_cm() {
|
||||
let mut args = Args::new_test_args().await.unwrap();
|
||||
args.ctx.settings.units = UnitLength::Cm;
|
||||
let result = inner_cm(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Cm);
|
||||
let result = inner_cm(&exec_state).unwrap();
|
||||
assert_eq!(result, 1.0);
|
||||
|
||||
args.ctx.settings.units = UnitLength::Mm;
|
||||
let result = inner_cm(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Mm);
|
||||
let result = inner_cm(&exec_state).unwrap();
|
||||
assert_eq!(result, 10.0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_units_inner_yd() {
|
||||
let mut args = Args::new_test_args().await.unwrap();
|
||||
args.ctx.settings.units = UnitLength::Yd;
|
||||
let result = inner_yd(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Yards);
|
||||
let result = inner_yd(&exec_state).unwrap();
|
||||
assert_eq!(result, 1.0);
|
||||
|
||||
args.ctx.settings.units = UnitLength::Mm;
|
||||
let result = inner_yd(&args).unwrap();
|
||||
let exec_state = exec_state_with_len_units(UnitLen::Mm);
|
||||
let result = inner_yd(&exec_state).unwrap();
|
||||
assert_eq!(result, 914.4);
|
||||
}
|
||||
}
|
||||
|
41
yarn.lock
@ -2422,6 +2422,11 @@
|
||||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/diff@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-6.0.0.tgz#031f27cf57564f3cce825f38fb19fdd4349ad07a"
|
||||
integrity sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==
|
||||
|
||||
"@types/electron@^1.6.10":
|
||||
version "1.6.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/electron/-/electron-1.6.10.tgz#7e87888ed3888767cca68e92772c2c8ea46bc873"
|
||||
@ -4247,6 +4252,11 @@ diff@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
diff@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a"
|
||||
integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==
|
||||
|
||||
dir-compare@^3.0.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-3.3.0.tgz#2c749f973b5c4b5d087f11edaae730db31788416"
|
||||
@ -8533,7 +8543,16 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@ -8627,7 +8646,14 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@ -9484,7 +9510,16 @@ word-wrap@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|