Compare commits
20 Commits
jtran/test
...
derive-doc
Author | SHA1 | Date | |
---|---|---|---|
86ba586318 | |||
cc313afb89 | |||
d0c8311e41 | |||
28311d160a | |||
162856064b | |||
652f82e8c3 | |||
d4d9bf6c7f | |||
0e9d37c0a0 | |||
9c18060d73 | |||
e2be66b024 | |||
1bb372b642 | |||
627fbda671 | |||
74bdb72edc | |||
a9182bf357 | |||
5f14023404 | |||
9a3ac64603 | |||
67e60cb832 | |||
110037df79 | |||
30b1dae38a | |||
f20fc5b467 |
@ -1,5 +1,4 @@
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DEV=false
|
|
||||||
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.zoo.dev
|
VITE_KC_API_BASE_URL=https://api.zoo.dev
|
||||||
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||||
|
1
.github/workflows/e2e-tests.yml
vendored
@ -3,7 +3,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
10885
docs/kcl/std.json
@ -19,6 +19,8 @@ test.describe(
|
|||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
// FIXME: Cannot use scene.waitForExecutionDone() since there is no KCL code
|
||||||
|
await page.waitForTimeout(10000)
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const coord =
|
const coord =
|
||||||
|
@ -10,6 +10,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
|
|||||||
test('Typing KCL errors induces a badge on the code pane button', async ({
|
test('Typing KCL errors induces a badge on the code pane button', async ({
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
|
||||||
@ -30,11 +31,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
|
|||||||
|
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
// wait for execution done
|
|
||||||
await u.openDebugPanel()
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
// Ensure no badge is present
|
// Ensure no badge is present
|
||||||
const codePaneButtonHolder = page.locator('#code-button-holder')
|
const codePaneButtonHolder = page.locator('#code-button-holder')
|
||||||
@ -175,7 +172,9 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
await page.waitForTimeout(1000)
|
// FIXME: await scene.waitForExecutionDone() does not work. It still fails.
|
||||||
|
// I needed to increase this timeout to get this to pass.
|
||||||
|
await page.waitForTimeout(10000)
|
||||||
|
|
||||||
// Ensure badge is present
|
// Ensure badge is present
|
||||||
const codePaneButtonHolder = page.locator('#code-button-holder')
|
const codePaneButtonHolder = page.locator('#code-button-holder')
|
||||||
@ -187,7 +186,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
|
|||||||
// click in the editor to focus it
|
// click in the editor to focus it
|
||||||
await page.locator('.cm-content').click()
|
await page.locator('.cm-content').click()
|
||||||
|
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(2000)
|
||||||
|
|
||||||
// go to the start of the editor and enter more text which will trigger
|
// go to the start of the editor and enter more text which will trigger
|
||||||
// a lint error.
|
// a lint error.
|
||||||
@ -204,8 +203,9 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.keyboard.press('ArrowUp')
|
await page.keyboard.press('ArrowUp')
|
||||||
await page.keyboard.press('Home')
|
await page.keyboard.press('Home')
|
||||||
await page.keyboard.type('foo_bar = 1')
|
await page.keyboard.type('foo_bar = 1')
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(2000)
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
await page.waitForTimeout(2000)
|
||||||
|
|
||||||
// ensure we have a lint error
|
// ensure we have a lint error
|
||||||
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
||||||
|
@ -174,6 +174,9 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
// FIXME: No KCL code, unable to wait for engine execution
|
||||||
|
await page.waitForTimeout(10000)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
|
@ -9,8 +9,8 @@ import fsp from 'fs/promises'
|
|||||||
|
|
||||||
test(
|
test(
|
||||||
'export works on the first try',
|
'export works on the first try',
|
||||||
{ tag: '@electron' },
|
{ tag: ['@electron', '@skipLocalEngine'] },
|
||||||
async ({ page, context }, testInfo) => {
|
async ({ page, context, scene }, testInfo) => {
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
const bracketDir = path.join(dir, 'bracket')
|
const bracketDir = path.join(dir, 'bracket')
|
||||||
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
|
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
|
||||||
@ -118,8 +118,9 @@ test(
|
|||||||
// Close the file pane
|
// Close the file pane
|
||||||
await u.closeFilePanel()
|
await u.closeFilePanel()
|
||||||
|
|
||||||
// wait for it to finish executing (todo: make this more robust)
|
// FIXME: await scene.waitForExecutionDone() does not work. The modeling indicator stays in -receive-reliable and not execution done
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(10000)
|
||||||
|
|
||||||
// expect zero errors in guter
|
// expect zero errors in guter
|
||||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
|
||||||
|
@ -490,6 +490,11 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.keyboard.press('ArrowLeft')
|
await page.keyboard.press('ArrowLeft')
|
||||||
await page.keyboard.press('ArrowRight')
|
await page.keyboard.press('ArrowRight')
|
||||||
|
|
||||||
|
// FIXME: lsp errors do not propagate to the frontend until engine is connected and code is executed
|
||||||
|
// This timeout is to wait for engine connection. LSP and code execution errors should be handled differently
|
||||||
|
// LSP can emit errors as fast as it waits and show them in the editor
|
||||||
|
await page.waitForTimeout(10000)
|
||||||
|
|
||||||
// error in guter
|
// error in guter
|
||||||
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
||||||
|
|
||||||
@ -641,7 +646,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|
|||||||
width = 0.500
|
width = 0.500
|
||||||
height = 0.500
|
height = 0.500
|
||||||
dia = 4
|
dia = 4
|
||||||
|
|
||||||
fn squareHole = (l, w) => {
|
fn squareHole = (l, w) => {
|
||||||
squareHoleSketch = startSketchOn('XY')
|
squareHoleSketch = startSketchOn('XY')
|
||||||
|> startProfileAt([-width / 2, -length / 2], %)
|
|> startProfileAt([-width / 2, -length / 2], %)
|
||||||
@ -714,7 +719,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|
|||||||
|> line(end = [0, -10], tag = $revolveAxis)
|
|> line(end = [0, -10], tag = $revolveAxis)
|
||||||
|> close()
|
|> close()
|
||||||
|> extrude(length = 10)
|
|> extrude(length = 10)
|
||||||
|
|
||||||
sketch001 = startSketchOn(box, revolveAxis)
|
sketch001 = startSketchOn(box, revolveAxis)
|
||||||
|> startProfileAt([5, 10], %)
|
|> startProfileAt([5, 10], %)
|
||||||
|> line(end = [0, -10])
|
|> line(end = [0, -10])
|
||||||
|
@ -112,6 +112,9 @@ export class CmdBarFixture {
|
|||||||
* and assumes we are past the `pickCommand` step.
|
* and assumes we are past the `pickCommand` step.
|
||||||
*/
|
*/
|
||||||
progressCmdBar = async (shouldFuzzProgressMethod = true) => {
|
progressCmdBar = async (shouldFuzzProgressMethod = true) => {
|
||||||
|
// FIXME: Progressing the command bar is a race condition. We have an async useEffect that reports the final state via useCalculateKclExpression. If this does not run quickly enough, it will not "fail" the continue because you can press continue if the state is not ready. E2E tests do not know this.
|
||||||
|
// Wait 1250ms to assume the await executeAst of the KCL input field is finished
|
||||||
|
await this.page.waitForTimeout(1250)
|
||||||
if (shouldFuzzProgressMethod || Math.random() > 0.5) {
|
if (shouldFuzzProgressMethod || Math.random() > 0.5) {
|
||||||
const arrowButton = this.page.getByRole('button', {
|
const arrowButton = this.page.getByRole('button', {
|
||||||
name: 'arrow right Continue',
|
name: 'arrow right Continue',
|
||||||
@ -128,6 +131,23 @@ export class CmdBarFixture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Added data-testid to the command bar buttons
|
||||||
|
// command-bar-continue are the buttons to go to the next step
|
||||||
|
// does not include the submit which is the final button press
|
||||||
|
// aka the right arrow button
|
||||||
|
continue = async () => {
|
||||||
|
const continueButton = this.page.getByTestId('command-bar-continue')
|
||||||
|
await continueButton.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Added data-testid to the command bar buttons
|
||||||
|
// command-bar-submit is the button for the final step to submit
|
||||||
|
// the command bar flow aka the checkmark button.
|
||||||
|
submit = async () => {
|
||||||
|
const submitButton = this.page.getByTestId('command-bar-submit')
|
||||||
|
await submitButton.click()
|
||||||
|
}
|
||||||
|
|
||||||
openCmdBar = async (selectCmd?: 'promptToEdit') => {
|
openCmdBar = async (selectCmd?: 'promptToEdit') => {
|
||||||
// TODO why does this button not work in electron tests?
|
// TODO why does this button not work in electron tests?
|
||||||
// await this.cmdBarOpenBtn.click()
|
// await this.cmdBarOpenBtn.click()
|
||||||
|
@ -29,11 +29,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
|
|||||||
localStorage.setItem('persistCode', file)
|
localStorage.setItem('persistCode', file)
|
||||||
}, file)
|
}, file)
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
|
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
|
||||||
|
|
||||||
await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => {
|
await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => {
|
||||||
await scene.clickNoWhere()
|
// FIXME: Do not click, clicking removes the activeLines in future checks
|
||||||
|
// await scene.clickNoWhere()
|
||||||
await expect(toolbar.extrudeButton).toBeEnabled()
|
await expect(toolbar.extrudeButton).toBeEnabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -199,6 +201,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
|
|||||||
}, file)
|
}, file)
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
|
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
|
||||||
|
|
||||||
@ -422,6 +425,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
|
|||||||
}, file)
|
}, file)
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
|
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
|
||||||
|
|
||||||
@ -712,6 +716,330 @@ openSketch = startSketchOn('XY')
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Shift-click to select and deselect edges and faces`, async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
}) => {
|
||||||
|
// Code samples
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, -6], %)
|
||||||
|
|> line(end = [0, 12])
|
||||||
|
|> line(end = [24, 0])
|
||||||
|
|> line(end = [0, -12])
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
|> extrude(%, length = -12)`
|
||||||
|
|
||||||
|
// Locators
|
||||||
|
const upperEdgeLocation = { x: 600, y: 192 }
|
||||||
|
const lowerEdgeLocation = { x: 600, y: 383 }
|
||||||
|
const faceLocation = { x: 630, y: 290 }
|
||||||
|
|
||||||
|
// Click helpers
|
||||||
|
const [clickOnUpperEdge] = scene.makeMouseHelpers(
|
||||||
|
upperEdgeLocation.x,
|
||||||
|
upperEdgeLocation.y
|
||||||
|
)
|
||||||
|
const [clickOnLowerEdge] = scene.makeMouseHelpers(
|
||||||
|
lowerEdgeLocation.x,
|
||||||
|
lowerEdgeLocation.y
|
||||||
|
)
|
||||||
|
const [clickOnFace] = scene.makeMouseHelpers(faceLocation.x, faceLocation.y)
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
const edgeColorWhite: [number, number, number] = [220, 220, 220] // varies from 192 to 255
|
||||||
|
const edgeColorYellow: [number, number, number] = [251, 251, 40] // vaies from 12 to 67
|
||||||
|
const faceColorGray: [number, number, number] = [168, 168, 168]
|
||||||
|
const faceColorYellow: [number, number, number] = [155, 155, 155]
|
||||||
|
const tolerance = 40
|
||||||
|
const timeout = 150
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
await test.step(`Initial test setup`, async () => {
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
// Wait for the scene and stream to load
|
||||||
|
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Select and deselect a single edge', async () => {
|
||||||
|
await test.step('Click the edge', async () => {
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
upperEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await clickOnUpperEdge()
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorYellow,
|
||||||
|
upperEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step('Shift-click the same edge to deselect', async () => {
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickOnUpperEdge()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
upperEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Select and deselect multiple objects', async () => {
|
||||||
|
await test.step('Select both edges and the face', async () => {
|
||||||
|
await test.step('Select the upper edge', async () => {
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
upperEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await clickOnUpperEdge()
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorYellow,
|
||||||
|
upperEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step('Select the lower edge (Shift-click)', async () => {
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
lowerEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickOnLowerEdge()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorYellow,
|
||||||
|
lowerEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step('Select the face (Shift-click)', async () => {
|
||||||
|
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickOnFace()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(faceColorYellow, faceLocation, tolerance)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await test.step('Deselect them one by one', async () => {
|
||||||
|
await test.step('Deselect the face (Shift-click)', async () => {
|
||||||
|
await scene.expectPixelColor(faceColorYellow, faceLocation, tolerance)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickOnFace()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
|
||||||
|
})
|
||||||
|
await test.step('Deselect the lower edge (Shift-click)', async () => {
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorYellow,
|
||||||
|
lowerEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickOnLowerEdge()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
lowerEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step('Deselect the upper edge (Shift-click)', async () => {
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorYellow,
|
||||||
|
upperEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickOnUpperEdge()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
upperEdgeLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`Shift-click to select and deselect sketch segments`, async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
}) => {
|
||||||
|
// Locators
|
||||||
|
const firstPointLocation = { x: 200, y: 100 }
|
||||||
|
const secondPointLocation = { x: 800, y: 100 }
|
||||||
|
const thirdPointLocation = { x: 800, y: 400 }
|
||||||
|
const fristSegmentLocation = { x: 750, y: 100 }
|
||||||
|
const secondSegmentLocation = { x: 800, y: 150 }
|
||||||
|
const planeLocation = { x: 700, y: 200 }
|
||||||
|
|
||||||
|
// Click helpers
|
||||||
|
const [clickFirstPoint] = scene.makeMouseHelpers(
|
||||||
|
firstPointLocation.x,
|
||||||
|
firstPointLocation.y
|
||||||
|
)
|
||||||
|
const [clickSecondPoint] = scene.makeMouseHelpers(
|
||||||
|
secondPointLocation.x,
|
||||||
|
secondPointLocation.y
|
||||||
|
)
|
||||||
|
const [clickThirdPoint] = scene.makeMouseHelpers(
|
||||||
|
thirdPointLocation.x,
|
||||||
|
thirdPointLocation.y
|
||||||
|
)
|
||||||
|
const [clickFirstSegment] = scene.makeMouseHelpers(
|
||||||
|
fristSegmentLocation.x,
|
||||||
|
fristSegmentLocation.y
|
||||||
|
)
|
||||||
|
const [clickSecondSegment] = scene.makeMouseHelpers(
|
||||||
|
secondSegmentLocation.x,
|
||||||
|
secondSegmentLocation.y
|
||||||
|
)
|
||||||
|
const [clickPlane] = scene.makeMouseHelpers(
|
||||||
|
planeLocation.x,
|
||||||
|
planeLocation.y
|
||||||
|
)
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
const edgeColorWhite: [number, number, number] = [220, 220, 220]
|
||||||
|
const edgeColorBlue: [number, number, number] = [20, 20, 200]
|
||||||
|
const backgroundColor: [number, number, number] = [30, 30, 30]
|
||||||
|
const tolerance = 40
|
||||||
|
const timeout = 150
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
await test.step(`Initial test setup`, async () => {
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
// Wait for the scene and stream to load
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
backgroundColor,
|
||||||
|
secondPointLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Select and deselect a single sketch segment', async () => {
|
||||||
|
await test.step('Get into sketch mode', async () => {
|
||||||
|
await editor.closePane()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickPlane()
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
})
|
||||||
|
await test.step('Draw sketch', async () => {
|
||||||
|
await clickFirstPoint()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickSecondPoint()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickThirdPoint()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
})
|
||||||
|
await test.step('Deselect line tool', async () => {
|
||||||
|
const btnLine = page.getByTestId('line')
|
||||||
|
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
|
||||||
|
if (btnLineAriaPressed === 'true') {
|
||||||
|
await btnLine.click()
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
})
|
||||||
|
await test.step('Select the first segment', async () => {
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickFirstSegment()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorBlue,
|
||||||
|
fristSegmentLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
secondSegmentLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step('Select the second segment (Shift-click)', async () => {
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickSecondSegment()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorBlue,
|
||||||
|
fristSegmentLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorBlue,
|
||||||
|
secondSegmentLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step('Deselect the first segment', async () => {
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickFirstSegment()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
fristSegmentLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorBlue,
|
||||||
|
secondSegmentLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await test.step('Deselect the second segment', async () => {
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await clickSecondSegment()
|
||||||
|
await page.waitForTimeout(timeout)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
fristSegmentLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
secondSegmentLocation,
|
||||||
|
tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test(`Offset plane point-and-click`, async ({
|
test(`Offset plane point-and-click`, async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
@ -727,6 +1055,9 @@ openSketch = startSketchOn('XY')
|
|||||||
const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
|
const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
// FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue.
|
||||||
|
// The engine may not be connected
|
||||||
|
await page.waitForTimeout(15000)
|
||||||
|
|
||||||
await test.step(`Look for the blue of the XZ plane`, async () => {
|
await test.step(`Look for the blue of the XZ plane`, async () => {
|
||||||
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
|
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
|
||||||
@ -952,6 +1283,7 @@ loft001 = loft([sketch001, sketch002])
|
|||||||
}, initialCode)
|
}, initialCode)
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
// One dumb hardcoded screen pixel value
|
// One dumb hardcoded screen pixel value
|
||||||
const testPoint = { x: 575, y: 200 }
|
const testPoint = { x: 575, y: 200 }
|
||||||
@ -1029,7 +1361,7 @@ sketch002 = startSketchOn('XZ')
|
|||||||
testPoint.x - 50,
|
testPoint.x - 50,
|
||||||
testPoint.y
|
testPoint.y
|
||||||
)
|
)
|
||||||
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
|
const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)'
|
||||||
|
|
||||||
await test.step(`Look for sketch001`, async () => {
|
await test.step(`Look for sketch001`, async () => {
|
||||||
await toolbar.closePane('code')
|
await toolbar.closePane('code')
|
||||||
@ -1594,16 +1926,7 @@ extrude001 = extrude(sketch001, length = -12)
|
|||||||
}, initialCode)
|
}, initialCode)
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
// verify modeling scene is loaded
|
|
||||||
await scene.expectPixelColor(
|
|
||||||
backgroundColor,
|
|
||||||
secondEdgeLocation,
|
|
||||||
lowTolerance
|
|
||||||
)
|
|
||||||
|
|
||||||
// wait for stream to load
|
|
||||||
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test 1: Command bar flow with preselected edges
|
// Test 1: Command bar flow with preselected edges
|
||||||
@ -1828,6 +2151,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
|
|||||||
}, initialCode)
|
}, initialCode)
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
// verify modeling scene is loaded
|
// verify modeling scene is loaded
|
||||||
await scene.expectPixelColor(
|
await scene.expectPixelColor(
|
||||||
@ -1950,6 +2274,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
|
|||||||
}, initialCode)
|
}, initialCode)
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
// One dumb hardcoded screen pixel value
|
// One dumb hardcoded screen pixel value
|
||||||
const testPoint = { x: 575, y: 200 }
|
const testPoint = { x: 575, y: 200 }
|
||||||
@ -2048,6 +2373,7 @@ extrude001 = extrude(sketch001, length = 40)
|
|||||||
}, initialCode)
|
}, initialCode)
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
// One dumb hardcoded screen pixel value
|
// One dumb hardcoded screen pixel value
|
||||||
const testPoint = { x: 580, y: 180 }
|
const testPoint = { x: 580, y: 180 }
|
||||||
@ -2221,7 +2547,7 @@ extrude002 = extrude(sketch002, length = 50)
|
|||||||
sketch002 = startSketchOn('XZ')
|
sketch002 = startSketchOn('XZ')
|
||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([0, 0], %)
|
||||||
|> xLine(-2000, %)
|
|> xLine(-2000, %)
|
||||||
sweep001 = sweep({ path = sketch002 }, sketch001)
|
sweep001 = sweep(sketch001, path = sketch002)
|
||||||
`
|
`
|
||||||
await context.addInitScript((initialCode) => {
|
await context.addInitScript((initialCode) => {
|
||||||
localStorage.setItem('persistCode', initialCode)
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
@ -455,7 +455,7 @@ test.describe('Can export from electron app', () => {
|
|||||||
for (const method of exportMethods) {
|
for (const method of exportMethods) {
|
||||||
test(
|
test(
|
||||||
`Can export using ${method}`,
|
`Can export using ${method}`,
|
||||||
{ tag: '@electron' },
|
{ tag: ['@electron', '@skipLocalEngine'] },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ context, page }, testInfo) => {
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
const bracketDir = path.join(dir, 'bracket')
|
const bracketDir = path.join(dir, 'bracket')
|
||||||
|
@ -36,7 +36,7 @@ extrude003 = extrude(sketch003, length = 20)
|
|||||||
`
|
`
|
||||||
|
|
||||||
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||||
test.describe('Check the happy path, for basic changing color', () => {
|
test.fixme('Check the happy path, for basic changing color', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
desc: 'User accepts change',
|
desc: 'User accepts change',
|
||||||
@ -60,6 +60,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
|||||||
localStorage.setItem('persistCode', file)
|
localStorage.setItem('persistCode', file)
|
||||||
}, file)
|
}, file)
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
const body1CapCoords = { x: 571, y: 351 }
|
const body1CapCoords = { x: 571, y: 351 }
|
||||||
const greenCheckCoords = { x: 565, y: 345 }
|
const greenCheckCoords = { x: 565, y: 345 }
|
||||||
@ -76,7 +77,9 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
|||||||
'Submitting to Text-to-CAD API...'
|
'Submitting to Text-to-CAD API...'
|
||||||
)
|
)
|
||||||
const successToast = page.getByText('Prompt to edit successful')
|
const successToast = page.getByText('Prompt to edit successful')
|
||||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
const acceptBtn = page.getByRole('button', {
|
||||||
|
name: 'checkmark Accept',
|
||||||
|
})
|
||||||
const rejectBtn = page.getByRole('button', { name: 'close Reject' })
|
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 test.step('wait for scene to load select body and check selection came through', async () => {
|
||||||
@ -99,14 +102,16 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await expect(submittingToast).toBeVisible()
|
await expect(submittingToast).toBeVisible()
|
||||||
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while
|
await expect(submittingToast).not.toBeVisible({
|
||||||
|
timeout: 2 * 60_000,
|
||||||
|
}) // can take a while
|
||||||
await expect(successToast).toBeVisible()
|
await expect(successToast).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('verify initial change', async () => {
|
await test.step('verify initial change', async () => {
|
||||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||||
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
|
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
|
||||||
await editor.expectEditor.toContain('appearance({')
|
await editor.expectEditor.toContain('appearance(')
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!shouldReject) {
|
if (!shouldReject) {
|
||||||
@ -115,13 +120,13 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
|||||||
await expect(successToast).not.toBeVisible()
|
await expect(successToast).not.toBeVisible()
|
||||||
|
|
||||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||||
await editor.expectEditor.toContain('appearance({')
|
await editor.expectEditor.toContain('appearance(')
|
||||||
|
|
||||||
// ctrl-z works after accepting
|
// ctrl-z works after accepting
|
||||||
await page.keyboard.down('ControlOrMeta')
|
await page.keyboard.down('ControlOrMeta')
|
||||||
await page.keyboard.press('KeyZ')
|
await page.keyboard.press('KeyZ')
|
||||||
await page.keyboard.up('ControlOrMeta')
|
await page.keyboard.up('ControlOrMeta')
|
||||||
await editor.expectEditor.not.toContain('appearance({')
|
await editor.expectEditor.not.toContain('appearance(')
|
||||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -130,7 +135,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
|||||||
await expect(successToast).not.toBeVisible()
|
await expect(successToast).not.toBeVisible()
|
||||||
|
|
||||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
||||||
await editor.expectEditor.not.toContain('appearance({')
|
await editor.expectEditor.not.toContain('appearance(')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -150,6 +155,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
|||||||
localStorage.setItem('persistCode', file)
|
localStorage.setItem('persistCode', file)
|
||||||
}, file)
|
}, file)
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
const body1CapCoords = { x: 571, y: 351 }
|
const body1CapCoords = { x: 571, y: 351 }
|
||||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||||
|
@ -251,7 +251,7 @@ extrude001 = extrude(sketch001, length = 50)
|
|||||||
|> yLineTo(0, %)
|
|> yLineTo(0, %)
|
||||||
|> close()
|
|> close()
|
||||||
|>
|
|>
|
||||||
|
|
||||||
example = extrude(exampleSketch, length = 5)
|
example = extrude(exampleSketch, length = 5)
|
||||||
shell(exampleSketch, faces = ['end'], thickness = 0.25)`
|
shell(exampleSketch, faces = ['end'], thickness = 0.25)`
|
||||||
)
|
)
|
||||||
@ -306,113 +306,113 @@ extrude001 = extrude(sketch001, length = 50)
|
|||||||
|> angledLine({ angle: 50, length: 45 }, %)
|
|> angledLine({ angle: 50, length: 45 }, %)
|
||||||
|> yLineTo(0, %)
|
|> yLineTo(0, %)
|
||||||
|> close()
|
|> close()
|
||||||
|
|
||||||
thing: "blah"`)
|
thing: "blah"`)
|
||||||
|
|
||||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test('when engine fails export we handle the failure and alert the user', async ({
|
test(
|
||||||
scene,
|
'when engine fails export we handle the failure and alert the user',
|
||||||
page,
|
{ tag: '@skipLocalEngine' },
|
||||||
homePage,
|
async ({ scene, page, homePage }) => {
|
||||||
}) => {
|
const u = await getUtils(page)
|
||||||
const u = await getUtils(page)
|
await page.addInitScript(
|
||||||
await page.addInitScript(
|
async ({ code }) => {
|
||||||
async ({ code }) => {
|
localStorage.setItem('persistCode', code)
|
||||||
localStorage.setItem('persistCode', code)
|
;(window as any).playwrightSkipFilePicker = true
|
||||||
;(window as any).playwrightSkipFilePicker = true
|
},
|
||||||
},
|
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
|
||||||
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
|
)
|
||||||
)
|
|
||||||
|
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
await u.waitForPageLoad()
|
await u.waitForPageLoad()
|
||||||
|
|
||||||
// wait for execution done
|
// wait for execution done
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// expect zero errors in guter
|
// expect zero errors in guter
|
||||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
|
||||||
// export the model
|
// export the model
|
||||||
const exportButton = page.getByTestId('export-pane-button')
|
const exportButton = page.getByTestId('export-pane-button')
|
||||||
await expect(exportButton).toBeVisible()
|
await expect(exportButton).toBeVisible()
|
||||||
|
|
||||||
// Click the export button
|
// Click the export button
|
||||||
await exportButton.click()
|
await exportButton.click()
|
||||||
|
|
||||||
// Click the stl.
|
// Click the stl.
|
||||||
const stlOption = page.getByText('glTF')
|
const stlOption = page.getByText('glTF')
|
||||||
await expect(stlOption).toBeVisible()
|
await expect(stlOption).toBeVisible()
|
||||||
|
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
// Click the checkbox
|
// Click the checkbox
|
||||||
const submitButton = page.getByText('Confirm Export')
|
const submitButton = page.getByText('Confirm Export')
|
||||||
await expect(submitButton).toBeVisible()
|
await expect(submitButton).toBeVisible()
|
||||||
|
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
// Find the toast.
|
// Find the toast.
|
||||||
// Look out for the toast message
|
// Look out for the toast message
|
||||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||||
|
|
||||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||||
await expect(engineErrorToastMessage).toBeVisible()
|
await expect(engineErrorToastMessage).toBeVisible()
|
||||||
|
|
||||||
// Make sure the exporting toast is gone
|
// Make sure the exporting toast is gone
|
||||||
await expect(exportingToastMessage).not.toBeVisible()
|
await expect(exportingToastMessage).not.toBeVisible()
|
||||||
|
|
||||||
// Click the code editor
|
// Click the code editor
|
||||||
await page.locator('.cm-content').click()
|
await page.locator('.cm-content').click()
|
||||||
|
|
||||||
await page.waitForTimeout(2000)
|
await page.waitForTimeout(2000)
|
||||||
|
|
||||||
// Expect the toast to be gone
|
// Expect the toast to be gone
|
||||||
await expect(errorToastMessage).not.toBeVisible()
|
await expect(errorToastMessage).not.toBeVisible()
|
||||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||||
|
|
||||||
// Now add in code that works.
|
// Now add in code that works.
|
||||||
await page.locator('.cm-content').fill(bracket)
|
await page.locator('.cm-content').fill(bracket)
|
||||||
await page.keyboard.press('End')
|
await page.keyboard.press('End')
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
await scene.waitForExecutionDone()
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
// Now try exporting
|
// Now try exporting
|
||||||
|
|
||||||
// Click the export button
|
// Click the export button
|
||||||
await exportButton.click()
|
await exportButton.click()
|
||||||
|
|
||||||
// Click the stl.
|
// Click the stl.
|
||||||
await expect(stlOption).toBeVisible()
|
await expect(stlOption).toBeVisible()
|
||||||
|
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
// Click the checkbox
|
// Click the checkbox
|
||||||
await expect(submitButton).toBeVisible()
|
await expect(submitButton).toBeVisible()
|
||||||
|
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
// Find the toast.
|
// Find the toast.
|
||||||
// Look out for the toast message
|
// Look out for the toast message
|
||||||
await expect(exportingToastMessage).toBeVisible()
|
await expect(exportingToastMessage).toBeVisible()
|
||||||
|
|
||||||
// Expect it to succeed.
|
// Expect it to succeed.
|
||||||
await expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 })
|
await expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 })
|
||||||
await expect(errorToastMessage).not.toBeVisible()
|
await expect(errorToastMessage).not.toBeVisible()
|
||||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||||
|
|
||||||
const successToastMessage = page.getByText(`Exported successfully`)
|
const successToastMessage = page.getByText(`Exported successfully`)
|
||||||
await expect(successToastMessage).toBeVisible()
|
await expect(successToastMessage).toBeVisible()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
test(
|
test(
|
||||||
'ensure you can not export while an export is already going',
|
'ensure you can not export while an export is already going',
|
||||||
{ tag: ['@skipLinux', '@skipWin'] },
|
{ tag: ['@skipLinux', '@skipWin'] },
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
PERSIST_MODELING_CONTEXT,
|
PERSIST_MODELING_CONTEXT,
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
import { uuidv4, roundOff } from 'lib/utils'
|
import { uuidv4, roundOff } from 'lib/utils'
|
||||||
|
import { SceneFixture } from './fixtures/sceneFixture'
|
||||||
|
|
||||||
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
||||||
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
|
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
|
||||||
@ -184,7 +185,8 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
|||||||
const doEditSegmentsByDraggingHandle = async (
|
const doEditSegmentsByDraggingHandle = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
homePage: HomePageFixture,
|
homePage: HomePageFixture,
|
||||||
openPanes: string[]
|
openPanes: string[],
|
||||||
|
scene: SceneFixture
|
||||||
) => {
|
) => {
|
||||||
// Load the app with the code panes
|
// Load the app with the code panes
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
@ -200,6 +202,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
|||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
@ -317,7 +320,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test(
|
test(
|
||||||
'code pane open at start-handles',
|
'code pane open at start-handles',
|
||||||
{ tag: ['@skipWin'] },
|
{ tag: ['@skipWin'] },
|
||||||
async ({ page, homePage }) => {
|
async ({ page, homePage, scene }) => {
|
||||||
// Load the app with the code panes
|
// Load the app with the code panes
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
@ -330,14 +333,14 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
|
await doEditSegmentsByDraggingHandle(page, homePage, ['code'], scene)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'code pane closed at start-handles',
|
'code pane closed at start-handles',
|
||||||
{ tag: ['@skipWin'] },
|
{ tag: ['@skipWin'] },
|
||||||
async ({ page, homePage }) => {
|
async ({ page, homePage, scene }) => {
|
||||||
// Load the app with the code panes
|
// Load the app with the code panes
|
||||||
await page.addInitScript(async (persistModelingContext) => {
|
await page.addInitScript(async (persistModelingContext) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
@ -345,7 +348,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
|||||||
JSON.stringify({ openPanes: [] })
|
JSON.stringify({ openPanes: [] })
|
||||||
)
|
)
|
||||||
}, PERSIST_MODELING_CONTEXT)
|
}, PERSIST_MODELING_CONTEXT)
|
||||||
await doEditSegmentsByDraggingHandle(page, homePage, [])
|
await doEditSegmentsByDraggingHandle(page, homePage, [], scene)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -547,6 +550,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
|||||||
test('Can edit a sketch that has been revolved in the same pipe', async ({
|
test('Can edit a sketch that has been revolved in the same pipe', async ({
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
@ -562,6 +566,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
@ -1187,14 +1187,12 @@ sweepSketch = startSketchOn('XY')
|
|||||||
angleStart = 0,
|
angleStart = 0,
|
||||||
radius = 2
|
radius = 2
|
||||||
}, %)
|
}, %)
|
||||||
|> sweep({
|
|> sweep(path = sweepPath)
|
||||||
path = sweepPath,
|
|> appearance(
|
||||||
}, %)
|
|
||||||
|> appearance({
|
|
||||||
color = "#bb00ff",
|
color = "#bb00ff",
|
||||||
metalness = 90,
|
metalness = 90,
|
||||||
roughness = 90
|
roughness = 90
|
||||||
}, %)
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1235,14 +1233,12 @@ sweepSketch = startSketchOn('XY')
|
|||||||
angleStart = 0,
|
angleStart = 0,
|
||||||
radius = 2
|
radius = 2
|
||||||
}, %)
|
}, %)
|
||||||
|> sweep({
|
|> sweep(path = sweepPath)
|
||||||
path = sweepPath,
|
|> appearance(
|
||||||
}, %)
|
|
||||||
|> appearance({
|
|
||||||
color = "#bb00ff",
|
color = "#bb00ff",
|
||||||
metalness = 90,
|
metalness = 90,
|
||||||
roughness = 90
|
roughness = 90
|
||||||
}, %)
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 143 KiB |
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@ -3,199 +3,200 @@ import { test, expect } from './zoo-test'
|
|||||||
import { commonPoints, getUtils } from './test-utils'
|
import { commonPoints, getUtils } from './test-utils'
|
||||||
|
|
||||||
test.describe('Test network and connection issues', () => {
|
test.describe('Test network and connection issues', () => {
|
||||||
test('simulate network down and network little widget', async ({
|
test(
|
||||||
page,
|
'simulate network down and network little widget',
|
||||||
homePage,
|
{ tag: '@skipLocalEngine' },
|
||||||
}) => {
|
async ({ page, homePage }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
const networkToggle = page.getByTestId('network-toggle')
|
const networkToggle = page.getByTestId('network-toggle')
|
||||||
|
|
||||||
// This is how we wait until the stream is online
|
// This is how we wait until the stream is online
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled({ timeout: 15000 })
|
).not.toBeDisabled({ timeout: 15000 })
|
||||||
|
|
||||||
const networkWidget = page.locator('[data-testid="network-toggle"]')
|
const networkWidget = page.locator('[data-testid="network-toggle"]')
|
||||||
await expect(networkWidget).toBeVisible()
|
await expect(networkWidget).toBeVisible()
|
||||||
await networkWidget.hover()
|
await networkWidget.hover()
|
||||||
|
|
||||||
const networkPopover = page.locator('[data-testid="network-popover"]')
|
const networkPopover = page.locator('[data-testid="network-popover"]')
|
||||||
await expect(networkPopover).not.toBeVisible()
|
await expect(networkPopover).not.toBeVisible()
|
||||||
|
|
||||||
// (First check) Expect the network to be up
|
// (First check) Expect the network to be up
|
||||||
await expect(networkToggle).toContainText('Connected')
|
await expect(networkToggle).toContainText('Connected')
|
||||||
|
|
||||||
// Click the network widget
|
// Click the network widget
|
||||||
await networkWidget.click()
|
await networkWidget.click()
|
||||||
|
|
||||||
// Check the modal opened.
|
// Check the modal opened.
|
||||||
await expect(networkPopover).toBeVisible()
|
await expect(networkPopover).toBeVisible()
|
||||||
|
|
||||||
// Click off the modal.
|
// Click off the modal.
|
||||||
await page.mouse.click(100, 100)
|
await page.mouse.click(100, 100)
|
||||||
await expect(networkPopover).not.toBeVisible()
|
await expect(networkPopover).not.toBeVisible()
|
||||||
|
|
||||||
// Turn off the network
|
// Turn off the network
|
||||||
await u.emulateNetworkConditions({
|
await u.emulateNetworkConditions({
|
||||||
offline: true,
|
offline: true,
|
||||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
// values of 0 remove any active throttling. crbug.com/456324#c9
|
||||||
latency: 0,
|
latency: 0,
|
||||||
downloadThroughput: -1,
|
downloadThroughput: -1,
|
||||||
uploadThroughput: -1,
|
uploadThroughput: -1,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Expect the network to be down
|
// Expect the network to be down
|
||||||
await expect(networkToggle).toContainText('Problem')
|
await expect(networkToggle).toContainText('Problem')
|
||||||
|
|
||||||
// Click the network widget
|
// Click the network widget
|
||||||
await networkWidget.click()
|
await networkWidget.click()
|
||||||
|
|
||||||
// Check the modal opened.
|
// Check the modal opened.
|
||||||
await expect(networkPopover).toBeVisible()
|
await expect(networkPopover).toBeVisible()
|
||||||
|
|
||||||
// Click off the modal.
|
// Click off the modal.
|
||||||
await page.mouse.click(0, 0)
|
await page.mouse.click(0, 0)
|
||||||
await expect(networkPopover).not.toBeVisible()
|
await expect(networkPopover).not.toBeVisible()
|
||||||
|
|
||||||
// Turn back on the network
|
// Turn back on the network
|
||||||
await u.emulateNetworkConditions({
|
await u.emulateNetworkConditions({
|
||||||
offline: false,
|
offline: false,
|
||||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
// values of 0 remove any active throttling. crbug.com/456324#c9
|
||||||
latency: 0,
|
latency: 0,
|
||||||
downloadThroughput: -1,
|
downloadThroughput: -1,
|
||||||
uploadThroughput: -1,
|
uploadThroughput: -1,
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled({ timeout: 15000 })
|
).not.toBeDisabled({ timeout: 15000 })
|
||||||
|
|
||||||
// (Second check) expect the network to be up
|
// (Second check) expect the network to be up
|
||||||
await expect(networkToggle).toContainText('Connected')
|
await expect(networkToggle).toContainText('Connected')
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
test('Engine disconnect & reconnect in sketch mode', async ({
|
test(
|
||||||
page,
|
'Engine disconnect & reconnect in sketch mode',
|
||||||
homePage,
|
{ tag: '@skipLocalEngine' },
|
||||||
}) => {
|
async ({ page, homePage }) => {
|
||||||
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
|
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
|
||||||
const networkToggle = page.getByTestId('network-toggle')
|
const networkToggle = page.getByTestId('network-toggle')
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
await u.waitForPageLoad()
|
await u.waitForPageLoad()
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`sketch001 = startSketchOn('XZ')`
|
`sketch001 = startSketchOn('XZ')`
|
||||||
)
|
)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> xLine(${commonPoints.num1}, %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
|
|
||||||
// Expect the network to be up
|
// Expect the network to be up
|
||||||
await expect(networkToggle).toContainText('Connected')
|
await expect(networkToggle).toContainText('Connected')
|
||||||
|
|
||||||
// simulate network down
|
// simulate network down
|
||||||
await u.emulateNetworkConditions({
|
await u.emulateNetworkConditions({
|
||||||
offline: true,
|
offline: true,
|
||||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
// values of 0 remove any active throttling. crbug.com/456324#c9
|
||||||
latency: 0,
|
latency: 0,
|
||||||
downloadThroughput: -1,
|
downloadThroughput: -1,
|
||||||
uploadThroughput: -1,
|
uploadThroughput: -1,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Expect the network to be down
|
// Expect the network to be down
|
||||||
await expect(networkToggle).toContainText('Problem')
|
await expect(networkToggle).toContainText('Problem')
|
||||||
|
|
||||||
// Ensure we are not in sketch mode
|
// Ensure we are not in sketch mode
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
).not.toBeVisible()
|
).not.toBeVisible()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|
||||||
// simulate network up
|
// simulate network up
|
||||||
await u.emulateNetworkConditions({
|
await u.emulateNetworkConditions({
|
||||||
offline: false,
|
offline: false,
|
||||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
// values of 0 remove any active throttling. crbug.com/456324#c9
|
||||||
latency: 0,
|
latency: 0,
|
||||||
downloadThroughput: -1,
|
downloadThroughput: -1,
|
||||||
uploadThroughput: -1,
|
uploadThroughput: -1,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wait for the app to be ready for use
|
// Wait for the app to be ready for use
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled({ timeout: 15000 })
|
).not.toBeDisabled({ timeout: 15000 })
|
||||||
|
|
||||||
// Expect the network to be up
|
// Expect the network to be up
|
||||||
await expect(networkToggle).toContainText('Connected')
|
await expect(networkToggle).toContainText('Connected')
|
||||||
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
|
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
|
||||||
|
|
||||||
// Click off the code pane.
|
// Click off the code pane.
|
||||||
await page.mouse.click(100, 100)
|
await page.mouse.click(100, 100)
|
||||||
|
|
||||||
// select a line
|
// select a line
|
||||||
await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
|
await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||||
'default_camera_get_settings'
|
'default_camera_get_settings'
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(150)
|
await page.waitForTimeout(150)
|
||||||
|
|
||||||
// Click the line tool
|
// Click the line tool
|
||||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||||
|
|
||||||
await page.waitForTimeout(150)
|
await page.waitForTimeout(150)
|
||||||
|
|
||||||
// Ensure we can continue sketching
|
// Ensure we can continue sketching
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect.poll(u.normalisedEditorCode)
|
await expect.poll(u.normalisedEditorCode)
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> xLine(12.34, %)
|
|> xLine(12.34, %)
|
||||||
|> line(end = [-12.34, 12.34])
|
|> line(end = [-12.34, 12.34])
|
||||||
|
|
||||||
`)
|
`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
|
|
||||||
await expect.poll(u.normalisedEditorCode)
|
await expect.poll(u.normalisedEditorCode)
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> xLine(12.34, %)
|
|> xLine(12.34, %)
|
||||||
|> line(end = [-12.34, 12.34])
|
|> line(end = [-12.34, 12.34])
|
||||||
@ -203,20 +204,21 @@ test.describe('Test network and connection issues', () => {
|
|||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// Unequip line tool
|
// Unequip line tool
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
// Make sure we didn't pop out of sketch mode.
|
// Make sure we didn't pop out of sketch mode.
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'line Line', exact: true })
|
page.getByRole('button', { name: 'line Line', exact: true })
|
||||||
).not.toHaveAttribute('aria-pressed', 'true')
|
).not.toHaveAttribute('aria-pressed', 'true')
|
||||||
|
|
||||||
// Exit sketch
|
// Exit sketch
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
).not.toBeVisible()
|
).not.toBeVisible()
|
||||||
})
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
@ -109,7 +109,8 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await page.mouse.move(600, 200)
|
await page.mouse.move(600, 200)
|
||||||
await page.mouse.down({ button: 'right' })
|
await page.mouse.down({ button: 'right' })
|
||||||
await page.mouse.move(700, 200, { steps: 2 })
|
// Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
|
||||||
|
await page.mouse.move(700, 200)
|
||||||
await page.mouse.up({ button: 'right' })
|
await page.mouse.up({ button: 'right' })
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
}, [-19, -85, -85])
|
}, [-19, -85, -85])
|
||||||
|
@ -248,7 +248,11 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Solids should be select and deletable', async ({ page, homePage }) => {
|
test('Solids should be select and deletable', async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
}) => {
|
||||||
test.setTimeout(90_000)
|
test.setTimeout(90_000)
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
@ -320,10 +324,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
await u.openDebugPanel()
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
await u.sendCustomCmd({
|
await u.sendCustomCmd({
|
||||||
@ -902,6 +903,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
test('Testing selections (and hovers) work on sketches when NOT in sketch mode', async ({
|
test('Testing selections (and hovers) work on sketches when NOT in sketch mode', async ({
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
@ -937,6 +939,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
await u.sendCustomCmd({
|
await u.sendCustomCmd({
|
||||||
@ -970,6 +973,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
test("Hovering and selection of extruded faces works, and is not overridden shortly after user's click", async ({
|
test("Hovering and selection of extruded faces works, and is not overridden shortly after user's click", async ({
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
@ -988,6 +992,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
await u.sendCustomCmd({
|
await u.sendCustomCmd({
|
||||||
@ -1021,19 +1026,19 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
|
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
|
||||||
.toBeLessThan(15)
|
.toBeLessThan(15)
|
||||||
await page.mouse.move(nothing.x, nothing.y)
|
await page.mouse.move(nothing.x, nothing.y)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(1000)
|
||||||
await page.mouse.move(extrudeWall.x, extrudeWall.y)
|
await page.mouse.move(extrudeWall.x, extrudeWall.y)
|
||||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||||
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
||||||
removeAfterFirstParenthesis(extrudeText)
|
removeAfterFirstParenthesis(extrudeText)
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(1000)
|
||||||
await expect(
|
await expect(
|
||||||
await u.getGreatestPixDiff(extrudeWall, hoverColor)
|
await u.getGreatestPixDiff(extrudeWall, hoverColor)
|
||||||
).toBeLessThan(15)
|
).toBeLessThan(15)
|
||||||
await page.mouse.click(extrudeWall.x, extrudeWall.y)
|
await page.mouse.click(extrudeWall.x, extrudeWall.y)
|
||||||
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
|
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(1000)
|
||||||
await expect(
|
await expect(
|
||||||
await u.getGreatestPixDiff(extrudeWall, selectColor)
|
await u.getGreatestPixDiff(extrudeWall, selectColor)
|
||||||
).toBeLessThan(15)
|
).toBeLessThan(15)
|
||||||
@ -1044,7 +1049,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
).toBeLessThan(15)
|
).toBeLessThan(15)
|
||||||
|
|
||||||
await page.mouse.move(nothing.x, nothing.y)
|
await page.mouse.move(nothing.x, nothing.y)
|
||||||
await page.waitForTimeout(300)
|
await page.waitForTimeout(1000)
|
||||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||||
|
|
||||||
// because of shading, color is not exact everywhere on the face
|
// because of shading, color is not exact everywhere on the face
|
||||||
@ -1058,11 +1063,11 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
||||||
removeAfterFirstParenthesis(capText)
|
removeAfterFirstParenthesis(capText)
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(1000)
|
||||||
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
|
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
|
||||||
await page.mouse.click(cap.x, cap.y)
|
await page.mouse.click(cap.x, cap.y)
|
||||||
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
|
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(1000)
|
||||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
// check color stays there, i.e. not overridden (this was a bug previously)
|
// check color stays there, i.e. not overridden (this was a bug previously)
|
||||||
@ -1109,7 +1114,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
|> line(end = [4.95, -8])
|
|> line(end = [4.95, -8])
|
||||||
|> line(end = [-20.38, -10.12])
|
|> line(end = [-20.38, -10.12])
|
||||||
|> line(end = [-15.79, 17.08])
|
|> line(end = [-15.79, 17.08])
|
||||||
|
|
||||||
fn yohey = (pos) => {
|
fn yohey = (pos) => {
|
||||||
sketch004 = startSketchOn('XZ')
|
sketch004 = startSketchOn('XZ')
|
||||||
${extrudeAndEditBlockedInFunction}
|
${extrudeAndEditBlockedInFunction}
|
||||||
@ -1119,7 +1124,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
|> line(end = [-15.79, 17.08])
|
|> line(end = [-15.79, 17.08])
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
yohey([15.79, -34.6])
|
yohey([15.79, -34.6])
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
@ -32,15 +32,18 @@ test.fixme('Units menu', async ({ page, homePage }) => {
|
|||||||
await expect(unitsMenuButton).toContainText('mm')
|
await expect(unitsMenuButton).toContainText('mm')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Successful export shows a success toast', async ({ page, homePage }) => {
|
test(
|
||||||
// FYI this test doesn't work with only engine running locally
|
'Successful export shows a success toast',
|
||||||
// And you will need to have the KittyCAD CLI installed
|
{ tag: '@skipLocalEngine' },
|
||||||
const u = await getUtils(page)
|
async ({ page, homePage }) => {
|
||||||
await page.addInitScript(async () => {
|
// FYI this test doesn't work with only engine running locally
|
||||||
;(window as any).playwrightSkipFilePicker = true
|
// And you will need to have the KittyCAD CLI installed
|
||||||
localStorage.setItem(
|
const u = await getUtils(page)
|
||||||
'persistCode',
|
await page.addInitScript(async () => {
|
||||||
`topAng = 25
|
;(window as any).playwrightSkipFilePicker = true
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`topAng = 25
|
||||||
bottomAng = 35
|
bottomAng = 35
|
||||||
baseLen = 3.5
|
baseLen = 3.5
|
||||||
baseHeight = 1
|
baseHeight = 1
|
||||||
@ -78,26 +81,27 @@ part001 = startSketchOn('-XZ')
|
|||||||
|> xLineTo(ZERO, %)
|
|> xLineTo(ZERO, %)
|
||||||
|> close()
|
|> close()
|
||||||
|> extrude(length = 4)`
|
|> extrude(length = 4)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.waitForCmdReceive('extrude')
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await doExport(
|
||||||
|
{
|
||||||
|
type: 'gltf',
|
||||||
|
storage: 'embedded',
|
||||||
|
presentation: 'pretty',
|
||||||
|
},
|
||||||
|
page
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
)
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
|
||||||
await u.openDebugPanel()
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
|
||||||
await u.waitForCmdReceive('extrude')
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
await u.clearAndCloseDebugPanel()
|
|
||||||
|
|
||||||
await doExport(
|
|
||||||
{
|
|
||||||
type: 'gltf',
|
|
||||||
storage: 'embedded',
|
|
||||||
presentation: 'pretty',
|
|
||||||
},
|
|
||||||
page
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Paste should not work unless an input is focused', async ({
|
test('Paste should not work unless an input is focused', async ({
|
||||||
page,
|
page,
|
||||||
@ -444,7 +448,7 @@ test('Delete key does not navigate back', async ({ page, homePage }) => {
|
|||||||
await expect.poll(() => page.url()).not.toContain('/settings')
|
await expect.poll(() => page.url()).not.toContain('/settings')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Sketch on face', async ({ page, homePage }) => {
|
test('Sketch on face', async ({ page, homePage, scene, cmdBar }) => {
|
||||||
test.setTimeout(90_000)
|
test.setTimeout(90_000)
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
@ -470,11 +474,7 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
|
|||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
// wait for execution done
|
|
||||||
await u.openDebugPanel()
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
@ -579,10 +579,9 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
|
|||||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'arrow right Continue' }).click()
|
await cmdBar.progressCmdBar()
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'checkmark Submit command' }).click()
|
await cmdBar.progressCmdBar()
|
||||||
|
|
||||||
const result2 = result.genNext`
|
const result2 = result.genNext`
|
||||||
const sketch002 = extrude(sketch002, length = ${[5, 5]} + 7)`
|
const sketch002 = extrude(sketch002, length = ${[5, 5]} + 7)`
|
||||||
|
@ -32,10 +32,6 @@ win:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
- arm64
|
- arm64
|
||||||
# - target: msi
|
|
||||||
# arch:
|
|
||||||
# - x64
|
|
||||||
# - arm64
|
|
||||||
signingHashAlgorithms:
|
signingHashAlgorithms:
|
||||||
- sha256
|
- sha256
|
||||||
sign: "./scripts/sign-win.js"
|
sign: "./scripts/sign-win.js"
|
||||||
@ -47,15 +43,12 @@ win:
|
|||||||
mimeType: text/vnd.zoo.kcl
|
mimeType: text/vnd.zoo.kcl
|
||||||
description: Zoo KCL File
|
description: Zoo KCL File
|
||||||
role: Editor
|
role: Editor
|
||||||
# msi:
|
|
||||||
# oneClick: false
|
|
||||||
# perMachine: true
|
|
||||||
nsis:
|
nsis:
|
||||||
oneClick: false
|
oneClick: false
|
||||||
perMachine: true
|
perMachine: true
|
||||||
allowElevation: true
|
allowElevation: true
|
||||||
installerIcon: "assets/icon.ico"
|
installerIcon: "assets/icon.ico"
|
||||||
include: "./installer.nsh"
|
include: "./scripts/installer.nsh"
|
||||||
linux:
|
linux:
|
||||||
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
|
||||||
target:
|
target:
|
||||||
|
@ -85,7 +85,7 @@
|
|||||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
|
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
|
||||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||||
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-shell/manifest.json",
|
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-appearance/manifest.json",
|
||||||
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
|
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
|
||||||
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||||
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||||
@ -120,6 +120,7 @@
|
|||||||
"test:playwright:electron:windows:local": "yarn tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
"test:playwright:electron:windows:local": "yarn tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||||
"test:playwright:electron:macos:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
"test:playwright:electron:macos:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||||
"test:playwright:electron:ubuntu:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
"test:playwright:electron:ubuntu:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
||||||
|
"test:playwright:electron:ubuntu:engine:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot|@skipLocalEngine'",
|
||||||
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
|
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
|
||||||
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
|
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
|
||||||
},
|
},
|
||||||
@ -200,7 +201,7 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.19.1",
|
"typescript-eslint": "^8.23.0",
|
||||||
"vite": "^5.4.12",
|
"vite": "^5.4.12",
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"vscode-uri": "^3.0.8"
|
"vscode-uri": "^3.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.6",
|
"@types/node": "^22.13.1",
|
||||||
"ts-node": "^10.9.2"
|
"ts-node": "^10.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,10 +109,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||||
|
|
||||||
"@types/node@^22.10.6":
|
"@types/node@^22.13.1":
|
||||||
version "22.10.6"
|
version "22.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.6.tgz#5c6795e71635876039f853cbccd59f523d9e4239"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
|
||||||
integrity sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==
|
integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~6.20.0"
|
undici-types "~6.20.0"
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ echo "$PACKAGE" > package.json
|
|||||||
# electron-builder.yml
|
# electron-builder.yml
|
||||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
|
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
|
||||||
yq -i '.appId = "dev.zoo.modeling-app-nightly"' electron-builder.yml
|
yq -i '.appId = "dev.zoo.modeling-app-nightly"' electron-builder.yml
|
||||||
|
yq -i '.nsis.include = "./scripts/installer-nightly.nsh"' electron-builder.yml
|
||||||
|
|
||||||
# Release notes
|
# Release notes
|
||||||
echo "Nightly build $VERSION (commit $COMMIT)" > release-notes.md
|
echo "Nightly build $VERSION (commit $COMMIT)" > release-notes.md
|
||||||
|
8
scripts/installer-nightly.nsh
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
!macro preInit
|
||||||
|
SetRegView 64
|
||||||
|
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
|
||||||
|
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
|
||||||
|
SetRegView 32
|
||||||
|
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
|
||||||
|
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
|
||||||
|
!macroend
|
@ -196,6 +196,7 @@ function ReviewingButton() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
form="review-form"
|
form="review-form"
|
||||||
className="w-fit !p-0 rounded-sm hover:shadow"
|
className="w-fit !p-0 rounded-sm hover:shadow"
|
||||||
|
data-testid="command-bar-submit"
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'checkmark',
|
icon: 'checkmark',
|
||||||
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
|
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
|
||||||
@ -214,6 +215,7 @@ function GatheringArgsButton() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
form="arg-form"
|
form="arg-form"
|
||||||
className="w-fit !p-0 rounded-sm hover:shadow"
|
className="w-fit !p-0 rounded-sm hover:shadow"
|
||||||
|
data-testid="command-bar-continue"
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'arrowRight',
|
icon: 'arrowRight',
|
||||||
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
|
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
|
||||||
|
@ -20,6 +20,7 @@ import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
|||||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
const machineContextSelector = (snapshot?: {
|
const machineContextSelector = (snapshot?: {
|
||||||
context: Record<string, unknown>
|
context: Record<string, unknown>
|
||||||
@ -97,6 +98,7 @@ function CommandBarKclInput({
|
|||||||
value,
|
value,
|
||||||
initialVariableName,
|
initialVariableName,
|
||||||
})
|
})
|
||||||
|
|
||||||
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||||
label: v.key,
|
label: v.key,
|
||||||
detail: String(roundOff(v.value as number)),
|
detail: String(roundOff(v.value as number)),
|
||||||
@ -170,7 +172,15 @@ function CommandBarKclInput({
|
|||||||
|
|
||||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||||
e?.preventDefault()
|
e?.preventDefault()
|
||||||
if (!canSubmit || valueNode === null) return
|
if (!canSubmit || valueNode === null) {
|
||||||
|
// Gotcha: Our application can attempt to submit a command value before the command bar kcl input is ready. Notify the scene and user.
|
||||||
|
if (!canSubmit) {
|
||||||
|
toast.error('Unable to submit command')
|
||||||
|
} else if (valueNode === null) {
|
||||||
|
toast.error('Unable to submit undefined command value')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit(
|
onSubmit(
|
||||||
createNewVariable
|
createNewVariable
|
||||||
|
@ -2,11 +2,15 @@ import { Dialog } from '@headlessui/react'
|
|||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
|
||||||
|
|
||||||
const DownloadAppBanner = () => {
|
const DownloadAppBanner = () => {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const [isBannerDismissed, setIsBannerDismissed] = useState(
|
const [isBannerDismissed, setIsBannerDismissed] = useState(
|
||||||
settings.context.app.dismissWebBanner.current
|
settings.context.app.dismissWebBanner.current || hasCreateFileParam
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -329,11 +329,83 @@ export const ModelingMachineProvider = ({
|
|||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
}
|
}
|
||||||
} else if (setSelections.selection && editorManager.isShiftDown) {
|
} else if (setSelections.selection && editorManager.isShiftDown) {
|
||||||
|
// selecting and deselecting multiple objects
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are two scenarios:
|
||||||
|
* 1. General case:
|
||||||
|
* When selecting and deselecting edges,
|
||||||
|
* faces or segment (during sketch edit)
|
||||||
|
* we use its artifact ID to identify the selection
|
||||||
|
* 2. Initial sketch setup:
|
||||||
|
* The artifact is not yet created
|
||||||
|
* so we use the codeRef.range
|
||||||
|
*/
|
||||||
|
|
||||||
|
let updatedSelections: typeof selectionRanges.graphSelections
|
||||||
|
|
||||||
|
// 1. General case: Artifact exists, use its ID
|
||||||
|
if (setSelections.selection.artifact?.id) {
|
||||||
|
// check if already selected
|
||||||
|
const alreadySelected = selectionRanges.graphSelections.some(
|
||||||
|
(selection) =>
|
||||||
|
selection.artifact?.id ===
|
||||||
|
setSelections.selection?.artifact?.id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
alreadySelected &&
|
||||||
|
setSelections.selection?.artifact?.id
|
||||||
|
) {
|
||||||
|
// remove it
|
||||||
|
updatedSelections = selectionRanges.graphSelections.filter(
|
||||||
|
(selection) =>
|
||||||
|
selection.artifact?.id !==
|
||||||
|
setSelections.selection?.artifact?.id
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// add it
|
||||||
|
updatedSelections = [
|
||||||
|
...selectionRanges.graphSelections,
|
||||||
|
setSelections.selection,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2. Initial sketch setup: Artifact not yet created – use codeRef.range
|
||||||
|
const selectionRange = JSON.stringify(
|
||||||
|
setSelections.selection?.codeRef?.range
|
||||||
|
)
|
||||||
|
|
||||||
|
// check if already selected
|
||||||
|
const alreadySelected = selectionRanges.graphSelections.some(
|
||||||
|
(selection) => {
|
||||||
|
const existingRange = JSON.stringify(
|
||||||
|
selection.codeRef?.range
|
||||||
|
)
|
||||||
|
return existingRange === selectionRange
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
alreadySelected &&
|
||||||
|
setSelections.selection?.codeRef?.range
|
||||||
|
) {
|
||||||
|
// remove it
|
||||||
|
updatedSelections = selectionRanges.graphSelections.filter(
|
||||||
|
(selection) =>
|
||||||
|
JSON.stringify(selection.codeRef?.range) !==
|
||||||
|
selectionRange
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// add it
|
||||||
|
updatedSelections = [
|
||||||
|
...selectionRanges.graphSelections,
|
||||||
|
setSelections.selection,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selections = {
|
selections = {
|
||||||
graphSelections: [
|
graphSelections: updatedSelections,
|
||||||
...selectionRanges.graphSelections,
|
|
||||||
setSelections.selection,
|
|
||||||
],
|
|
||||||
otherSelections: selectionRanges.otherSelections,
|
otherSelections: selectionRanges.otherSelections,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ function ProjectMenuPopover({
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
const { settings } = useSettingsAuthContext()
|
useSettingsAuthContext()
|
||||||
const token = useToken()
|
const token = useToken()
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const commands = useSelector(commandBarActor, commandsSelector)
|
const commands = useSelector(commandBarActor, commandsSelector)
|
||||||
@ -193,14 +193,13 @@ function ProjectMenuPopover({
|
|||||||
{
|
{
|
||||||
id: 'share-link',
|
id: 'share-link',
|
||||||
Element: 'button',
|
Element: 'button',
|
||||||
children: 'Share link to file',
|
children: 'Share current part (via Zoo link)',
|
||||||
disabled: IS_NIGHTLY_OR_DEBUG || !findCommand(shareCommandInfo),
|
disabled: !(IS_NIGHTLY_OR_DEBUG && findCommand(shareCommandInfo)),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await copyFileShareLink({
|
await copyFileShareLink({
|
||||||
token: token ?? '',
|
token: token ?? '',
|
||||||
code: codeManager.code,
|
code: codeManager.code,
|
||||||
name: project?.name || '',
|
name: project?.name || '',
|
||||||
units: settings.context.modeling.defaultUnit.current,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -263,7 +262,7 @@ function ProjectMenuPopover({
|
|||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<Popover.Panel
|
<Popover.Panel
|
||||||
className={`z-10 absolute top-full left-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
|
className={`z-10 absolute top-full left-0 mt-1 pb-1 w-52 bg-chalkboard-10 dark:bg-chalkboard-90
|
||||||
border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
|
border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
|
||||||
shadow-lg`}
|
shadow-lg`}
|
||||||
>
|
>
|
||||||
|
@ -30,15 +30,7 @@ import {
|
|||||||
FILE_EXT,
|
FILE_EXT,
|
||||||
PROJECT_ENTRYPOINT,
|
PROJECT_ENTRYPOINT,
|
||||||
} from 'lib/constants'
|
} from 'lib/constants'
|
||||||
import { DeepPartial } from 'lib/types'
|
import { codeManager, kclManager } from 'lib/singletons'
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
|
||||||
import { codeManager } from 'lib/singletons'
|
|
||||||
import {
|
|
||||||
loadAndValidateSettings,
|
|
||||||
projectConfigurationToSettingsPayload,
|
|
||||||
saveSettings,
|
|
||||||
setSettingsAtLevel,
|
|
||||||
} from 'lib/settings/settingsUtils'
|
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
@ -86,7 +78,7 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
|||||||
setSearchParams(searchParams)
|
setSearchParams(searchParams)
|
||||||
}, [searchParams, setSearchParams])
|
}, [searchParams, setSearchParams])
|
||||||
const {
|
const {
|
||||||
settings: { context: settings, send: settingsSend },
|
settings: { context: settings },
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
|
|
||||||
const [state, send, actor] = useMachine(
|
const [state, send, actor] = useMachine(
|
||||||
@ -132,17 +124,10 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
|||||||
clearImportSearchParams()
|
clearImportSearchParams()
|
||||||
codeManager.updateCodeStateEditor(input.code || '')
|
codeManager.updateCodeStateEditor(input.code || '')
|
||||||
await codeManager.writeToFile()
|
await codeManager.writeToFile()
|
||||||
|
await kclManager.executeCode(true)
|
||||||
settingsSend({
|
|
||||||
type: 'set.modeling.defaultUnit',
|
|
||||||
data: {
|
|
||||||
level: 'project',
|
|
||||||
value: input.units,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: 'File and units overwritten successfully',
|
message: 'File overwritten successfully',
|
||||||
fileName: input.name,
|
fileName: input.name,
|
||||||
projectName: '',
|
projectName: '',
|
||||||
}
|
}
|
||||||
@ -392,16 +377,6 @@ const ProjectsContextDesktop = ({
|
|||||||
? input.name
|
? input.name
|
||||||
: input.name + FILE_EXT
|
: input.name + FILE_EXT
|
||||||
let message = 'File created successfully'
|
let message = 'File created successfully'
|
||||||
const unitsConfiguration: DeepPartial<Configuration> = {
|
|
||||||
settings: {
|
|
||||||
project: {
|
|
||||||
directory: settings.app.projectDirectory.current,
|
|
||||||
},
|
|
||||||
modeling: {
|
|
||||||
base_unit: input.units,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
|
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
|
||||||
if (needsInterpolated) {
|
if (needsInterpolated) {
|
||||||
@ -414,28 +389,10 @@ const ProjectsContextDesktop = ({
|
|||||||
|
|
||||||
// Create the project around the file if newProject
|
// Create the project around the file if newProject
|
||||||
if (input.method === 'newProject') {
|
if (input.method === 'newProject') {
|
||||||
await createNewProjectDirectory(
|
await createNewProjectDirectory(projectName, input.code)
|
||||||
projectName,
|
|
||||||
input.code,
|
|
||||||
unitsConfiguration
|
|
||||||
)
|
|
||||||
message = `Project "${projectName}" created successfully with link contents`
|
message = `Project "${projectName}" created successfully with link contents`
|
||||||
} else {
|
} else {
|
||||||
let projectPath = window.electron.join(
|
|
||||||
settings.app.projectDirectory.current,
|
|
||||||
projectName
|
|
||||||
)
|
|
||||||
|
|
||||||
message = `File "${fileName}" created successfully`
|
message = `File "${fileName}" created successfully`
|
||||||
const existingConfiguration = await loadAndValidateSettings(
|
|
||||||
projectPath
|
|
||||||
)
|
|
||||||
const settingsToSave = setSettingsAtLevel(
|
|
||||||
existingConfiguration.settings,
|
|
||||||
'project',
|
|
||||||
projectConfigurationToSettingsPayload(unitsConfiguration)
|
|
||||||
)
|
|
||||||
await saveSettings(settingsToSave, projectPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the file
|
// Create the file
|
||||||
|
@ -302,7 +302,7 @@ export const Stream = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const path = getArtifactOfTypes(
|
const path = getArtifactOfTypes(
|
||||||
{ key: entity_id, types: ['path', 'solid2d', 'segment'] },
|
{ key: entity_id, types: ['path', 'solid2d', 'segment', 'helix'] },
|
||||||
engineCommandManager.artifactGraph
|
engineCommandManager.artifactGraph
|
||||||
)
|
)
|
||||||
if (err(path)) {
|
if (err(path)) {
|
||||||
|
@ -25,6 +25,18 @@ export class KclPlugin implements PluginValue {
|
|||||||
|
|
||||||
constructor(client: LanguageServerClient) {
|
constructor(client: LanguageServerClient) {
|
||||||
this.client = client
|
this.client = client
|
||||||
|
|
||||||
|
// Gotcha: Code can be written into the CodeMirror editor but not propagated to codeManager.code
|
||||||
|
// because the update function has not run. We need to initialize the codeManager.code when lsp initializes
|
||||||
|
// because new code could have been written into the editor before the update callback is initialized.
|
||||||
|
// There appears to be limited ways to safely get the current doc content. This appears to be sync and safe.
|
||||||
|
const kclLspPlugin = this.client.plugins.find((plugin) => {
|
||||||
|
return plugin.client.name === 'kcl'
|
||||||
|
})
|
||||||
|
if (kclLspPlugin) {
|
||||||
|
// @ts-ignore Ignoring this private dereference of .view on the plugin. I do not have another helper method that can give me doc string
|
||||||
|
codeManager.code = kclLspPlugin.view.state.doc.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a doc update needs to be sent to the server, this holds the
|
// When a doc update needs to be sent to the server, this holds the
|
||||||
|
@ -6,7 +6,6 @@ import { useSettingsAuthContext } from './useSettingsAuthContext'
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { FileLinkParams } from 'lib/links'
|
import { FileLinkParams } from 'lib/links'
|
||||||
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
|
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||||
import { baseUnitsUnion } from 'lib/settings/settingsTypes'
|
|
||||||
|
|
||||||
// For initializing the command arguments, we actually want `method` to be undefined
|
// For initializing the command arguments, we actually want `method` to be undefined
|
||||||
// so that we don't skip it in the command palette.
|
// so that we don't skip it in the command palette.
|
||||||
@ -37,13 +36,7 @@ export function useCreateFileLinkQuery(
|
|||||||
code: base64ToString(
|
code: base64ToString(
|
||||||
decodeURIComponent(searchParams.get('code') ?? '')
|
decodeURIComponent(searchParams.get('code') ?? '')
|
||||||
),
|
),
|
||||||
|
|
||||||
name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
|
name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
|
||||||
|
|
||||||
units:
|
|
||||||
(baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
|
|
||||||
settings.context.modeling.defaultUnit.default) ??
|
|
||||||
settings.context.modeling.defaultUnit.current,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const argDefaultValues: CreateFileSchemaMethodOptional = {
|
const argDefaultValues: CreateFileSchemaMethodOptional = {
|
||||||
@ -55,7 +48,6 @@ export function useCreateFileLinkQuery(
|
|||||||
? settings.context.projects.defaultProjectName.current
|
? settings.context.projects.defaultProjectName.current
|
||||||
: DEFAULT_FILE_NAME,
|
: DEFAULT_FILE_NAME,
|
||||||
code: params.code || '',
|
code: params.code || '',
|
||||||
units: params.units,
|
|
||||||
method: isDesktop() ? undefined : 'existingProject',
|
method: isDesktop() ? undefined : 'existingProject',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ export class KclManager {
|
|||||||
nonCodeNodes: {},
|
nonCodeNodes: {},
|
||||||
startNodes: [],
|
startNodes: [],
|
||||||
},
|
},
|
||||||
|
trivia: [],
|
||||||
}
|
}
|
||||||
private _execState: ExecState = emptyExecState()
|
private _execState: ExecState = emptyExecState()
|
||||||
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
||||||
@ -239,6 +240,7 @@ export class KclManager {
|
|||||||
nonCodeNodes: {},
|
nonCodeNodes: {},
|
||||||
startNodes: [],
|
startNodes: [],
|
||||||
},
|
},
|
||||||
|
trivia: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ child_process.spawnSync('git', [
|
|||||||
'clone',
|
'clone',
|
||||||
'--single-branch',
|
'--single-branch',
|
||||||
'--branch',
|
'--branch',
|
||||||
'achalmers/kw-shell',
|
'achalmers/kw-appearance',
|
||||||
URL_GIT_KCL_SAMPLES,
|
URL_GIT_KCL_SAMPLES,
|
||||||
DIR_KCL_SAMPLES,
|
DIR_KCL_SAMPLES,
|
||||||
])
|
])
|
||||||
|
@ -128,15 +128,78 @@ describe('Testing findUniqueName', () => {
|
|||||||
it('should find a unique name', () => {
|
it('should find a unique name', () => {
|
||||||
const result = findUniqueName(
|
const result = findUniqueName(
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
{ type: 'Identifier', name: 'yo01', start: 0, end: 0, moduleId: 0 },
|
{
|
||||||
{ type: 'Identifier', name: 'yo02', start: 0, end: 0, moduleId: 0 },
|
type: 'Identifier',
|
||||||
{ type: 'Identifier', name: 'yo03', start: 0, end: 0, moduleId: 0 },
|
name: 'yo01',
|
||||||
{ type: 'Identifier', name: 'yo04', start: 0, end: 0, moduleId: 0 },
|
start: 0,
|
||||||
{ type: 'Identifier', name: 'yo05', start: 0, end: 0, moduleId: 0 },
|
end: 0,
|
||||||
{ type: 'Identifier', name: 'yo06', start: 0, end: 0, moduleId: 0 },
|
moduleId: 0,
|
||||||
{ type: 'Identifier', name: 'yo07', start: 0, end: 0, moduleId: 0 },
|
trivia: [],
|
||||||
{ type: 'Identifier', name: 'yo08', start: 0, end: 0, moduleId: 0 },
|
},
|
||||||
{ type: 'Identifier', name: 'yo09', start: 0, end: 0, moduleId: 0 },
|
{
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'yo02',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'yo03',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'yo04',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'yo05',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'yo06',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'yo07',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'yo08',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'yo09',
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
},
|
||||||
] satisfies Node<Identifier>[]),
|
] satisfies Node<Identifier>[]),
|
||||||
'yo',
|
'yo',
|
||||||
2
|
2
|
||||||
@ -154,6 +217,7 @@ describe('Testing addSketchTo', () => {
|
|||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
nonCodeMeta: { nonCodeNodes: {}, startNodes: [] },
|
nonCodeMeta: { nonCodeNodes: {}, startNodes: [] },
|
||||||
|
trivia: [],
|
||||||
},
|
},
|
||||||
'yz'
|
'yz'
|
||||||
)
|
)
|
||||||
|
@ -278,6 +278,7 @@ export function mutateObjExpProp(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -431,10 +432,11 @@ export function addSweep(
|
|||||||
} {
|
} {
|
||||||
const modifiedAst = structuredClone(node)
|
const modifiedAst = structuredClone(node)
|
||||||
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
|
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
|
||||||
const sweep = createCallExpressionStdLib('sweep', [
|
const sweep = createCallExpressionStdLibKw(
|
||||||
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
|
'sweep',
|
||||||
createIdentifier(profileDeclarator.id.name),
|
createIdentifier(profileDeclarator.id.name),
|
||||||
])
|
[createLabeledArg('path', createIdentifier(pathDeclarator.id.name))]
|
||||||
|
)
|
||||||
const declaration = createVariableDeclaration(name, sweep)
|
const declaration = createVariableDeclaration(name, sweep)
|
||||||
modifiedAst.body.push(declaration)
|
modifiedAst.body.push(declaration)
|
||||||
const pathToNode: PathToNode = [
|
const pathToNode: PathToNode = [
|
||||||
@ -442,8 +444,9 @@ export function addSweep(
|
|||||||
[modifiedAst.body.length - 1, 'index'],
|
[modifiedAst.body.length - 1, 'index'],
|
||||||
['declaration', 'VariableDeclaration'],
|
['declaration', 'VariableDeclaration'],
|
||||||
['init', 'VariableDeclarator'],
|
['init', 'VariableDeclarator'],
|
||||||
['arguments', 'CallExpression'],
|
['arguments', 'CallExpressionKw'],
|
||||||
[0, 'index'],
|
[0, ARG_INDEX_FIELD],
|
||||||
|
['arg', LABELED_ARG_FIELD],
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -888,6 +891,7 @@ export function createLiteral(value: LiteralValue | number): Node<Literal> {
|
|||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
value,
|
value,
|
||||||
raw,
|
raw,
|
||||||
|
trivia: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -897,6 +901,7 @@ export function createTagDeclarator(value: string): Node<TagDeclarator> {
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
value,
|
value,
|
||||||
}
|
}
|
||||||
@ -908,6 +913,7 @@ export function createIdentifier(name: string): Node<Identifier> {
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
name,
|
name,
|
||||||
}
|
}
|
||||||
@ -919,6 +925,7 @@ export function createPipeSubstitution(): Node<PipeSubstitution> {
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -931,11 +938,13 @@ export function createCallExpressionStdLib(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
callee: {
|
callee: {
|
||||||
type: 'Identifier',
|
type: 'Identifier',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
@ -953,11 +962,13 @@ export function createCallExpressionStdLibKw(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
callee: {
|
callee: {
|
||||||
type: 'Identifier',
|
type: 'Identifier',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
@ -975,11 +986,13 @@ export function createCallExpression(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
callee: {
|
callee: {
|
||||||
type: 'Identifier',
|
type: 'Identifier',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
@ -995,6 +1008,7 @@ export function createArrayExpression(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
nonCodeMeta: nonCodeMetaEmpty(),
|
nonCodeMeta: nonCodeMetaEmpty(),
|
||||||
elements,
|
elements,
|
||||||
@ -1009,6 +1023,7 @@ export function createPipeExpression(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
body,
|
body,
|
||||||
nonCodeMeta: nonCodeMetaEmpty(),
|
nonCodeMeta: nonCodeMetaEmpty(),
|
||||||
@ -1026,12 +1041,14 @@ export function createVariableDeclaration(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
declaration: {
|
declaration: {
|
||||||
type: 'VariableDeclarator',
|
type: 'VariableDeclarator',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
id: createIdentifier(varName),
|
id: createIdentifier(varName),
|
||||||
init,
|
init,
|
||||||
@ -1049,6 +1066,7 @@ export function createObjectExpression(properties: {
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
nonCodeMeta: nonCodeMetaEmpty(),
|
nonCodeMeta: nonCodeMetaEmpty(),
|
||||||
properties: Object.entries(properties).map(([key, value]) => ({
|
properties: Object.entries(properties).map(([key, value]) => ({
|
||||||
@ -1056,6 +1074,7 @@ export function createObjectExpression(properties: {
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
key: createIdentifier(key),
|
key: createIdentifier(key),
|
||||||
|
|
||||||
value,
|
value,
|
||||||
@ -1072,6 +1091,7 @@ export function createUnaryExpression(
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
operator,
|
operator,
|
||||||
argument,
|
argument,
|
||||||
@ -1088,6 +1108,7 @@ export function createBinaryExpression([left, operator, right]: [
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
moduleId: 0,
|
moduleId: 0,
|
||||||
|
trivia: [],
|
||||||
|
|
||||||
operator,
|
operator,
|
||||||
left,
|
left,
|
||||||
@ -1373,6 +1394,7 @@ export async function deleteFromSelection(
|
|||||||
varDec.node.init.type === 'PipeExpression') ||
|
varDec.node.init.type === 'PipeExpression') ||
|
||||||
selection.artifact?.type === 'sweep' ||
|
selection.artifact?.type === 'sweep' ||
|
||||||
selection.artifact?.type === 'plane' ||
|
selection.artifact?.type === 'plane' ||
|
||||||
|
selection.artifact?.type === 'helix' ||
|
||||||
!selection.artifact // aka expected to be a shell at this point
|
!selection.artifact // aka expected to be a shell at this point
|
||||||
) {
|
) {
|
||||||
let extrudeNameToDelete = ''
|
let extrudeNameToDelete = ''
|
||||||
@ -1380,7 +1402,8 @@ export async function deleteFromSelection(
|
|||||||
if (
|
if (
|
||||||
selection.artifact &&
|
selection.artifact &&
|
||||||
selection.artifact.type !== 'sweep' &&
|
selection.artifact.type !== 'sweep' &&
|
||||||
selection.artifact.type !== 'plane'
|
selection.artifact.type !== 'plane' &&
|
||||||
|
selection.artifact.type !== 'helix'
|
||||||
) {
|
) {
|
||||||
const varDecName = varDec.node.id.name
|
const varDecName = varDec.node.id.name
|
||||||
traverse(astClone, {
|
traverse(astClone, {
|
||||||
@ -1419,13 +1442,17 @@ export async function deleteFromSelection(
|
|||||||
if (!pathToNode) return new Error('Could not find extrude variable')
|
if (!pathToNode) return new Error('Could not find extrude variable')
|
||||||
} else {
|
} else {
|
||||||
pathToNode = selection.codeRef.pathToNode
|
pathToNode = selection.codeRef.pathToNode
|
||||||
const extrudeVarDec = getNodeFromPath<VariableDeclarator>(
|
if (varDec.node.type !== 'VariableDeclarator') {
|
||||||
astClone,
|
const callExp = getNodeFromPath<CallExpression>(
|
||||||
pathToNode,
|
astClone,
|
||||||
'VariableDeclarator'
|
pathToNode,
|
||||||
)
|
'CallExpression'
|
||||||
if (err(extrudeVarDec)) return extrudeVarDec
|
)
|
||||||
extrudeNameToDelete = extrudeVarDec.node.id.name
|
if (err(callExp)) return callExp
|
||||||
|
extrudeNameToDelete = callExp.node.callee.name
|
||||||
|
} else {
|
||||||
|
extrudeNameToDelete = varDec.node.id.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const expressionIndex = pathToNode[1][0] as number
|
const expressionIndex = pathToNode[1][0] as number
|
||||||
|
@ -46,6 +46,7 @@ export function revolveSketch(
|
|||||||
if (err(sketchNode)) return sketchNode
|
if (err(sketchNode)) return sketchNode
|
||||||
|
|
||||||
let generatedAxis
|
let generatedAxis
|
||||||
|
let axisDeclaration: PathToNode | null = null
|
||||||
|
|
||||||
if (axisOrEdge === 'Edge') {
|
if (axisOrEdge === 'Edge') {
|
||||||
const pathToAxisSelection = getNodePathFromSourceRange(
|
const pathToAxisSelection = getNodePathFromSourceRange(
|
||||||
@ -70,6 +71,13 @@ export function revolveSketch(
|
|||||||
const axisSelection = edge?.graphSelections[0]?.artifact
|
const axisSelection = edge?.graphSelections[0]?.artifact
|
||||||
if (!axisSelection) return new Error('Generated axis selection is missing.')
|
if (!axisSelection) return new Error('Generated axis selection is missing.')
|
||||||
generatedAxis = getEdgeTagCall(tag, axisSelection)
|
generatedAxis = getEdgeTagCall(tag, axisSelection)
|
||||||
|
if (
|
||||||
|
axisSelection.type === 'segment' ||
|
||||||
|
axisSelection.type === 'path' ||
|
||||||
|
axisSelection.type === 'edgeCut'
|
||||||
|
) {
|
||||||
|
axisDeclaration = axisSelection.codeRef.pathToNode
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
generatedAxis = createLiteral(axis)
|
generatedAxis = createLiteral(axis)
|
||||||
}
|
}
|
||||||
@ -139,18 +147,34 @@ export function revolveSketch(
|
|||||||
const sketchIndexInPathToNode =
|
const sketchIndexInPathToNode =
|
||||||
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
||||||
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
|
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
|
||||||
if (typeof sketchIndexInBody !== 'number')
|
let insertIndex = sketchIndexInBody
|
||||||
return new Error('expected sketchIndexInBody to be a number')
|
|
||||||
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
if (typeof insertIndex !== 'number')
|
||||||
|
return new Error('expected insertIndex to be a number')
|
||||||
|
|
||||||
|
// If an axis was selected in KCL, find the max index to insert the revolve command
|
||||||
|
if (axisDeclaration) {
|
||||||
|
const axisIndexInPathToNode =
|
||||||
|
axisDeclaration.findIndex((a) => a[0] === 'body') + 1
|
||||||
|
const axisIndex = axisDeclaration[axisIndexInPathToNode][0]
|
||||||
|
|
||||||
|
if (typeof axisIndex !== 'number')
|
||||||
|
return new Error('expected axisIndex to be a number')
|
||||||
|
|
||||||
|
insertIndex = Math.max(insertIndex, axisIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
clonedAst.body.splice(insertIndex + 1, 0, VariableDeclaration)
|
||||||
|
|
||||||
const pathToRevolveArg: PathToNode = [
|
const pathToRevolveArg: PathToNode = [
|
||||||
['body', ''],
|
['body', ''],
|
||||||
[sketchIndexInBody + 1, 'index'],
|
[insertIndex + 1, 'index'],
|
||||||
['declaration', 'VariableDeclaration'],
|
['declaration', 'VariableDeclaration'],
|
||||||
['init', 'VariableDeclarator'],
|
['init', 'VariableDeclarator'],
|
||||||
['arguments', 'CallExpression'],
|
['arguments', 'CallExpression'],
|
||||||
[0, 'index'],
|
[0, 'index'],
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modifiedAst: clonedAst,
|
modifiedAst: clonedAst,
|
||||||
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
|
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
|
||||||
|
@ -1954,6 +1954,7 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
|
|||||||
startNodes: [],
|
startNodes: [],
|
||||||
nonCodeNodes: [],
|
nonCodeNodes: [],
|
||||||
},
|
},
|
||||||
|
trivia: [],
|
||||||
},
|
},
|
||||||
pathToNode,
|
pathToNode,
|
||||||
}
|
}
|
||||||
@ -2534,6 +2535,8 @@ function addTagKw(): addTagFn {
|
|||||||
...primaryCallExp,
|
...primaryCallExp,
|
||||||
start: callExpr.node.start,
|
start: callExpr.node.start,
|
||||||
end: callExpr.node.end,
|
end: callExpr.node.end,
|
||||||
|
moduleId: callExpr.node.moduleId,
|
||||||
|
trivia: callExpr.node.trivia,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
|
||||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
|
||||||
import { projectsMachine } from 'machines/projectsMachine'
|
import { projectsMachine } from 'machines/projectsMachine'
|
||||||
|
|
||||||
export type ProjectsCommandSchema = {
|
export type ProjectsCommandSchema = {
|
||||||
@ -23,7 +21,6 @@ export type ProjectsCommandSchema = {
|
|||||||
'Import file from URL': {
|
'Import file from URL': {
|
||||||
name: string
|
name: string
|
||||||
code?: string
|
code?: string
|
||||||
units: UnitLength_type
|
|
||||||
method: 'newProject' | 'existingProject'
|
method: 'newProject' | 'existingProject'
|
||||||
projectName?: string
|
projectName?: string
|
||||||
}
|
}
|
||||||
@ -157,15 +154,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
|
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
units: {
|
|
||||||
inputType: 'options',
|
|
||||||
required: false,
|
|
||||||
skip: true,
|
|
||||||
options: baseUnitsUnion.map((unit) => ({
|
|
||||||
name: baseUnitLabels[unit],
|
|
||||||
value: unit,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
reviewMessage(commandBarContext) {
|
reviewMessage(commandBarContext) {
|
||||||
return isDesktop()
|
return isDesktop()
|
||||||
|
@ -136,7 +136,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'share-file-link',
|
name: 'share-file-link',
|
||||||
displayName: 'Share file',
|
displayName: 'Share current part (via Zoo link)',
|
||||||
hide: IS_NIGHTLY_OR_DEBUG ? undefined : 'desktop',
|
hide: IS_NIGHTLY_OR_DEBUG ? undefined : 'desktop',
|
||||||
description: 'Create a link that contains a copy of the current file.',
|
description: 'Create a link that contains a copy of the current file.',
|
||||||
groupId: 'code',
|
groupId: 'code',
|
||||||
@ -147,7 +147,6 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
|||||||
token: commandProps.authToken,
|
token: commandProps.authToken,
|
||||||
code: codeManager.code,
|
code: codeManager.code,
|
||||||
name: commandProps.projectData.project?.name || '',
|
name: commandProps.projectData.project?.name || '',
|
||||||
units: commandProps.settings.defaultUnit,
|
|
||||||
}).catch(reportRejection)
|
}).catch(reportRejection)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -5,13 +5,12 @@ describe(`link creation tests`, () => {
|
|||||||
test(`createCreateFileUrl happy path`, async () => {
|
test(`createCreateFileUrl happy path`, async () => {
|
||||||
const code = `extrusionDistance = 12`
|
const code = `extrusionDistance = 12`
|
||||||
const name = `test`
|
const name = `test`
|
||||||
const units = `mm`
|
|
||||||
|
|
||||||
// Converted with external online tools
|
// Converted with external online tools
|
||||||
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
|
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
|
||||||
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
|
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&code=${expectedEncodedCode}&ask-open-desktop=true`
|
||||||
|
|
||||||
const result = createCreateFileUrl({ code, name, units })
|
const result = createCreateFileUrl({ code, name })
|
||||||
expect(result.toString()).toBe(expectedLink)
|
expect(result.toString()).toBe(expectedLink)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
|
||||||
import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM } from './constants'
|
import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM } from './constants'
|
||||||
import { stringToBase64 } from './base64'
|
import { stringToBase64 } from './base64'
|
||||||
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from 'env'
|
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from 'env'
|
||||||
@ -7,7 +6,6 @@ import { err } from './trap'
|
|||||||
export interface FileLinkParams {
|
export interface FileLinkParams {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
units: UnitLength_type
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyFileShareLink(
|
export async function copyFileShareLink(
|
||||||
@ -46,12 +44,11 @@ export async function copyFileShareLink(
|
|||||||
* With the additional step of asking the user if they want to
|
* With the additional step of asking the user if they want to
|
||||||
* open the URL in the desktop app.
|
* open the URL in the desktop app.
|
||||||
*/
|
*/
|
||||||
export function createCreateFileUrl({ code, name, units }: FileLinkParams) {
|
export function createCreateFileUrl({ code, name }: FileLinkParams) {
|
||||||
let origin = VITE_KC_SITE_APP_URL
|
let origin = VITE_KC_SITE_APP_URL
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
[CREATE_FILE_URL_PARAM]: String(true),
|
[CREATE_FILE_URL_PARAM]: String(true),
|
||||||
name,
|
name,
|
||||||
units,
|
|
||||||
code: stringToBase64(code),
|
code: stringToBase64(code),
|
||||||
[ASK_TO_OPEN_QUERY_PARAM]: String(true),
|
[ASK_TO_OPEN_QUERY_PARAM]: String(true),
|
||||||
})
|
})
|
||||||
|
@ -9,6 +9,8 @@ import {
|
|||||||
getCalculatedKclExpressionValue,
|
getCalculatedKclExpressionValue,
|
||||||
programMemoryFromVariables,
|
programMemoryFromVariables,
|
||||||
} from './kclHelpers'
|
} from './kclHelpers'
|
||||||
|
import { parse, resultIsOk } from 'lang/wasm'
|
||||||
|
import { err } from 'lib/trap'
|
||||||
|
|
||||||
const isValidVariableName = (name: string) =>
|
const isValidVariableName = (name: string) =>
|
||||||
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
||||||
@ -50,7 +52,20 @@ export function useCalculateKclExpression({
|
|||||||
bodyPath: [],
|
bodyPath: [],
|
||||||
})
|
})
|
||||||
const [valueNode, setValueNode] = useState<Expr | null>(null)
|
const [valueNode, setValueNode] = useState<Expr | null>(null)
|
||||||
const [calcResult, setCalcResult] = useState('NAN')
|
// Gotcha: If we do not attempt to parse numeric literals instantly it means that there is an async action to verify
|
||||||
|
// the value is good. This means all E2E tests have a race condition on when they can hit "next" in the command bar.
|
||||||
|
// Most scenarios automatically pass a numeric literal. We can try to parse that first, otherwise make it go through the slow
|
||||||
|
// async method.
|
||||||
|
// If we pass in numeric literals, we should instantly parse them, they have nothing to do with application memory
|
||||||
|
const _code_value = `const __result__ = ${value}`
|
||||||
|
const codeValueParseResult = parse(_code_value)
|
||||||
|
let isValueParsable = true
|
||||||
|
if (err(codeValueParseResult) || !resultIsOk(codeValueParseResult)) {
|
||||||
|
isValueParsable = false
|
||||||
|
}
|
||||||
|
const initialCalcResult: number | string =
|
||||||
|
Number.isNaN(Number(value)) || !isValueParsable ? 'NAN' : value
|
||||||
|
const [calcResult, setCalcResult] = useState(initialCalcResult)
|
||||||
const [newVariableName, setNewVariableName] = useState('')
|
const [newVariableName, setNewVariableName] = useState('')
|
||||||
const [isNewVariableNameUnique, setIsNewVariableNameUnique] = useState(true)
|
const [isNewVariableNameUnique, setIsNewVariableNameUnique] = useState(true)
|
||||||
|
|
||||||
|
@ -306,7 +306,6 @@ export const projectsMachine = setup({
|
|||||||
return {
|
return {
|
||||||
code: '',
|
code: '',
|
||||||
name: '',
|
name: '',
|
||||||
units: 'mm',
|
|
||||||
method: 'existingProject',
|
method: 'existingProject',
|
||||||
projects: context.projects,
|
projects: context.projects,
|
||||||
}
|
}
|
||||||
@ -314,7 +313,6 @@ export const projectsMachine = setup({
|
|||||||
return {
|
return {
|
||||||
code: event.data.code || '',
|
code: event.data.code || '',
|
||||||
name: event.data.name,
|
name: event.data.name,
|
||||||
units: event.data.units,
|
|
||||||
method: event.data.method,
|
method: event.data.method,
|
||||||
projectName: event.data.projectName,
|
projectName: event.data.projectName,
|
||||||
projects: context.projects,
|
projects: context.projects,
|
||||||
|
14
src/main.ts
@ -40,7 +40,6 @@ dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
|
|||||||
|
|
||||||
// default vite values based on mode
|
// default vite values based on mode
|
||||||
process.env.NODE_ENV ??= viteEnv.MODE
|
process.env.NODE_ENV ??= viteEnv.MODE
|
||||||
process.env.DEV ??= viteEnv.DEV + ''
|
|
||||||
process.env.BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
|
process.env.BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
|
||||||
process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL
|
process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL
|
||||||
process.env.VITE_KC_API_BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
|
process.env.VITE_KC_API_BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
|
||||||
@ -94,12 +93,11 @@ const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deep Link: Case of a cold start from Windows or Linux
|
// Deep Link: Case of a cold start from Windows or Linux
|
||||||
if (
|
const zooProtocolArg = process.argv.find((a) =>
|
||||||
!pathToOpen &&
|
a.startsWith(ZOO_STUDIO_PROTOCOL + '://')
|
||||||
process.argv.length > 1 &&
|
)
|
||||||
process.argv[1].indexOf(ZOO_STUDIO_PROTOCOL + '://') > -1
|
if (!pathToOpen && zooProtocolArg) {
|
||||||
) {
|
pathToOpen = zooProtocolArg
|
||||||
pathToOpen = process.argv[1]
|
|
||||||
console.log('Retrieved deep link from argv', pathToOpen)
|
console.log('Retrieved deep link from argv', pathToOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,6 +327,7 @@ ipcMain.handle('kittycad', (event, data) => {
|
|||||||
)(data.args)
|
)(data.args)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Used to find other devices on the local network, e.g. 3D printers, CNC machines, etc.
|
||||||
ipcMain.handle('find_machine_api', () => {
|
ipcMain.handle('find_machine_api', () => {
|
||||||
const timeoutAfterMs = 5000
|
const timeoutAfterMs = 5000
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -339,7 +338,6 @@ ipcMain.handle('find_machine_api', () => {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
resolve(null)
|
resolve(null)
|
||||||
})
|
})
|
||||||
console.log('Looking for machine API...')
|
|
||||||
bonjourEt.find(
|
bonjourEt.find(
|
||||||
{ protocol: 'tcp', type: 'machine-api' },
|
{ protocol: 'tcp', type: 'machine-api' },
|
||||||
(service: Service) => {
|
(service: Service) => {
|
||||||
|
@ -32,7 +32,8 @@ export const PACKAGE_NAME = isDesktop()
|
|||||||
|
|
||||||
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
|
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
|
||||||
|
|
||||||
export const IS_NIGHTLY_OR_DEBUG = IS_NIGHTLY || APP_VERSION === '0.0.0'
|
export const IS_NIGHTLY_OR_DEBUG =
|
||||||
|
IS_NIGHTLY || APP_VERSION === '0.0.0' || APP_VERSION === '11.22.33'
|
||||||
|
|
||||||
export function getReleaseUrl(version: string = APP_VERSION) {
|
export function getReleaseUrl(version: string = APP_VERSION) {
|
||||||
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
|
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
|
||||||
|
8
src/wasm-lib/Cargo.lock
generated
@ -730,7 +730,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
version = "0.1.35"
|
version = "0.1.36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -1712,7 +1712,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.2.34"
|
version = "0.2.35"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx 0.5.1",
|
"approx 0.5.1",
|
||||||
@ -3273,9 +3273,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.137"
|
version = "1.0.138"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
|
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.7.1",
|
"indexmap 2.7.1",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
@ -15,7 +15,7 @@ gloo-utils = "0.2.0"
|
|||||||
kcl-lib = { path = "kcl" }
|
kcl-lib = { path = "kcl" }
|
||||||
kittycad.workspace = true
|
kittycad.workspace = true
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
serde_json = "1.0.135"
|
serde_json = "1.0.138"
|
||||||
tokio = { version = "1.41.1", features = ["sync"] }
|
tokio = { version = "1.41.1", features = ["sync"] }
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
description = "A tool for generating documentation from Rust derive macros"
|
description = "A tool for generating documentation from Rust derive macros"
|
||||||
version = "0.1.35"
|
version = "0.1.36"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -22,10 +22,15 @@ copy-exec-test-into-sim-test test_name:
|
|||||||
zoo kcl fmt -w kcl/tests/{{test_name}}/input.kcl
|
zoo kcl fmt -w kcl/tests/{{test_name}}/input.kcl
|
||||||
just new-sim-test {{test_name}}
|
just new-sim-test {{test_name}}
|
||||||
|
|
||||||
# Create a new KCL deterministic simulation test case.
|
# Create a new, empty KCL deterministic simulation test case.
|
||||||
new-sim-test test_name render_to_png="true":
|
new-sim-test test_name render_to_png="true":
|
||||||
|
mkdir kcl/tests/{{test_name}}
|
||||||
|
touch kcl/tests/{{test_name}}/input.kcl
|
||||||
# Add the various tests for this new test case.
|
# Add the various tests for this new test case.
|
||||||
cat kcl/tests/simtest.tmpl | sed "s/TEST_NAME_HERE/{{test_name}}/" | sed "s/RENDER_TO_PNG/{{render_to_png}}/" >> kcl/src/simulation_tests.rs
|
cat kcl/tests/simtest.tmpl | sed "s/TEST_NAME_HERE/{{test_name}}/" | sed "s/RENDER_TO_PNG/{{render_to_png}}/" >> kcl/src/simulation_tests.rs
|
||||||
|
|
||||||
|
# Run a KCL deterministic simulation test case and accept output.
|
||||||
|
run-sim-test test_name:
|
||||||
# Run all the tests for the first time, in the right order.
|
# Run all the tests for the first time, in the right order.
|
||||||
{{cita}} -p kcl-lib -- simulation_tests::{{test_name}}::parse
|
{{cita}} -p kcl-lib -- simulation_tests::{{test_name}}::parse
|
||||||
{{cita}} -p kcl-lib -- simulation_tests::{{test_name}}::unparse
|
{{cita}} -p kcl-lib -- simulation_tests::{{test_name}}::unparse
|
||||||
|
@ -11,7 +11,7 @@ hyper = { version = "0.14.29", features = ["http1", "server", "tcp"] }
|
|||||||
kcl-lib = { version = "0.2", path = "../kcl" }
|
kcl-lib = { version = "0.2", path = "../kcl" }
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
serde_json = "1.0.135"
|
serde_json = "1.0.138"
|
||||||
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
description = "KittyCAD Language implementation and tools"
|
description = "KittyCAD Language implementation and tools"
|
||||||
version = "0.2.34"
|
version = "0.2.35"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
@ -56,7 +56,7 @@ schemars = { version = "0.8.17", features = [
|
|||||||
"preserve_order",
|
"preserve_order",
|
||||||
] }
|
] }
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
serde_json = "1.0.135"
|
serde_json = "1.0.138"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
tabled = { version = "0.15.0", optional = true }
|
tabled = { version = "0.15.0", optional = true }
|
||||||
thiserror = "2.0.0"
|
thiserror = "2.0.0"
|
||||||
|
@ -118,7 +118,7 @@ impl StdLibFnArg {
|
|||||||
} else if self.type_ == "KclValue" && self.required {
|
} else if self.type_ == "KclValue" && self.required {
|
||||||
return Ok(Some((index, format!("{label}${{{}:{}}}", index, "3"))));
|
return Ok(Some((index, format!("{label}${{{}:{}}}", index, "3"))));
|
||||||
}
|
}
|
||||||
self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn)
|
self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn, &self.name)
|
||||||
.map(|maybe| maybe.map(|(index, snippet)| (index, format!("{label}{snippet}"))))
|
.map(|maybe| maybe.map(|(index, snippet)| (index, format!("{label}{snippet}"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +136,7 @@ impl StdLibFnArg {
|
|||||||
schema: &schemars::schema::Schema,
|
schema: &schemars::schema::Schema,
|
||||||
index: usize,
|
index: usize,
|
||||||
in_keyword_fn: bool,
|
in_keyword_fn: bool,
|
||||||
|
name: &str,
|
||||||
) -> Result<Option<(usize, String)>> {
|
) -> Result<Option<(usize, String)>> {
|
||||||
match schema {
|
match schema {
|
||||||
schemars::schema::Schema::Object(o) => {
|
schemars::schema::Schema::Object(o) => {
|
||||||
@ -149,6 +150,10 @@ impl StdLibFnArg {
|
|||||||
return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000"))));
|
return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if name == "color" {
|
||||||
|
let snippet = format!("${{{}:\"#ff0000\"}}", index);
|
||||||
|
return Ok(Some((index, snippet)));
|
||||||
|
}
|
||||||
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
|
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
|
||||||
if (!in_keyword_fn && *nullable) || (in_keyword_fn && !self.include_in_snippet) {
|
if (!in_keyword_fn && *nullable) || (in_keyword_fn && !self.include_in_snippet) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@ -192,13 +197,9 @@ impl StdLibFnArg {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if prop_name == "color" {
|
if let Some((new_index, snippet)) =
|
||||||
fn_docs.push_str(&format!("\t{} = ${{{}:\"#ff0000\"}},\n", prop_name, i));
|
self.get_autocomplete_snippet_from_schema(prop, i, false, name)?
|
||||||
i += 1;
|
{
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((new_index, snippet)) = self.get_autocomplete_snippet_from_schema(prop, i, false)? {
|
|
||||||
fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet));
|
fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet));
|
||||||
i = new_index + 1;
|
i = new_index + 1;
|
||||||
}
|
}
|
||||||
@ -223,7 +224,8 @@ impl StdLibFnArg {
|
|||||||
.get_autocomplete_snippet_from_schema(
|
.get_autocomplete_snippet_from_schema(
|
||||||
items,
|
items,
|
||||||
index + (v as usize),
|
index + (v as usize),
|
||||||
in_keyword_fn
|
in_keyword_fn,
|
||||||
|
name
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -238,7 +240,7 @@ impl StdLibFnArg {
|
|||||||
index,
|
index,
|
||||||
format!(
|
format!(
|
||||||
"[{}]",
|
"[{}]",
|
||||||
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn)?
|
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)?
|
||||||
.ok_or_else(|| anyhow::anyhow!("expected snippet"))?
|
.ok_or_else(|| anyhow::anyhow!("expected snippet"))?
|
||||||
.1
|
.1
|
||||||
),
|
),
|
||||||
@ -250,7 +252,7 @@ impl StdLibFnArg {
|
|||||||
index,
|
index,
|
||||||
format!(
|
format!(
|
||||||
"[{}]",
|
"[{}]",
|
||||||
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn)?
|
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)?
|
||||||
.ok_or_else(|| anyhow::anyhow!("expected snippet"))?
|
.ok_or_else(|| anyhow::anyhow!("expected snippet"))?
|
||||||
.1
|
.1
|
||||||
),
|
),
|
||||||
@ -293,7 +295,7 @@ impl StdLibFnArg {
|
|||||||
return Ok(Some((index, parsed_enum_values[0].to_string())));
|
return Ok(Some((index, parsed_enum_values[0].to_string())));
|
||||||
} else if let Some(item) = items.iter().next() {
|
} else if let Some(item) = items.iter().next() {
|
||||||
if let Some((new_index, snippet)) =
|
if let Some((new_index, snippet)) =
|
||||||
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn)?
|
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)?
|
||||||
{
|
{
|
||||||
i = new_index + 1;
|
i = new_index + 1;
|
||||||
fn_docs.push_str(&snippet);
|
fn_docs.push_str(&snippet);
|
||||||
@ -302,7 +304,7 @@ impl StdLibFnArg {
|
|||||||
} else if let Some(items) = &subschemas.any_of {
|
} else if let Some(items) = &subschemas.any_of {
|
||||||
if let Some(item) = items.iter().next() {
|
if let Some(item) = items.iter().next() {
|
||||||
if let Some((new_index, snippet)) =
|
if let Some((new_index, snippet)) =
|
||||||
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn)?
|
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)?
|
||||||
{
|
{
|
||||||
i = new_index + 1;
|
i = new_index + 1;
|
||||||
fn_docs.push_str(&snippet);
|
fn_docs.push_str(&snippet);
|
||||||
@ -1018,12 +1020,7 @@ mod tests {
|
|||||||
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
|
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snippet,
|
snippet,
|
||||||
r#"appearance({
|
r#"appearance(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})${}"#
|
||||||
color = ${0:"#
|
|
||||||
.to_owned()
|
|
||||||
+ "\"#"
|
|
||||||
+ r#"ff0000"},
|
|
||||||
}, ${1:%})${}"#
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1038,12 +1035,7 @@ mod tests {
|
|||||||
fn get_autocomplete_snippet_sweep() {
|
fn get_autocomplete_snippet_sweep() {
|
||||||
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
|
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
|
||||||
let snippet = sweep_fn.to_autocomplete_snippet().unwrap();
|
let snippet = sweep_fn.to_autocomplete_snippet().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(snippet, r#"sweep(${0:%}, path = ${1:sketch000})${}"#);
|
||||||
snippet,
|
|
||||||
r#"sweep({
|
|
||||||
path = ${0:sketch000},
|
|
||||||
}, ${1:%})${}"#
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -5,7 +5,8 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
|||||||
use crate::{
|
use crate::{
|
||||||
execution::{ArtifactCommand, ArtifactGraph, Operation},
|
execution::{ArtifactCommand, ArtifactGraph, Operation},
|
||||||
lsp::IntoDiagnostic,
|
lsp::IntoDiagnostic,
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::SourceRange,
|
||||||
|
ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// How did the KCL execution fail
|
/// How did the KCL execution fail
|
||||||
|
@ -10,6 +10,7 @@ use crate::{
|
|||||||
pub(crate) const SETTINGS: &str = "settings";
|
pub(crate) const SETTINGS: &str = "settings";
|
||||||
pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
|
pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
|
||||||
pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
|
pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
|
||||||
|
pub(super) const NO_PRELUDE: &str = "no_prelude";
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub(super) enum AnnotationScope {
|
pub(super) enum AnnotationScope {
|
||||||
@ -23,7 +24,7 @@ pub(super) fn expect_properties<'a>(
|
|||||||
) -> Result<&'a [Node<ObjectProperty>], KclError> {
|
) -> Result<&'a [Node<ObjectProperty>], KclError> {
|
||||||
match annotation {
|
match annotation {
|
||||||
NonCodeValue::Annotation { name, properties } => {
|
NonCodeValue::Annotation { name, properties } => {
|
||||||
assert_eq!(name.name, for_key);
|
assert_eq!(name.as_ref().unwrap().name, for_key);
|
||||||
Ok(&**properties.as_ref().ok_or_else(|| {
|
Ok(&**properties.as_ref().ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("Empty `{for_key}` annotation"),
|
message: format!("Empty `{for_key}` annotation"),
|
||||||
|
@ -269,6 +269,17 @@ pub struct EdgeCutEdge {
|
|||||||
pub surface_id: ArtifactId,
|
pub surface_id: ArtifactId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||||
|
#[ts(export_to = "Artifact.ts")]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Helix {
|
||||||
|
pub id: ArtifactId,
|
||||||
|
/// The axis of the helix. Currently this is always an edge ID, but we may
|
||||||
|
/// add axes to the graph.
|
||||||
|
pub axis_id: Option<ArtifactId>,
|
||||||
|
pub code_ref: CodeRef,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||||
#[ts(export_to = "Artifact.ts")]
|
#[ts(export_to = "Artifact.ts")]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
@ -295,6 +306,7 @@ pub enum Artifact {
|
|||||||
SweepEdge(SweepEdge),
|
SweepEdge(SweepEdge),
|
||||||
EdgeCut(EdgeCut),
|
EdgeCut(EdgeCut),
|
||||||
EdgeCutEdge(EdgeCutEdge),
|
EdgeCutEdge(EdgeCutEdge),
|
||||||
|
Helix(Helix),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Artifact {
|
impl Artifact {
|
||||||
@ -312,6 +324,7 @@ impl Artifact {
|
|||||||
Artifact::SweepEdge(a) => a.id,
|
Artifact::SweepEdge(a) => a.id,
|
||||||
Artifact::EdgeCut(a) => a.id,
|
Artifact::EdgeCut(a) => a.id,
|
||||||
Artifact::EdgeCutEdge(a) => a.id,
|
Artifact::EdgeCutEdge(a) => a.id,
|
||||||
|
Artifact::Helix(a) => a.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,6 +344,7 @@ impl Artifact {
|
|||||||
Artifact::SweepEdge(_) => None,
|
Artifact::SweepEdge(_) => None,
|
||||||
Artifact::EdgeCut(a) => Some(&a.code_ref),
|
Artifact::EdgeCut(a) => Some(&a.code_ref),
|
||||||
Artifact::EdgeCutEdge(_) => None,
|
Artifact::EdgeCutEdge(_) => None,
|
||||||
|
Artifact::Helix(a) => Some(&a.code_ref),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,6 +364,7 @@ impl Artifact {
|
|||||||
Artifact::SweepEdge(_) => Some(new),
|
Artifact::SweepEdge(_) => Some(new),
|
||||||
Artifact::EdgeCut(a) => a.merge(new),
|
Artifact::EdgeCut(a) => a.merge(new),
|
||||||
Artifact::EdgeCutEdge(_) => Some(new),
|
Artifact::EdgeCutEdge(_) => Some(new),
|
||||||
|
Artifact::Helix(_) => Some(new),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -924,6 +939,25 @@ fn artifacts_to_update(
|
|||||||
}
|
}
|
||||||
return Ok(return_arr);
|
return Ok(return_arr);
|
||||||
}
|
}
|
||||||
|
ModelingCmd::EntityMakeHelixFromParams(_) => {
|
||||||
|
let return_arr = vec![Artifact::Helix(Helix {
|
||||||
|
id,
|
||||||
|
axis_id: None,
|
||||||
|
code_ref: CodeRef { range, path_to_node },
|
||||||
|
})];
|
||||||
|
return Ok(return_arr);
|
||||||
|
}
|
||||||
|
ModelingCmd::EntityMakeHelixFromEdge(helix) => {
|
||||||
|
let edge_id = ArtifactId::new(helix.edge_id);
|
||||||
|
let return_arr = vec![Artifact::Helix(Helix {
|
||||||
|
id,
|
||||||
|
axis_id: Some(edge_id),
|
||||||
|
code_ref: CodeRef { range, path_to_node },
|
||||||
|
})];
|
||||||
|
// We could add the reverse graph edge connecting from the edge to
|
||||||
|
// the helix here, but it's not useful right now.
|
||||||
|
return Ok(return_arr);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +79,7 @@ impl Artifact {
|
|||||||
Artifact::SweepEdge(a) => vec![a.seg_id, a.sweep_id],
|
Artifact::SweepEdge(a) => vec![a.seg_id, a.sweep_id],
|
||||||
Artifact::EdgeCut(a) => vec![a.consumed_edge_id],
|
Artifact::EdgeCut(a) => vec![a.consumed_edge_id],
|
||||||
Artifact::EdgeCutEdge(a) => vec![a.edge_cut_id],
|
Artifact::EdgeCutEdge(a) => vec![a.edge_cut_id],
|
||||||
|
Artifact::Helix(a) => a.axis_id.map(|id| vec![id]).unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,6 +158,10 @@ impl Artifact {
|
|||||||
// Note: Don't include these since they're parents: edge_cut_id.
|
// Note: Don't include these since they're parents: edge_cut_id.
|
||||||
vec![a.surface_id]
|
vec![a.surface_id]
|
||||||
}
|
}
|
||||||
|
Artifact::Helix(_) => {
|
||||||
|
// Note: Don't include these since they're parents: axis_id.
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +229,8 @@ impl ArtifactGraph {
|
|||||||
| Artifact::Cap(_)
|
| Artifact::Cap(_)
|
||||||
| Artifact::SweepEdge(_)
|
| Artifact::SweepEdge(_)
|
||||||
| Artifact::EdgeCut(_)
|
| Artifact::EdgeCut(_)
|
||||||
| Artifact::EdgeCutEdge(_) => false,
|
| Artifact::EdgeCutEdge(_)
|
||||||
|
| Artifact::Helix(_) => false,
|
||||||
};
|
};
|
||||||
if !grouped {
|
if !grouped {
|
||||||
ungrouped.push(id);
|
ungrouped.push(id);
|
||||||
@ -341,6 +347,14 @@ impl ArtifactGraph {
|
|||||||
Artifact::EdgeCutEdge(_edge_cut_edge) => {
|
Artifact::EdgeCutEdge(_edge_cut_edge) => {
|
||||||
writeln!(output, "{prefix}{}[EdgeCutEdge]", id)?;
|
writeln!(output, "{prefix}{}[EdgeCutEdge]", id)?;
|
||||||
}
|
}
|
||||||
|
Artifact::Helix(helix) => {
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
"{prefix}{}[\"Helix<br>{:?}\"]",
|
||||||
|
id,
|
||||||
|
code_ref_display(&helix.code_ref)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -522,6 +536,9 @@ impl ArtifactGraph {
|
|||||||
Artifact::EdgeCutEdge(_edge_cut_edge) => {
|
Artifact::EdgeCutEdge(_edge_cut_edge) => {
|
||||||
writeln!(output, "{prefix}EdgeCutEdge")?;
|
writeln!(output, "{prefix}EdgeCutEdge")?;
|
||||||
}
|
}
|
||||||
|
Artifact::Helix(_) => {
|
||||||
|
writeln!(output, "{prefix}Helix")?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ids_seen.contains(&artifact.id()) {
|
if ids_seen.contains(&artifact.id()) {
|
||||||
|
@ -128,7 +128,7 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
|
|||||||
properties: new_properties,
|
properties: new_properties,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
name.digest == new_name.digest
|
name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
|
||||||
&& properties
|
&& properties
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
|
.map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
|
||||||
|
@ -10,17 +10,17 @@ use crate::{
|
|||||||
annotations,
|
annotations,
|
||||||
cad_op::{OpArg, Operation},
|
cad_op::{OpArg, Operation},
|
||||||
state::ModuleState,
|
state::ModuleState,
|
||||||
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModuleRepr, ProgramMemory,
|
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ProgramMemory, TagEngineInfo,
|
||||||
TagEngineInfo, TagIdentifier,
|
TagIdentifier,
|
||||||
},
|
},
|
||||||
fs::FileSystem,
|
modules::{ModuleId, ModulePath, ModuleRepr},
|
||||||
parsing::ast::types::{
|
parsing::ast::types::{
|
||||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
||||||
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
|
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
|
||||||
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression,
|
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression,
|
||||||
PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator,
|
PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator,
|
||||||
},
|
},
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::SourceRange,
|
||||||
std::{
|
std::{
|
||||||
args::{Arg, KwArgs},
|
args::{Arg, KwArgs},
|
||||||
FunctionKind,
|
FunctionKind,
|
||||||
@ -38,7 +38,8 @@ impl ExecutorContext {
|
|||||||
annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>,
|
annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>,
|
||||||
scope: annotations::AnnotationScope,
|
scope: annotations::AnnotationScope,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
) -> Result<(), KclError> {
|
) -> Result<bool, KclError> {
|
||||||
|
let mut no_prelude = false;
|
||||||
for (annotation, source_range) in annotations {
|
for (annotation, source_range) in annotations {
|
||||||
if annotation.annotation_name() == Some(annotations::SETTINGS) {
|
if annotation.annotation_name() == Some(annotations::SETTINGS) {
|
||||||
if scope == annotations::AnnotationScope::Module {
|
if scope == annotations::AnnotationScope::Module {
|
||||||
@ -48,7 +49,7 @@ impl ExecutorContext {
|
|||||||
.settings
|
.settings
|
||||||
.update_from_annotation(annotation, source_range)?;
|
.update_from_annotation(annotation, source_range)?;
|
||||||
let new_units = exec_state.length_unit();
|
let new_units = exec_state.length_unit();
|
||||||
if old_units != new_units {
|
if !self.engine.execution_kind().is_isolated() && old_units != new_units {
|
||||||
self.engine.set_units(new_units.into(), source_range).await?;
|
self.engine.set_units(new_units.into(), source_range).await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -58,9 +59,19 @@ impl ExecutorContext {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if annotation.annotation_name() == Some(annotations::NO_PRELUDE) {
|
||||||
|
if scope == annotations::AnnotationScope::Module {
|
||||||
|
no_prelude = true;
|
||||||
|
} else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "Prelude can only be skipped at the top level scope of a file".to_owned(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
// TODO warn on unknown annotations
|
// TODO warn on unknown annotations
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(no_prelude)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute an AST's program.
|
/// Execute an AST's program.
|
||||||
@ -71,22 +82,32 @@ impl ExecutorContext {
|
|||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
body_type: BodyType,
|
body_type: BodyType,
|
||||||
) -> Result<Option<KclValue>, KclError> {
|
) -> Result<Option<KclValue>, KclError> {
|
||||||
self.handle_annotations(
|
if body_type == BodyType::Root {
|
||||||
program
|
let _no_prelude = self
|
||||||
.non_code_meta
|
.handle_annotations(
|
||||||
.start_nodes
|
program
|
||||||
.iter()
|
.non_code_meta
|
||||||
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
|
.start_nodes
|
||||||
annotations::AnnotationScope::Module,
|
.iter()
|
||||||
exec_state,
|
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
|
||||||
)
|
annotations::AnnotationScope::Module,
|
||||||
.await?;
|
exec_state,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut last_expr = None;
|
let mut last_expr = None;
|
||||||
// Iterate over the body of the program.
|
// Iterate over the body of the program.
|
||||||
for statement in &program.body {
|
for (i, statement) in program.body.iter().enumerate() {
|
||||||
match statement {
|
match statement {
|
||||||
BodyItem::ImportStatement(import_stmt) => {
|
BodyItem::ImportStatement(import_stmt) => {
|
||||||
|
if body_type != BodyType::Root {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "Imports are only supported at the top-level of a file.".to_owned(),
|
||||||
|
source_ranges: vec![import_stmt.into()],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
let source_range = SourceRange::from(import_stmt);
|
let source_range = SourceRange::from(import_stmt);
|
||||||
let module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?;
|
let module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?;
|
||||||
|
|
||||||
@ -178,6 +199,14 @@ impl ExecutorContext {
|
|||||||
let source_range = SourceRange::from(&variable_declaration.declaration.init);
|
let source_range = SourceRange::from(&variable_declaration.declaration.init);
|
||||||
let metadata = Metadata { source_range };
|
let metadata = Metadata { source_range };
|
||||||
|
|
||||||
|
let _meta_nodes = if i == 0 {
|
||||||
|
&program.non_code_meta.start_nodes
|
||||||
|
} else if let Some(meta) = program.non_code_meta.non_code_nodes.get(&(i - 1)) {
|
||||||
|
meta
|
||||||
|
} else {
|
||||||
|
&Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
let memory_item = self
|
let memory_item = self
|
||||||
.execute_expr(
|
.execute_expr(
|
||||||
&variable_declaration.declaration.init,
|
&variable_declaration.declaration.init,
|
||||||
@ -231,63 +260,45 @@ impl ExecutorContext {
|
|||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
source_range: SourceRange,
|
source_range: SourceRange,
|
||||||
) -> Result<ModuleId, KclError> {
|
) -> Result<ModuleId, KclError> {
|
||||||
|
let resolved_path = ModulePath::from_import_path(path, &self.settings.project_directory);
|
||||||
match path {
|
match path {
|
||||||
ImportPath::Kcl { filename } => {
|
ImportPath::Kcl { .. } => {
|
||||||
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
exec_state.global.mod_loader.cycle_check(&resolved_path, source_range)?;
|
||||||
project_dir.join(filename)
|
|
||||||
} else {
|
|
||||||
std::path::PathBuf::from(filename)
|
|
||||||
};
|
|
||||||
|
|
||||||
if exec_state.mod_local.import_stack.contains(&resolved_path) {
|
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
||||||
return Err(KclError::ImportCycle(KclErrorDetails {
|
return Ok(id);
|
||||||
message: format!(
|
|
||||||
"circular import of modules is not allowed: {} -> {}",
|
|
||||||
exec_state
|
|
||||||
.mod_local
|
|
||||||
.import_stack
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.as_path().to_string_lossy())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" -> "),
|
|
||||||
resolved_path.to_string_lossy()
|
|
||||||
),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
|
let id = exec_state.next_module_id();
|
||||||
return Ok(*id);
|
let source = resolved_path.source(&self.fs, source_range).await?;
|
||||||
}
|
|
||||||
|
|
||||||
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
|
|
||||||
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
|
|
||||||
// TODO handle parsing errors properly
|
// TODO handle parsing errors properly
|
||||||
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
|
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
|
||||||
let repr = ModuleRepr::Kcl(parsed);
|
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed));
|
||||||
|
Ok(id)
|
||||||
Ok(exec_state.add_module(id, resolved_path, repr))
|
|
||||||
}
|
}
|
||||||
ImportPath::Foreign { path } => {
|
ImportPath::Foreign { .. } => {
|
||||||
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
||||||
project_dir.join(path)
|
return Ok(id);
|
||||||
} else {
|
|
||||||
std::path::PathBuf::from(path)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
|
|
||||||
return Ok(*id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let geom = super::import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?;
|
let id = exec_state.next_module_id();
|
||||||
let repr = ModuleRepr::Foreign(geom);
|
let geom =
|
||||||
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
|
super::import::import_foreign(resolved_path.expect_path(), None, exec_state, self, source_range)
|
||||||
Ok(exec_state.add_module(id, resolved_path, repr))
|
.await?;
|
||||||
|
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
ImportPath::Std { .. } => {
|
||||||
|
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
||||||
|
return Ok(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = exec_state.next_module_id();
|
||||||
|
let source = resolved_path.source(&self.fs, source_range).await?;
|
||||||
|
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err().unwrap();
|
||||||
|
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed));
|
||||||
|
Ok(id)
|
||||||
}
|
}
|
||||||
i => Err(KclError::Semantic(KclErrorDetails {
|
|
||||||
message: format!("Unsupported import: `{i}`"),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,22 +318,20 @@ impl ExecutorContext {
|
|||||||
message: format!(
|
message: format!(
|
||||||
"circular import of modules is not allowed: {} -> {}",
|
"circular import of modules is not allowed: {} -> {}",
|
||||||
exec_state
|
exec_state
|
||||||
.mod_local
|
.global
|
||||||
|
.mod_loader
|
||||||
.import_stack
|
.import_stack
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.as_path().to_string_lossy())
|
.map(|p| p.as_path().to_string_lossy())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" -> "),
|
.join(" -> "),
|
||||||
info.path.display()
|
info.path
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
source_ranges: vec![source_range],
|
||||||
})),
|
})),
|
||||||
ModuleRepr::Kcl(program) => {
|
ModuleRepr::Kcl(program) => {
|
||||||
let mut local_state = ModuleState {
|
let mut local_state = ModuleState::new(&self.settings);
|
||||||
import_stack: exec_state.mod_local.import_stack.clone(),
|
exec_state.global.mod_loader.enter_module(&info.path);
|
||||||
..ModuleState::new(&self.settings)
|
|
||||||
};
|
|
||||||
local_state.import_stack.push(info.path.clone());
|
|
||||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||||
let original_execution = self.engine.replace_execution_kind(exec_kind);
|
let original_execution = self.engine.replace_execution_kind(exec_kind);
|
||||||
|
|
||||||
@ -332,7 +341,8 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
let new_units = exec_state.length_unit();
|
let new_units = exec_state.length_unit();
|
||||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||||
if new_units != old_units {
|
exec_state.global.mod_loader.leave_module(&info.path);
|
||||||
|
if !exec_kind.is_isolated() && new_units != old_units {
|
||||||
self.engine.set_units(old_units.into(), Default::default()).await?;
|
self.engine.set_units(old_units.into(), Default::default()).await?;
|
||||||
}
|
}
|
||||||
self.engine.replace_execution_kind(original_execution);
|
self.engine.replace_execution_kind(original_execution);
|
||||||
@ -345,7 +355,7 @@ impl ExecutorContext {
|
|||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Error loading imported file. Open it to view more details. {}: {}",
|
"Error loading imported file. Open it to view more details. {}: {}",
|
||||||
info.path.display(),
|
info.path,
|
||||||
err.message()
|
err.message()
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
source_ranges: vec![source_range],
|
||||||
@ -1759,6 +1769,7 @@ mod test {
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
module_id: ModuleId::default(),
|
module_id: ModuleId::default(),
|
||||||
|
trivia: Vec::new(),
|
||||||
},
|
},
|
||||||
return_type: None,
|
return_type: None,
|
||||||
digest: None,
|
digest: None,
|
||||||
|
@ -9,7 +9,9 @@ use cache::OldAstState;
|
|||||||
pub use cad_op::Operation;
|
pub use cad_op::Operation;
|
||||||
pub use exec_ast::FunctionParam;
|
pub use exec_ast::FunctionParam;
|
||||||
pub use geometry::*;
|
pub use geometry::*;
|
||||||
pub(crate) use import::{import_foreign, send_to_engine as send_import_to_engine, ZOO_COORD_SYSTEM};
|
pub(crate) use import::{
|
||||||
|
import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM,
|
||||||
|
};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
|
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
|
||||||
use kcmc::{
|
use kcmc::{
|
||||||
@ -34,7 +36,7 @@ use crate::{
|
|||||||
fs::FileManager,
|
fs::FileManager,
|
||||||
parsing::ast::types::{Expr, FunctionExpression, Node, NodeRef, Program},
|
parsing::ast::types::{Expr, FunctionExpression, Node, NodeRef, Program},
|
||||||
settings::types::UnitLength,
|
settings::types::UnitLength,
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::SourceRange,
|
||||||
std::{args::Arg, StdLib},
|
std::{args::Arg, StdLib},
|
||||||
ExecError, KclErrorWithOutputs,
|
ExecError, KclErrorWithOutputs,
|
||||||
};
|
};
|
||||||
@ -162,25 +164,6 @@ pub enum BodyType {
|
|||||||
Block,
|
Block,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Info about a module. Right now, this is pretty minimal. We hope to cache
|
|
||||||
/// modules here in the future.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
|
||||||
pub struct ModuleInfo {
|
|
||||||
/// The ID of the module.
|
|
||||||
id: ModuleId,
|
|
||||||
/// Absolute path of the module's source file.
|
|
||||||
path: std::path::PathBuf,
|
|
||||||
repr: ModuleRepr,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
|
||||||
pub enum ModuleRepr {
|
|
||||||
Root,
|
|
||||||
Kcl(Node<Program>),
|
|
||||||
Foreign(import::PreImportedGeometry),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata.
|
/// Metadata.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@ -791,7 +774,7 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::errors::KclErrorDetails;
|
use crate::{errors::KclErrorDetails, ModuleId};
|
||||||
|
|
||||||
/// Convenience function to get a JSON value from memory and unwrap.
|
/// Convenience function to get a JSON value from memory and unwrap.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
@ -9,10 +9,11 @@ use crate::{
|
|||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
execution::{
|
execution::{
|
||||||
annotations, kcl_value, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, ExecOutcome, ExecutorSettings,
|
annotations, kcl_value, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, ExecOutcome, ExecutorSettings,
|
||||||
KclValue, ModuleInfo, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen,
|
KclValue, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen,
|
||||||
},
|
},
|
||||||
|
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr},
|
||||||
parsing::ast::types::NonCodeValue,
|
parsing::ast::types::NonCodeValue,
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::SourceRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// State for executing a program.
|
/// State for executing a program.
|
||||||
@ -29,7 +30,7 @@ pub struct GlobalState {
|
|||||||
/// The stable artifact ID generator.
|
/// The stable artifact ID generator.
|
||||||
pub id_generator: IdGenerator,
|
pub id_generator: IdGenerator,
|
||||||
/// Map from source file absolute path to module ID.
|
/// Map from source file absolute path to module ID.
|
||||||
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
|
pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
|
||||||
/// Map from module ID to module info.
|
/// Map from module ID to module info.
|
||||||
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
|
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
|
||||||
/// Output map of UUIDs to artifacts.
|
/// Output map of UUIDs to artifacts.
|
||||||
@ -45,6 +46,8 @@ pub struct GlobalState {
|
|||||||
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
|
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
|
||||||
/// Output artifact graph.
|
/// Output artifact graph.
|
||||||
pub artifact_graph: ArtifactGraph,
|
pub artifact_graph: ArtifactGraph,
|
||||||
|
/// Module loader.
|
||||||
|
pub mod_loader: ModuleLoader,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@ -59,9 +62,6 @@ pub struct ModuleState {
|
|||||||
pub pipe_value: Option<KclValue>,
|
pub pipe_value: Option<KclValue>,
|
||||||
/// Identifiers that have been exported from the current module.
|
/// Identifiers that have been exported from the current module.
|
||||||
pub module_exports: Vec<String>,
|
pub module_exports: Vec<String>,
|
||||||
/// The stack of import statements for detecting circular module imports.
|
|
||||||
/// If this is empty, we're not currently executing an import statement.
|
|
||||||
pub import_stack: Vec<std::path::PathBuf>,
|
|
||||||
/// Operations that have been performed in execution order, for display in
|
/// Operations that have been performed in execution order, for display in
|
||||||
/// the Feature Tree.
|
/// the Feature Tree.
|
||||||
pub operations: Vec<Operation>,
|
pub operations: Vec<Operation>,
|
||||||
@ -124,15 +124,21 @@ impl ExecState {
|
|||||||
self.global.artifacts.insert(id, artifact);
|
self.global.artifacts.insert(id, artifact);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn add_module(&mut self, id: ModuleId, path: std::path::PathBuf, repr: ModuleRepr) -> ModuleId {
|
pub(super) fn next_module_id(&self) -> ModuleId {
|
||||||
|
ModuleId::from_usize(self.global.path_to_source_id.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn id_for_module(&self, path: &ModulePath) -> Option<ModuleId> {
|
||||||
|
self.global.path_to_source_id.get(path).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
|
||||||
debug_assert!(!self.global.path_to_source_id.contains_key(&path));
|
debug_assert!(!self.global.path_to_source_id.contains_key(&path));
|
||||||
|
|
||||||
self.global.path_to_source_id.insert(path.clone(), id);
|
self.global.path_to_source_id.insert(path.clone(), id);
|
||||||
|
|
||||||
let module_info = ModuleInfo { id, repr, path };
|
let module_info = ModuleInfo { id, repr, path };
|
||||||
self.global.module_infos.insert(id, module_info);
|
self.global.module_infos.insert(id, module_info);
|
||||||
|
|
||||||
id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn length_unit(&self) -> UnitLen {
|
pub fn length_unit(&self) -> UnitLen {
|
||||||
@ -154,6 +160,7 @@ impl GlobalState {
|
|||||||
artifact_commands: Default::default(),
|
artifact_commands: Default::default(),
|
||||||
artifact_responses: Default::default(),
|
artifact_responses: Default::default(),
|
||||||
artifact_graph: Default::default(),
|
artifact_graph: Default::default(),
|
||||||
|
mod_loader: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let root_id = ModuleId::default();
|
let root_id = ModuleId::default();
|
||||||
@ -162,11 +169,11 @@ impl GlobalState {
|
|||||||
root_id,
|
root_id,
|
||||||
ModuleInfo {
|
ModuleInfo {
|
||||||
id: root_id,
|
id: root_id,
|
||||||
path: root_path.clone(),
|
path: ModulePath::Local(root_path.clone()),
|
||||||
repr: ModuleRepr::Root,
|
repr: ModuleRepr::Root,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
global.path_to_source_id.insert(root_path, root_id);
|
global.path_to_source_id.insert(ModulePath::Local(root_path), root_id);
|
||||||
global
|
global
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,7 +185,6 @@ impl ModuleState {
|
|||||||
dynamic_state: Default::default(),
|
dynamic_state: Default::default(),
|
||||||
pipe_value: Default::default(),
|
pipe_value: Default::default(),
|
||||||
module_exports: Default::default(),
|
module_exports: Default::default(),
|
||||||
import_stack: Default::default(),
|
|
||||||
operations: Default::default(),
|
operations: Default::default(),
|
||||||
settings: MetaSettings {
|
settings: MetaSettings {
|
||||||
default_length_units: exec_settings.units.into(),
|
default_length_units: exec_settings.units.into(),
|
||||||
|
@ -65,6 +65,7 @@ mod fs;
|
|||||||
pub mod lint;
|
pub mod lint;
|
||||||
mod log;
|
mod log;
|
||||||
mod lsp;
|
mod lsp;
|
||||||
|
mod modules;
|
||||||
mod parsing;
|
mod parsing;
|
||||||
mod settings;
|
mod settings;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -87,9 +88,10 @@ pub use lsp::{
|
|||||||
copilot::Backend as CopilotLspBackend,
|
copilot::Backend as CopilotLspBackend,
|
||||||
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
|
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
|
||||||
};
|
};
|
||||||
|
pub use modules::ModuleId;
|
||||||
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
|
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
|
||||||
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
|
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
|
||||||
pub use source_range::{ModuleId, SourceRange};
|
pub use source_range::SourceRange;
|
||||||
|
|
||||||
// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
|
// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
|
||||||
// Ideally we wouldn't export these things at all, they should only be used for testing.
|
// Ideally we wouldn't export these things at all, they should only be used for testing.
|
||||||
|
157
src/wasm-lib/kcl/src/modules.rs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
use std::{fmt, path::PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{KclError, KclErrorDetails},
|
||||||
|
execution::PreImportedGeometry,
|
||||||
|
fs::{FileManager, FileSystem},
|
||||||
|
parsing::ast::types::{ImportPath, Node, Program},
|
||||||
|
source_range::SourceRange,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Identifier of a source file. Uses a u32 to keep the size small.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct ModuleId(u32);
|
||||||
|
|
||||||
|
impl ModuleId {
|
||||||
|
pub fn from_usize(id: usize) -> Self {
|
||||||
|
Self(u32::try_from(id).expect("module ID should fit in a u32"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_usize(&self) -> usize {
|
||||||
|
usize::try_from(self.0).expect("module ID should fit in a usize")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top-level file is the one being executed.
|
||||||
|
/// Represented by module ID of 0, i.e. the default value.
|
||||||
|
pub fn is_top_level(&self) -> bool {
|
||||||
|
*self == Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ModuleLoader {
|
||||||
|
/// The stack of import statements for detecting circular module imports.
|
||||||
|
/// If this is empty, we're not currently executing an import statement.
|
||||||
|
pub import_stack: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModuleLoader {
|
||||||
|
pub(crate) fn cycle_check(&self, path: &ModulePath, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
|
if self.import_stack.contains(path.expect_path()) {
|
||||||
|
return Err(KclError::ImportCycle(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"circular import of modules is not allowed: {} -> {}",
|
||||||
|
self.import_stack
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.as_path().to_string_lossy())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" -> "),
|
||||||
|
path,
|
||||||
|
),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn enter_module(&mut self, path: &ModulePath) {
|
||||||
|
if let ModulePath::Local(ref path) = path {
|
||||||
|
self.import_stack.push(path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn leave_module(&mut self, path: &ModulePath) {
|
||||||
|
if let ModulePath::Local(ref path) = path {
|
||||||
|
let popped = self.import_stack.pop().unwrap();
|
||||||
|
assert_eq!(path, &popped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_std(_mod_name: &str) -> Option<&'static str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info about a module. Right now, this is pretty minimal. We hope to cache
|
||||||
|
/// modules here in the future.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct ModuleInfo {
|
||||||
|
/// The ID of the module.
|
||||||
|
pub(crate) id: ModuleId,
|
||||||
|
/// Absolute path of the module's source file.
|
||||||
|
pub(crate) path: ModulePath,
|
||||||
|
pub(crate) repr: ModuleRepr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub enum ModuleRepr {
|
||||||
|
Root,
|
||||||
|
Kcl(Node<Program>),
|
||||||
|
Foreign(PreImportedGeometry),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)]
|
||||||
|
pub enum ModulePath {
|
||||||
|
Local(PathBuf),
|
||||||
|
Std(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModulePath {
|
||||||
|
pub(crate) fn expect_path(&self) -> &PathBuf {
|
||||||
|
match self {
|
||||||
|
ModulePath::Local(p) => p,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result<String, KclError> {
|
||||||
|
match self {
|
||||||
|
ModulePath::Local(p) => fs.read_to_string(p, source_range).await,
|
||||||
|
ModulePath::Std(name) => read_std(name)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("Cannot find standard library module to import: std::{name}."),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(str::to_owned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_import_path(path: &ImportPath, project_directory: &Option<PathBuf>) -> Self {
|
||||||
|
match path {
|
||||||
|
ImportPath::Kcl { filename: path } | ImportPath::Foreign { path } => {
|
||||||
|
let resolved_path = if let Some(project_dir) = project_directory {
|
||||||
|
project_dir.join(path)
|
||||||
|
} else {
|
||||||
|
std::path::PathBuf::from(path)
|
||||||
|
};
|
||||||
|
ModulePath::Local(resolved_path)
|
||||||
|
}
|
||||||
|
ImportPath::Std { path } => {
|
||||||
|
// For now we only support importing from singly-nested modules inside std.
|
||||||
|
assert_eq!(path.len(), 2);
|
||||||
|
assert_eq!(&path[0], "std");
|
||||||
|
|
||||||
|
ModulePath::Std(path[1].clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ModulePath {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ModulePath::Local(path) => path.display().fmt(f),
|
||||||
|
ModulePath::Std(s) => write!(f, "std::{s}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -121,7 +121,9 @@ impl NonCodeValue {
|
|||||||
ref mut name,
|
ref mut name,
|
||||||
properties,
|
properties,
|
||||||
} => {
|
} => {
|
||||||
hasher.update(name.compute_digest());
|
if let Some(name) = name {
|
||||||
|
hasher.update(name.compute_digest());
|
||||||
|
}
|
||||||
if let Some(properties) = properties {
|
if let Some(properties) = properties {
|
||||||
hasher.update(properties.len().to_ne_bytes());
|
hasher.update(properties.len().to_ne_bytes());
|
||||||
for property in properties.iter_mut() {
|
for property in properties.iter_mut() {
|
||||||
|
@ -4,7 +4,7 @@ pub mod types;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject},
|
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject},
|
||||||
source_range::ModuleId,
|
ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl BodyItem {
|
impl BodyItem {
|
||||||
|
@ -15,8 +15,8 @@ use crate::{
|
|||||||
ArrayExpression, CallExpression, ConstraintLevel, FormatOptions, Literal, Node, PipeExpression,
|
ArrayExpression, CallExpression, ConstraintLevel, FormatOptions, Literal, Node, PipeExpression,
|
||||||
PipeSubstitution, VariableDeclarator,
|
PipeSubstitution, VariableDeclarator,
|
||||||
},
|
},
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::SourceRange,
|
||||||
Program,
|
ModuleId, Program,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Point3d = kcmc::shared::Point3d<f64>;
|
type Point3d = kcmc::shared::Point3d<f64>;
|
||||||
|
@ -27,7 +27,9 @@ use crate::{
|
|||||||
errors::KclError,
|
errors::KclError,
|
||||||
execution::{annotations, KclValue, Metadata, TagIdentifier},
|
execution::{annotations, KclValue, Metadata, TagIdentifier},
|
||||||
parsing::{ast::digest::Digest, PIPE_OPERATOR},
|
parsing::{ast::digest::Digest, PIPE_OPERATOR},
|
||||||
source_range::{ModuleId, SourceRange},
|
pretty::NumericSuffix,
|
||||||
|
source_range::SourceRange,
|
||||||
|
ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod condition;
|
mod condition;
|
||||||
@ -39,7 +41,7 @@ pub enum Definition<'a> {
|
|||||||
Import(NodeRef<'a, ImportStatement>),
|
Import(NodeRef<'a, ImportStatement>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Node<T> {
|
pub struct Node<T> {
|
||||||
@ -49,6 +51,8 @@ pub struct Node<T> {
|
|||||||
pub end: usize,
|
pub end: usize,
|
||||||
#[serde(default, skip_serializing_if = "ModuleId::is_top_level")]
|
#[serde(default, skip_serializing_if = "ModuleId::is_top_level")]
|
||||||
pub module_id: ModuleId,
|
pub module_id: ModuleId,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub trivia: NodeList<NonCodeNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Node<T> {
|
impl<T> Node<T> {
|
||||||
@ -92,6 +96,7 @@ impl<T> Node<T> {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
module_id,
|
module_id,
|
||||||
|
trivia: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +106,7 @@ impl<T> Node<T> {
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
module_id: ModuleId::default(),
|
module_id: ModuleId::default(),
|
||||||
|
trivia: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +116,7 @@ impl<T> Node<T> {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
module_id,
|
module_id,
|
||||||
|
trivia: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,6 +647,7 @@ impl From<&BodyItem> for SourceRange {
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum Expr {
|
pub enum Expr {
|
||||||
Literal(BoxNode<Literal>),
|
Literal(BoxNode<Literal>),
|
||||||
Identifier(BoxNode<Identifier>),
|
Identifier(BoxNode<Identifier>),
|
||||||
@ -868,6 +876,43 @@ impl Expr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn literal_bool(&self) -> Option<bool> {
|
||||||
|
match self {
|
||||||
|
Expr::Literal(lit) => match lit.value {
|
||||||
|
LiteralValue::Bool(b) => Some(b),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn literal_num(&self) -> Option<(f64, NumericSuffix)> {
|
||||||
|
match self {
|
||||||
|
Expr::Literal(lit) => match lit.value {
|
||||||
|
LiteralValue::Number { value, suffix } => Some((value, suffix)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn literal_str(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Expr::Literal(lit) => match &lit.value {
|
||||||
|
LiteralValue::String(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ident_name(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Expr::Identifier(ident) => Some(&ident.name),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Describe this expression's type for a human, for typechecking.
|
/// Describe this expression's type for a human, for typechecking.
|
||||||
/// This is a best-effort function, it's OK to give a shitty string here (but we should work on improving it)
|
/// This is a best-effort function, it's OK to give a shitty string here (but we should work on improving it)
|
||||||
pub fn human_friendly_type(&self) -> &'static str {
|
pub fn human_friendly_type(&self) -> &'static str {
|
||||||
@ -1089,7 +1134,7 @@ impl NonCodeNode {
|
|||||||
NonCodeValue::BlockComment { value, style: _ } => value.clone(),
|
NonCodeValue::BlockComment { value, style: _ } => value.clone(),
|
||||||
NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
|
NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
|
||||||
NonCodeValue::NewLine => "\n\n".to_string(),
|
NonCodeValue::NewLine => "\n\n".to_string(),
|
||||||
NonCodeValue::Annotation { name, .. } => name.name.clone(),
|
n @ NonCodeValue::Annotation { .. } => n.annotation_name().unwrap_or("").to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1158,7 +1203,7 @@ pub enum NonCodeValue {
|
|||||||
// This is also not a comment.
|
// This is also not a comment.
|
||||||
NewLine,
|
NewLine,
|
||||||
Annotation {
|
Annotation {
|
||||||
name: Node<Identifier>,
|
name: Option<Node<Identifier>>,
|
||||||
properties: Option<Vec<Node<ObjectProperty>>>,
|
properties: Option<Vec<Node<ObjectProperty>>>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1166,7 +1211,7 @@ pub enum NonCodeValue {
|
|||||||
impl NonCodeValue {
|
impl NonCodeValue {
|
||||||
pub fn annotation_name(&self) -> Option<&str> {
|
pub fn annotation_name(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
NonCodeValue::Annotation { name, .. } => Some(&name.name),
|
NonCodeValue::Annotation { name, .. } => name.as_ref().map(|i| &*i.name),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1184,7 +1229,7 @@ impl NonCodeValue {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
NonCodeValue::Annotation {
|
NonCodeValue::Annotation {
|
||||||
name: Identifier::new(annotations::SETTINGS),
|
name: Some(Identifier::new(annotations::SETTINGS)),
|
||||||
properties: Some(properties),
|
properties: Some(properties),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1212,6 +1257,20 @@ impl NonCodeMeta {
|
|||||||
pub fn non_code_nodes_len(&self) -> usize {
|
pub fn non_code_nodes_len(&self) -> usize {
|
||||||
self.non_code_nodes.values().map(|x| x.len()).sum()
|
self.non_code_nodes.values().map(|x| x.len()).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, i: usize, new: Node<NonCodeNode>) {
|
||||||
|
self.non_code_nodes.entry(i).or_default().push(new);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, pos: usize) -> bool {
|
||||||
|
if self.start_nodes.iter().any(|node| node.contains(pos)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.non_code_nodes
|
||||||
|
.iter()
|
||||||
|
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize
|
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize
|
||||||
@ -1242,22 +1301,6 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NonCodeMeta {
|
|
||||||
pub fn insert(&mut self, i: usize, new: Node<NonCodeNode>) {
|
|
||||||
self.non_code_nodes.entry(i).or_default().push(new);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contains(&self, pos: usize) -> bool {
|
|
||||||
if self.start_nodes.iter().any(|node| node.contains(pos)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.non_code_nodes
|
|
||||||
.iter()
|
|
||||||
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
@ -1382,14 +1425,14 @@ impl ImportSelector {
|
|||||||
pub enum ImportPath {
|
pub enum ImportPath {
|
||||||
Kcl { filename: String },
|
Kcl { filename: String },
|
||||||
Foreign { path: String },
|
Foreign { path: String },
|
||||||
Std,
|
Std { path: Vec<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ImportPath {
|
impl fmt::Display for ImportPath {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => write!(f, "{s}"),
|
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => write!(f, "{s}"),
|
||||||
ImportPath::Std => write!(f, "std"),
|
ImportPath::Std { path } => write!(f, "{}", path.join("::")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1746,7 +1789,7 @@ pub enum ItemVisibility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ItemVisibility {
|
impl ItemVisibility {
|
||||||
fn is_default(&self) -> bool {
|
pub fn is_default(&self) -> bool {
|
||||||
matches!(self, Self::Default)
|
matches!(self, Self::Default)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2941,16 +2984,14 @@ impl PipeExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, FromStr, Display)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
#[display(style = "snake_case")]
|
|
||||||
pub enum FnArgPrimitive {
|
pub enum FnArgPrimitive {
|
||||||
/// A string type.
|
/// A string type.
|
||||||
String,
|
String,
|
||||||
/// A number type.
|
/// A number type.
|
||||||
Number,
|
Number(NumericSuffix),
|
||||||
/// A boolean type.
|
/// A boolean type.
|
||||||
#[display("bool")]
|
|
||||||
#[serde(rename = "bool")]
|
#[serde(rename = "bool")]
|
||||||
Boolean,
|
Boolean,
|
||||||
/// A tag.
|
/// A tag.
|
||||||
@ -2967,12 +3008,46 @@ impl FnArgPrimitive {
|
|||||||
pub fn digestable_id(&self) -> &[u8] {
|
pub fn digestable_id(&self) -> &[u8] {
|
||||||
match self {
|
match self {
|
||||||
FnArgPrimitive::String => b"string",
|
FnArgPrimitive::String => b"string",
|
||||||
FnArgPrimitive::Number => b"number",
|
FnArgPrimitive::Number(suffix) => suffix.digestable_id(),
|
||||||
FnArgPrimitive::Boolean => b"boolean",
|
FnArgPrimitive::Boolean => b"bool",
|
||||||
FnArgPrimitive::Tag => b"tag",
|
FnArgPrimitive::Tag => b"tag",
|
||||||
FnArgPrimitive::Sketch => b"sketch",
|
FnArgPrimitive::Sketch => b"Sketch",
|
||||||
FnArgPrimitive::SketchSurface => b"sketch_surface",
|
FnArgPrimitive::SketchSurface => b"SketchSurface",
|
||||||
FnArgPrimitive::Solid => b"solid",
|
FnArgPrimitive::Solid => b"Solid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str, suffix: Option<NumericSuffix>) -> Option<Self> {
|
||||||
|
match (s, suffix) {
|
||||||
|
("string", None) => Some(FnArgPrimitive::String),
|
||||||
|
("bool", None) => Some(FnArgPrimitive::Boolean),
|
||||||
|
("tag", None) => Some(FnArgPrimitive::Tag),
|
||||||
|
("Sketch", None) => Some(FnArgPrimitive::Sketch),
|
||||||
|
("SketchSurface", None) => Some(FnArgPrimitive::SketchSurface),
|
||||||
|
("Solid", None) => Some(FnArgPrimitive::Solid),
|
||||||
|
("number", None) => Some(FnArgPrimitive::Number(NumericSuffix::None)),
|
||||||
|
("number", Some(s)) => Some(FnArgPrimitive::Number(s)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FnArgPrimitive {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
FnArgPrimitive::Number(suffix) => {
|
||||||
|
write!(f, "number")?;
|
||||||
|
if *suffix != NumericSuffix::None {
|
||||||
|
write!(f, "({suffix})")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
FnArgPrimitive::String => write!(f, "string"),
|
||||||
|
FnArgPrimitive::Boolean => write!(f, "bool"),
|
||||||
|
FnArgPrimitive::Tag => write!(f, "tag"),
|
||||||
|
FnArgPrimitive::Sketch => write!(f, "Sketch"),
|
||||||
|
FnArgPrimitive::SketchSurface => write!(f, "SketchSurface"),
|
||||||
|
FnArgPrimitive::Solid => write!(f, "Solid"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3029,8 +3104,7 @@ pub struct Parameter {
|
|||||||
pub identifier: Node<Identifier>,
|
pub identifier: Node<Identifier>,
|
||||||
/// The type of the parameter.
|
/// The type of the parameter.
|
||||||
/// This is optional if the user defines a type.
|
/// This is optional if the user defines a type.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(skip)]
|
||||||
#[ts(skip)]
|
|
||||||
pub type_: Option<FnArgType>,
|
pub type_: Option<FnArgType>,
|
||||||
/// Is the parameter optional?
|
/// Is the parameter optional?
|
||||||
/// If so, what is its default value?
|
/// If so, what is its default value?
|
||||||
@ -3516,7 +3590,7 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_parse_type_args_on_functions() {
|
async fn test_parse_type_args_on_functions() {
|
||||||
let some_program_string = r#"fn thing = (arg0: number, arg1: string, tag?: string) => {
|
let some_program_string = r#"fn thing = (arg0: number(mm), arg1: string, tag?: string) => {
|
||||||
return arg0
|
return arg0
|
||||||
}"#;
|
}"#;
|
||||||
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
|
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
|
||||||
@ -3531,7 +3605,10 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
};
|
};
|
||||||
let params = &func_expr.params;
|
let params = &func_expr.params;
|
||||||
assert_eq!(params.len(), 3);
|
assert_eq!(params.len(), 3);
|
||||||
assert_eq!(params[0].type_, Some(FnArgType::Primitive(FnArgPrimitive::Number)));
|
assert_eq!(
|
||||||
|
params[0].type_,
|
||||||
|
Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::Mm)))
|
||||||
|
);
|
||||||
assert_eq!(params[1].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
|
assert_eq!(params[1].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
|
||||||
assert_eq!(params[2].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
|
assert_eq!(params[2].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
|
||||||
}
|
}
|
||||||
@ -3553,7 +3630,10 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
};
|
};
|
||||||
let params = &func_expr.params;
|
let params = &func_expr.params;
|
||||||
assert_eq!(params.len(), 3);
|
assert_eq!(params.len(), 3);
|
||||||
assert_eq!(params[0].type_, Some(FnArgType::Array(FnArgPrimitive::Number)));
|
assert_eq!(
|
||||||
|
params[0].type_,
|
||||||
|
Some(FnArgType::Array(FnArgPrimitive::Number(NumericSuffix::None)))
|
||||||
|
);
|
||||||
assert_eq!(params[1].type_, Some(FnArgType::Array(FnArgPrimitive::String)));
|
assert_eq!(params[1].type_, Some(FnArgType::Array(FnArgPrimitive::String)));
|
||||||
assert_eq!(params[2].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
|
assert_eq!(params[2].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
|
||||||
}
|
}
|
||||||
@ -3576,7 +3656,10 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
};
|
};
|
||||||
let params = &func_expr.params;
|
let params = &func_expr.params;
|
||||||
assert_eq!(params.len(), 3);
|
assert_eq!(params.len(), 3);
|
||||||
assert_eq!(params[0].type_, Some(FnArgType::Array(FnArgPrimitive::Number)));
|
assert_eq!(
|
||||||
|
params[0].type_,
|
||||||
|
Some(FnArgType::Array(FnArgPrimitive::Number(NumericSuffix::None)))
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
params[1].type_,
|
params[1].type_,
|
||||||
Some(FnArgType::Object {
|
Some(FnArgType::Object {
|
||||||
@ -3591,7 +3674,7 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
40,
|
40,
|
||||||
module_id,
|
module_id,
|
||||||
),
|
),
|
||||||
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number)),
|
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::None))),
|
||||||
default_value: None,
|
default_value: None,
|
||||||
labeled: true,
|
labeled: true,
|
||||||
digest: None,
|
digest: None,
|
||||||
@ -3664,7 +3747,7 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
18,
|
18,
|
||||||
module_id,
|
module_id,
|
||||||
),
|
),
|
||||||
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number)),
|
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::None))),
|
||||||
default_value: None,
|
default_value: None,
|
||||||
labeled: true,
|
labeled: true,
|
||||||
digest: None
|
digest: None
|
||||||
@ -3746,6 +3829,7 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
module_id: ModuleId::default(),
|
module_id: ModuleId::default(),
|
||||||
|
trivia: Vec::new(),
|
||||||
},
|
},
|
||||||
return_type: None,
|
return_type: None,
|
||||||
digest: None,
|
digest: None,
|
||||||
@ -3775,6 +3859,7 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
module_id: ModuleId::default(),
|
module_id: ModuleId::default(),
|
||||||
|
trivia: Vec::new(),
|
||||||
},
|
},
|
||||||
return_type: None,
|
return_type: None,
|
||||||
digest: None,
|
digest: None,
|
||||||
@ -3816,6 +3901,7 @@ const cylinder = startSketchOn('-XZ')
|
|||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
module_id: ModuleId::default(),
|
module_id: ModuleId::default(),
|
||||||
|
trivia: Vec::new(),
|
||||||
},
|
},
|
||||||
return_type: None,
|
return_type: None,
|
||||||
digest: None,
|
digest: None,
|
||||||
|
@ -131,7 +131,7 @@ mod tests {
|
|||||||
ast::types::{Literal, LiteralValue},
|
ast::types::{Literal, LiteralValue},
|
||||||
token::NumericSuffix,
|
token::NumericSuffix,
|
||||||
},
|
},
|
||||||
source_range::ModuleId,
|
ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -4,7 +4,8 @@ use crate::{
|
|||||||
ast::types::{Node, Program},
|
ast::types::{Node, Program},
|
||||||
token::TokenStream,
|
token::TokenStream,
|
||||||
},
|
},
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::SourceRange,
|
||||||
|
ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod ast;
|
pub(crate) mod ast;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// TODO optimise size of CompilationError
|
// TODO optimise size of CompilationError
|
||||||
#![allow(clippy::result_large_err)]
|
#![allow(clippy::result_large_err)]
|
||||||
|
|
||||||
use std::{cell::RefCell, collections::BTreeMap, str::FromStr};
|
use std::{cell::RefCell, collections::BTreeMap};
|
||||||
|
|
||||||
use winnow::{
|
use winnow::{
|
||||||
combinator::{alt, delimited, opt, peek, preceded, repeat, separated, separated_pair, terminated},
|
combinator::{alt, delimited, opt, peek, preceded, repeat, separated, separated_pair, terminated},
|
||||||
@ -286,8 +286,8 @@ fn non_code_node(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
|
|||||||
|
|
||||||
fn annotation(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
|
fn annotation(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
|
||||||
let at = at_sign.parse_next(i)?;
|
let at = at_sign.parse_next(i)?;
|
||||||
let name = binding_name.parse_next(i)?;
|
let name = opt(binding_name).parse_next(i)?;
|
||||||
let mut end = name.end;
|
let mut end = name.as_ref().map(|n| n.end).unwrap_or(at.end);
|
||||||
|
|
||||||
let properties = if peek(open_paren).parse_next(i).is_ok() {
|
let properties = if peek(open_paren).parse_next(i).is_ok() {
|
||||||
open_paren(i)?;
|
open_paren(i)?;
|
||||||
@ -308,6 +308,7 @@ fn annotation(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
|
|||||||
value,
|
value,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
}),
|
}),
|
||||||
comma_sep,
|
comma_sep,
|
||||||
)
|
)
|
||||||
@ -320,6 +321,12 @@ fn annotation(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if name.is_none() && properties.is_none() {
|
||||||
|
return Err(ErrMode::Cut(
|
||||||
|
CompilationError::fatal(at.as_source_range(), format!("Unexpected token: {}", at.value)).into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let value = NonCodeValue::Annotation { name, properties };
|
let value = NonCodeValue::Annotation { name, properties };
|
||||||
Ok(Node::new(
|
Ok(Node::new(
|
||||||
NonCodeNode { value, digest: None },
|
NonCodeNode { value, digest: None },
|
||||||
@ -420,6 +427,7 @@ fn pipe_expression(i: &mut TokenSlice) -> PResult<Node<PipeExpression>> {
|
|||||||
non_code_meta,
|
non_code_meta,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,7 +499,7 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult<Node<Litera
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if token.numeric_suffix().is_some() {
|
if token.numeric_suffix().is_some() {
|
||||||
ParseContext::err(CompilationError::err(
|
ParseContext::warn(CompilationError::err(
|
||||||
(&token).into(),
|
(&token).into(),
|
||||||
"Unit of Measure suffixes are experimental and currently do nothing.",
|
"Unit of Measure suffixes are experimental and currently do nothing.",
|
||||||
));
|
));
|
||||||
@ -822,6 +830,7 @@ fn object_property_same_key_and_val(i: &mut TokenSlice) -> PResult<Node<ObjectPr
|
|||||||
key,
|
key,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -850,6 +859,7 @@ fn object_property(i: &mut TokenSlice) -> PResult<Node<ObjectProperty>> {
|
|||||||
value: expr,
|
value: expr,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if sep.token_type == TokenType::Colon {
|
if sep.token_type == TokenType::Colon {
|
||||||
@ -1117,9 +1127,25 @@ fn function_decl(i: &mut TokenSlice) -> PResult<(Node<FunctionExpression>, bool)
|
|||||||
// Optional return type.
|
// Optional return type.
|
||||||
let return_type = opt(return_type).parse_next(i)?;
|
let return_type = opt(return_type).parse_next(i)?;
|
||||||
ignore_whitespace(i);
|
ignore_whitespace(i);
|
||||||
open_brace(i)?;
|
let brace = open_brace(i)?;
|
||||||
let body = function_body(i)?;
|
let close: Option<(Vec<Vec<Token>>, Token)> = opt((repeat(0.., whitespace), close_brace)).parse_next(i)?;
|
||||||
let end = close_brace(i)?.end;
|
let (body, end) = match close {
|
||||||
|
Some((_, end)) => (
|
||||||
|
Node::new(
|
||||||
|
Program {
|
||||||
|
body: Vec::new(),
|
||||||
|
non_code_meta: NonCodeMeta::default(),
|
||||||
|
shebang: None,
|
||||||
|
digest: None,
|
||||||
|
},
|
||||||
|
brace.end,
|
||||||
|
brace.end,
|
||||||
|
brace.module_id,
|
||||||
|
),
|
||||||
|
end.end,
|
||||||
|
),
|
||||||
|
None => (function_body(i)?, close_brace(i)?.end),
|
||||||
|
};
|
||||||
let result = Node::new(
|
let result = Node::new(
|
||||||
FunctionExpression {
|
FunctionExpression {
|
||||||
params,
|
params,
|
||||||
@ -1587,6 +1613,14 @@ fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
|
|||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
));
|
));
|
||||||
|
} else if matches!(path, ImportPath::Std { .. }) && matches!(selector, ImportSelector::None { .. }) {
|
||||||
|
return Err(ErrMode::Cut(
|
||||||
|
CompilationError::fatal(
|
||||||
|
SourceRange::new(start, end, module_id),
|
||||||
|
"the standard library cannot be imported as a part",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Node::boxed(
|
Ok(Node::boxed(
|
||||||
@ -1639,13 +1673,34 @@ fn validate_path_string(path_string: String, var_name: bool, path_range: SourceR
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImportPath::Kcl { filename: path_string }
|
ImportPath::Kcl { filename: path_string }
|
||||||
} else if path_string.starts_with("std") {
|
} else if path_string.starts_with("std::") {
|
||||||
ParseContext::warn(CompilationError::err(
|
ParseContext::warn(CompilationError::err(
|
||||||
path_range,
|
path_range,
|
||||||
"explicit imports from the standard library are experimental, likely to be buggy, and likely to change.",
|
"explicit imports from the standard library are experimental, likely to be buggy, and likely to change.",
|
||||||
));
|
));
|
||||||
|
|
||||||
ImportPath::Std
|
let segments: Vec<String> = path_string.split("::").map(str::to_owned).collect();
|
||||||
|
|
||||||
|
for s in &segments {
|
||||||
|
if s.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') || s.starts_with('_') {
|
||||||
|
return Err(ErrMode::Cut(
|
||||||
|
CompilationError::fatal(path_range, "invalid path in import statement.").into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now we only support importing from singly-nested modules inside std.
|
||||||
|
if segments.len() != 2 {
|
||||||
|
return Err(ErrMode::Cut(
|
||||||
|
CompilationError::fatal(
|
||||||
|
path_range,
|
||||||
|
format!("Invalid import path for import from std: {}.", path_string),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportPath::Std { path: segments }
|
||||||
} else if path_string.contains('.') {
|
} else if path_string.contains('.') {
|
||||||
let extn = &path_string[path_string.rfind('.').unwrap() + 1..];
|
let extn = &path_string[path_string.rfind('.').unwrap() + 1..];
|
||||||
if !FOREIGN_IMPORT_EXTENSIONS.contains(&extn) {
|
if !FOREIGN_IMPORT_EXTENSIONS.contains(&extn) {
|
||||||
@ -1735,6 +1790,7 @@ fn return_stmt(i: &mut TokenSlice) -> PResult<Node<ReturnStatement>> {
|
|||||||
end: argument.end(),
|
end: argument.end(),
|
||||||
module_id: ret.module_id,
|
module_id: ret.module_id,
|
||||||
inner: ReturnStatement { argument, digest: None },
|
inner: ReturnStatement { argument, digest: None },
|
||||||
|
trivia: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1961,11 +2017,13 @@ fn declaration(i: &mut TokenSlice) -> PResult<BoxNode<VariableDeclaration>> {
|
|||||||
init: val,
|
init: val,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
kind,
|
kind,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2171,6 +2229,7 @@ fn unary_expression(i: &mut TokenSlice) -> PResult<Node<UnaryExpression>> {
|
|||||||
argument,
|
argument,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2251,6 +2310,7 @@ fn expression_stmt(i: &mut TokenSlice) -> PResult<Node<ExpressionStatement>> {
|
|||||||
expression: val,
|
expression: val,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2433,11 +2493,19 @@ fn argument_type(i: &mut TokenSlice) -> PResult<FnArgType> {
|
|||||||
// TODO it is buggy to treat object fields like parameters since the parameters parser assumes a terminating `)`.
|
// TODO it is buggy to treat object fields like parameters since the parameters parser assumes a terminating `)`.
|
||||||
(open_brace, parameters, close_brace).map(|(_, params, _)| Ok(FnArgType::Object { properties: params })),
|
(open_brace, parameters, close_brace).map(|(_, params, _)| Ok(FnArgType::Object { properties: params })),
|
||||||
// Array types
|
// Array types
|
||||||
(one_of(TokenType::Type), open_bracket, close_bracket).map(|(token, _, _)| {
|
(
|
||||||
FnArgPrimitive::from_str(&token.value)
|
one_of(TokenType::Type),
|
||||||
.map(FnArgType::Array)
|
opt(delimited(open_paren, uom_for_type, close_paren)),
|
||||||
.map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err)))
|
open_bracket,
|
||||||
}),
|
close_bracket,
|
||||||
|
)
|
||||||
|
.map(|(token, uom, _, _)| {
|
||||||
|
FnArgPrimitive::from_str(&token.value, uom)
|
||||||
|
.map(FnArgType::Array)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", token.value))
|
||||||
|
})
|
||||||
|
}),
|
||||||
// Primitive types
|
// Primitive types
|
||||||
(
|
(
|
||||||
one_of(TokenType::Type),
|
one_of(TokenType::Type),
|
||||||
@ -2445,14 +2513,16 @@ fn argument_type(i: &mut TokenSlice) -> PResult<FnArgType> {
|
|||||||
)
|
)
|
||||||
.map(|(token, suffix)| {
|
.map(|(token, suffix)| {
|
||||||
if suffix.is_some() {
|
if suffix.is_some() {
|
||||||
ParseContext::err(CompilationError::err(
|
ParseContext::warn(CompilationError::err(
|
||||||
(&token).into(),
|
(&token).into(),
|
||||||
"Unit of Measure types are experimental and currently do nothing.",
|
"Unit of Measure types are experimental and currently do nothing.",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
FnArgPrimitive::from_str(&token.value)
|
FnArgPrimitive::from_str(&token.value, suffix)
|
||||||
.map(FnArgType::Primitive)
|
.map(FnArgType::Primitive)
|
||||||
.map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err)))
|
.ok_or_else(|| {
|
||||||
|
CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", token.value))
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
.parse_next(i)?
|
.parse_next(i)?
|
||||||
@ -2516,8 +2586,7 @@ fn parameters(i: &mut TokenSlice) -> PResult<Vec<Parameter>> {
|
|||||||
type_,
|
type_,
|
||||||
default_value,
|
default_value,
|
||||||
}| {
|
}| {
|
||||||
let identifier =
|
let identifier = Node::<Identifier>::try_from(arg_name)?;
|
||||||
Node::<Identifier>::try_from(arg_name).and_then(Node::<Identifier>::into_valid_binding_name)?;
|
|
||||||
|
|
||||||
Ok(Parameter {
|
Ok(Parameter {
|
||||||
identifier,
|
identifier,
|
||||||
@ -2564,24 +2633,9 @@ fn optional_after_required(params: &[Parameter]) -> Result<(), CompilationError>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node<Identifier> {
|
|
||||||
fn into_valid_binding_name(self) -> Result<Node<Identifier>, CompilationError> {
|
|
||||||
// Make sure they are not assigning a variable to a stdlib function.
|
|
||||||
if crate::std::name_in_stdlib(&self.name) {
|
|
||||||
return Err(CompilationError::fatal(
|
|
||||||
SourceRange::from(&self),
|
|
||||||
format!("Cannot assign a variable to a reserved keyword: {}", self.name),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Introduce a new name, which binds some value.
|
/// Introduce a new name, which binds some value.
|
||||||
fn binding_name(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
|
fn binding_name(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
|
||||||
identifier
|
identifier
|
||||||
.context(expected("an identifier, which will be the name of some value"))
|
|
||||||
.try_map(Node::<Identifier>::into_valid_binding_name)
|
|
||||||
.context(expected("an identifier, which will be the name of some value"))
|
.context(expected("an identifier, which will be the name of some value"))
|
||||||
.parse_next(i)
|
.parse_next(i)
|
||||||
}
|
}
|
||||||
@ -2696,6 +2750,7 @@ fn fn_call(i: &mut TokenSlice) -> PResult<Node<CallExpression>> {
|
|||||||
arguments: args,
|
arguments: args,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2725,6 +2780,7 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
|
|||||||
arguments: args,
|
arguments: args,
|
||||||
digest: None,
|
digest: None,
|
||||||
},
|
},
|
||||||
|
trivia: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4018,17 +4074,6 @@ e
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_error_stdlib_in_fn_name() {
|
|
||||||
assert_err(
|
|
||||||
r#"fn cos = () => {
|
|
||||||
return 1
|
|
||||||
}"#,
|
|
||||||
"Cannot assign a variable to a reserved keyword: cos",
|
|
||||||
[3, 6],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_error_keyword_in_fn_args() {
|
fn test_error_keyword_in_fn_args() {
|
||||||
assert_err(
|
assert_err(
|
||||||
@ -4040,17 +4085,6 @@ e
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_error_stdlib_in_fn_args() {
|
|
||||||
assert_err(
|
|
||||||
r#"fn thing = (cos) => {
|
|
||||||
return 1
|
|
||||||
}"#,
|
|
||||||
"Cannot assign a variable to a reserved keyword: cos",
|
|
||||||
[12, 15],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bad_imports() {
|
fn bad_imports() {
|
||||||
assert_err(
|
assert_err(
|
||||||
@ -4095,6 +4129,27 @@ e
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn std_fn_decl() {
|
||||||
|
let code = r#"/// Compute the cosine of a number (in radians).
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// exampleSketch = startSketchOn("XZ")
|
||||||
|
/// |> startProfileAt([0, 0], %)
|
||||||
|
/// |> angledLine({
|
||||||
|
/// angle = 30,
|
||||||
|
/// length = 3 / cos(toRadians(30)),
|
||||||
|
/// }, %)
|
||||||
|
/// |> yLineTo(0, %)
|
||||||
|
/// |> close(%)
|
||||||
|
///
|
||||||
|
/// example = extrude(5, exampleSketch)
|
||||||
|
/// ```
|
||||||
|
@(impl = std_rust)
|
||||||
|
export fn cos(num: number(rad)): number(_) {}"#;
|
||||||
|
let _ast = crate::parsing::top_level_parse(code).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn warn_import() {
|
fn warn_import() {
|
||||||
let some_program_string = r#"import "foo.kcl""#;
|
let some_program_string = r#"import "foo.kcl""#;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parsing/parser.rs
|
source: kcl/src/parsing/parser.rs
|
||||||
assertion_line: 4521
|
|
||||||
expression: actual
|
expression: actual
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"body": [
|
"body": [
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parsing/parser.rs
|
source: kcl/src/parsing/parser.rs
|
||||||
assertion_line: 4522
|
|
||||||
expression: actual
|
expression: actual
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"body": [
|
"body": [
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parsing/parser.rs
|
source: kcl/src/parsing/parser.rs
|
||||||
assertion_line: 4523
|
|
||||||
expression: actual
|
expression: actual
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"body": [
|
"body": [
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parsing/parser.rs
|
source: kcl/src/parsing/parser.rs
|
||||||
assertion_line: 4524
|
|
||||||
expression: actual
|
expression: actual
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"body": [
|
"body": [
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parsing/parser.rs
|
source: kcl/src/parsing/parser.rs
|
||||||
assertion_line: 4525
|
|
||||||
expression: actual
|
expression: actual
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"body": [
|
"body": [
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parsing/parser.rs
|
source: kcl/src/parsing/parser.rs
|
||||||
expression: actual
|
expression: actual
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"body": [
|
"body": [
|
||||||
|
@ -47,10 +47,6 @@ expression: actual
|
|||||||
"start": 7,
|
"start": 7,
|
||||||
"type": "Identifier"
|
"type": "Identifier"
|
||||||
},
|
},
|
||||||
"type_": {
|
|
||||||
"type": "Primitive",
|
|
||||||
"type": "Number"
|
|
||||||
},
|
|
||||||
"default_value": {
|
"default_value": {
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parsing/parser.rs
|
source: kcl/src/parsing/parser.rs
|
||||||
expression: actual
|
expression: actual
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"body": [
|
"body": [
|
||||||
|
@ -18,8 +18,8 @@ use winnow::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
errors::KclError,
|
errors::KclError,
|
||||||
parsing::ast::types::{ItemVisibility, VariableKind},
|
parsing::ast::types::{ItemVisibility, VariableKind},
|
||||||
source_range::{ModuleId, SourceRange},
|
source_range::SourceRange,
|
||||||
CompilationError,
|
CompilationError, ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod tokeniser;
|
mod tokeniser;
|
||||||
@ -54,6 +54,21 @@ impl NumericSuffix {
|
|||||||
pub fn is_some(self) -> bool {
|
pub fn is_some(self) -> bool {
|
||||||
self != Self::None
|
self != Self::None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn digestable_id(&self) -> &[u8] {
|
||||||
|
match self {
|
||||||
|
NumericSuffix::None => &[],
|
||||||
|
NumericSuffix::Count => b"_",
|
||||||
|
NumericSuffix::Mm => b"mm",
|
||||||
|
NumericSuffix::Cm => b"cm",
|
||||||
|
NumericSuffix::M => b"m",
|
||||||
|
NumericSuffix::Inch => b"in",
|
||||||
|
NumericSuffix::Ft => b"ft",
|
||||||
|
NumericSuffix::Yd => b"yd",
|
||||||
|
NumericSuffix::Deg => b"deg",
|
||||||
|
NumericSuffix::Rad => b"rad",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for NumericSuffix {
|
impl FromStr for NumericSuffix {
|
||||||
|
@ -13,7 +13,7 @@ use winnow::{
|
|||||||
use super::TokenStream;
|
use super::TokenStream;
|
||||||
use crate::{
|
use crate::{
|
||||||
parsing::token::{Token, TokenType},
|
parsing::token::{Token, TokenType},
|
||||||
source_range::ModuleId,
|
ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -7,7 +7,7 @@ use crate::{
|
|||||||
exec::ArtifactCommand,
|
exec::ArtifactCommand,
|
||||||
execution::{ArtifactGraph, Operation},
|
execution::{ArtifactGraph, Operation},
|
||||||
parsing::ast::types::{Node, Program},
|
parsing::ast::types::{Node, Program},
|
||||||
source_range::ModuleId,
|
ModuleId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Deserialize the data from a snapshot.
|
/// Deserialize the data from a snapshot.
|
||||||
@ -1943,3 +1943,24 @@ mod array_elem_pop_fail {
|
|||||||
super::execute(TEST_NAME, false).await
|
super::execute(TEST_NAME, false).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mod helix_simple {
|
||||||
|
const TEST_NAME: &str = "helix_simple";
|
||||||
|
|
||||||
|
/// Test parsing KCL.
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
super::parse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||||
|
#[test]
|
||||||
|
fn unparse() {
|
||||||
|
super::unparse(TEST_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that KCL is executed correctly.
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn kcl_test_execute() {
|
||||||
|
super::execute(TEST_NAME, true).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,26 +2,7 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
|
||||||
|
|
||||||
/// Identifier of a source file. Uses a u32 to keep the size small.
|
use crate::modules::ModuleId;
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct ModuleId(u32);
|
|
||||||
|
|
||||||
impl ModuleId {
|
|
||||||
pub fn from_usize(id: usize) -> Self {
|
|
||||||
Self(u32::try_from(id).expect("module ID should fit in a u32"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_usize(&self) -> usize {
|
|
||||||
usize::try_from(self.0).expect("module ID should fit in a usize")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Top-level file is the one being executed.
|
|
||||||
/// Represented by module ID of 0, i.e. the default value.
|
|
||||||
pub fn is_top_level(&self) -> bool {
|
|
||||||
*self == Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The first two items are the start and end points (byte offsets from the start of the file).
|
/// The first two items are the start and end points (byte offsets from the start of the file).
|
||||||
/// The third item is whether the source range belongs to the 'main' file, i.e., the file currently
|
/// The third item is whether the source range belongs to the 'main' file, i.e., the file currently
|
||||||
|
@ -24,7 +24,7 @@ lazy_static::lazy_static! {
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Validate)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppearanceData {
|
struct AppearanceData {
|
||||||
/// Color of the new material, a hex string like "#ff0000".
|
/// Color of the new material, a hex string like "#ff0000".
|
||||||
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
|
#[schemars(regex(pattern = "#[0-9a-fA-F]{6}"))]
|
||||||
pub color: String,
|
pub color: String,
|
||||||
@ -39,7 +39,16 @@ pub struct AppearanceData {
|
|||||||
|
|
||||||
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
|
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
|
||||||
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (data, solid_set): (AppearanceData, SolidSet) = args.get_data_and_solid_set()?;
|
let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
|
||||||
|
|
||||||
|
let color: String = args.get_kw_arg("color")?;
|
||||||
|
let metalness: Option<f64> = args.get_kw_arg_opt("metalness")?;
|
||||||
|
let roughness: Option<f64> = args.get_kw_arg_opt("roughness")?;
|
||||||
|
let data = AppearanceData {
|
||||||
|
color,
|
||||||
|
metalness,
|
||||||
|
roughness,
|
||||||
|
};
|
||||||
|
|
||||||
// Validate the data.
|
// Validate the data.
|
||||||
data.validate().map_err(|err| {
|
data.validate().map_err(|err| {
|
||||||
@ -57,7 +66,7 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = inner_appearance(data, solid_set, args).await?;
|
let result = inner_appearance(solid_set, data.color, data.metalness, data.roughness, args).await?;
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +83,8 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// |> close()
|
/// |> close()
|
||||||
///
|
///
|
||||||
/// example = extrude(exampleSketch, length = 5)
|
/// example = extrude(exampleSketch, length = 5)
|
||||||
/// |> appearance({color= '#ff0000', metalness= 50, roughness= 50}, %)
|
/// // There are other options besides 'color', but they're optional.
|
||||||
|
/// |> appearance(color='#ff0000')
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -82,11 +92,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// sketch001 = startSketchOn('XY')
|
/// sketch001 = startSketchOn('XY')
|
||||||
/// |> circle({ center = [15, 0], radius = 5 }, %)
|
/// |> circle({ center = [15, 0], radius = 5 }, %)
|
||||||
/// |> revolve({ angle = 360, axis = 'y' }, %)
|
/// |> revolve({ angle = 360, axis = 'y' }, %)
|
||||||
/// |> appearance({
|
/// |> appearance(
|
||||||
/// color = '#ff0000',
|
/// color = '#ff0000',
|
||||||
/// metalness = 90,
|
/// metalness = 90,
|
||||||
/// roughness = 90
|
/// roughness = 90
|
||||||
/// }, %)
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -105,8 +115,8 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// example1 = cube([20, 0])
|
/// example1 = cube([20, 0])
|
||||||
/// example2 = cube([40, 0])
|
/// example2 = cube([40, 0])
|
||||||
///
|
///
|
||||||
/// appearance({color= '#ff0000', metalness= 50, roughness= 50}, [example0, example1])
|
/// appearance([example0, example1], color='#ff0000', metalness=50, roughness=50)
|
||||||
/// appearance({color= '#00ff00', metalness= 50, roughness= 50}, example2)
|
/// appearance(example2, color='#00ff00', metalness=50, roughness=50)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -125,11 +135,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// faces = ['end'],
|
/// faces = ['end'],
|
||||||
/// thickness = 0.25,
|
/// thickness = 0.25,
|
||||||
/// )
|
/// )
|
||||||
/// |> appearance({
|
/// |> appearance(
|
||||||
/// color = '#ff0000',
|
/// color = '#ff0000',
|
||||||
/// metalness = 90,
|
/// metalness = 90,
|
||||||
/// roughness = 90
|
/// roughness = 90
|
||||||
/// }, %)
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -142,11 +152,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// |> line(end = [-24, 0])
|
/// |> line(end = [-24, 0])
|
||||||
/// |> close()
|
/// |> close()
|
||||||
/// |> extrude(length = 6)
|
/// |> extrude(length = 6)
|
||||||
/// |> appearance({
|
/// |> appearance(
|
||||||
/// color = '#ff0000',
|
/// color = '#ff0000',
|
||||||
/// metalness = 90,
|
/// metalness = 90,
|
||||||
/// roughness = 90
|
/// roughness = 90
|
||||||
/// }, %)
|
/// )
|
||||||
///
|
///
|
||||||
/// shell(
|
/// shell(
|
||||||
/// firstSketch,
|
/// firstSketch,
|
||||||
@ -166,11 +176,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// |> close()
|
/// |> close()
|
||||||
///
|
///
|
||||||
/// example = extrude(exampleSketch, length = 1)
|
/// example = extrude(exampleSketch, length = 1)
|
||||||
/// |> appearance({
|
/// |> appearance(
|
||||||
/// color = '#ff0000',
|
/// color = '#ff0000',
|
||||||
/// metalness = 90,
|
/// metalness = 90,
|
||||||
/// roughness = 90
|
/// roughness = 90
|
||||||
/// }, %)
|
/// )
|
||||||
/// |> patternLinear3d({
|
/// |> patternLinear3d({
|
||||||
/// axis = [1, 0, 1],
|
/// axis = [1, 0, 1],
|
||||||
/// instances = 7,
|
/// instances = 7,
|
||||||
@ -194,11 +204,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// instances = 7,
|
/// instances = 7,
|
||||||
/// distance = 6
|
/// distance = 6
|
||||||
/// }, %)
|
/// }, %)
|
||||||
/// |> appearance({
|
/// |> appearance(
|
||||||
/// color = '#ff0000',
|
/// color = '#ff0000',
|
||||||
/// metalness = 90,
|
/// metalness = 90,
|
||||||
/// roughness = 90
|
/// roughness = 90
|
||||||
/// }, %)
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -217,11 +227,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// }, %)
|
/// }, %)
|
||||||
///
|
///
|
||||||
/// example = extrude(exampleSketch, length = 1)
|
/// example = extrude(exampleSketch, length = 1)
|
||||||
/// |> appearance({
|
/// |> appearance(
|
||||||
/// color = '#ff0000',
|
/// color = '#ff0000',
|
||||||
/// metalness = 90,
|
/// metalness = 90,
|
||||||
/// roughness = 90
|
/// roughness = 90
|
||||||
/// }, %)
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -254,26 +264,38 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
|||||||
/// radius = 2,
|
/// radius = 2,
|
||||||
/// }, %)
|
/// }, %)
|
||||||
/// |> hole(pipeHole, %)
|
/// |> hole(pipeHole, %)
|
||||||
/// |> sweep({
|
/// |> sweep(path = sweepPath)
|
||||||
/// path: sweepPath,
|
/// |> appearance(
|
||||||
/// }, %)
|
/// color = "#ff0000",
|
||||||
/// |> appearance({
|
/// metalness = 50,
|
||||||
/// color: "#ff0000",
|
/// roughness = 50
|
||||||
/// metalness: 50,
|
/// )
|
||||||
/// roughness: 50
|
|
||||||
/// }, %)
|
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "appearance",
|
name = "appearance",
|
||||||
|
keywords = true,
|
||||||
|
unlabeled_first = true,
|
||||||
|
args = {
|
||||||
|
solid_set = { docs = "The solid(s) whose appearance is being set" },
|
||||||
|
color = { docs = "Color of the new material, a hex string like '#ff0000'"},
|
||||||
|
metalness = { docs = "Metalness of the new material, a percentage like 95.7." },
|
||||||
|
roughness = { docs = "Roughness of the new material, a percentage like 95.7." },
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args) -> Result<SolidSet, KclError> {
|
async fn inner_appearance(
|
||||||
|
solid_set: SolidSet,
|
||||||
|
color: String,
|
||||||
|
metalness: Option<f64>,
|
||||||
|
roughness: Option<f64>,
|
||||||
|
args: Args,
|
||||||
|
) -> Result<SolidSet, KclError> {
|
||||||
let solids: Vec<Box<Solid>> = solid_set.into();
|
let solids: Vec<Box<Solid>> = solid_set.into();
|
||||||
|
|
||||||
for solid in &solids {
|
for solid in &solids {
|
||||||
// Set the material properties.
|
// Set the material properties.
|
||||||
let rgb = rgba_simple::RGB::<f32>::from_hex(&data.color).map_err(|err| {
|
let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("Invalid hex color (`{}`): {}", data.color, err),
|
message: format!("Invalid hex color (`{color}`): {err}"),
|
||||||
source_ranges: vec![args.source_range],
|
source_ranges: vec![args.source_range],
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
@ -290,8 +312,8 @@ async fn inner_appearance(data: AppearanceData, solid_set: SolidSet, args: Args)
|
|||||||
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
|
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
|
||||||
object_id: solid.id,
|
object_id: solid.id,
|
||||||
color,
|
color,
|
||||||
metalness: data.metalness.unwrap_or_default() as f32 / 100.0,
|
metalness: metalness.unwrap_or_default() as f32 / 100.0,
|
||||||
roughness: data.roughness.unwrap_or_default() as f32 / 100.0,
|
roughness: roughness.unwrap_or_default() as f32 / 100.0,
|
||||||
ambient_occlusion: 0.0,
|
ambient_occlusion: 0.0,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -1042,34 +1042,6 @@ impl<'a> FromKclValue<'a> for super::fillet::FilletData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FromKclValue<'a> for super::sweep::SweepData {
|
|
||||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
|
||||||
let obj = arg.as_object()?;
|
|
||||||
let_field_of!(obj, path);
|
|
||||||
let_field_of!(obj, sectional?);
|
|
||||||
let_field_of!(obj, tolerance?);
|
|
||||||
Some(Self {
|
|
||||||
path,
|
|
||||||
sectional,
|
|
||||||
tolerance,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> FromKclValue<'a> for super::appearance::AppearanceData {
|
|
||||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
|
||||||
let obj = arg.as_object()?;
|
|
||||||
let_field_of!(obj, color);
|
|
||||||
let_field_of!(obj, metalness?);
|
|
||||||
let_field_of!(obj, roughness?);
|
|
||||||
Some(Self {
|
|
||||||
color,
|
|
||||||
metalness,
|
|
||||||
roughness,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> FromKclValue<'a> for super::helix::HelixRevolutionsData {
|
impl<'a> FromKclValue<'a> for super::helix::HelixRevolutionsData {
|
||||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||||
let obj = arg.as_object()?;
|
let obj = arg.as_object()?;
|
||||||
|
@ -43,7 +43,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
/// // Create a spring by sweeping around the helix path.
|
/// // Create a spring by sweeping around the helix path.
|
||||||
/// springSketch = startSketchOn('YZ')
|
/// springSketch = startSketchOn('YZ')
|
||||||
/// |> circle({ center = [0, 0], radius = 0.5 }, %)
|
/// |> circle({ center = [0, 0], radius = 0.5 }, %)
|
||||||
/// |> sweep({ path = helixPath }, %)
|
/// |> sweep(path = helixPath)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -64,7 +64,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
/// // Create a spring by sweeping around the helix path.
|
/// // Create a spring by sweeping around the helix path.
|
||||||
/// springSketch = startSketchOn('XY')
|
/// springSketch = startSketchOn('XY')
|
||||||
/// |> circle({ center = [0, 0], radius = 0.5 }, %)
|
/// |> circle({ center = [0, 0], radius = 0.5 }, %)
|
||||||
/// |> sweep({ path = helixPath }, %)
|
/// |> sweep(path = helixPath)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -86,7 +86,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
/// // Create a spring by sweeping around the helix path.
|
/// // Create a spring by sweeping around the helix path.
|
||||||
/// springSketch = startSketchOn('XY')
|
/// springSketch = startSketchOn('XY')
|
||||||
/// |> circle({ center = [0, 0], radius = 1 }, %)
|
/// |> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
/// |> sweep({ path = helixPath }, %)
|
/// |> sweep(path = helixPath)
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "helix",
|
name = "helix",
|
||||||
|
@ -22,24 +22,14 @@ pub enum SweepPath {
|
|||||||
Helix(Box<Helix>),
|
Helix(Box<Helix>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data for a sweep.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
|
||||||
#[ts(export)]
|
|
||||||
pub struct SweepData {
|
|
||||||
/// The path to sweep along.
|
|
||||||
pub path: SweepPath,
|
|
||||||
/// If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components.
|
|
||||||
pub sectional: Option<bool>,
|
|
||||||
/// Tolerance for the sweep operation.
|
|
||||||
#[serde(default)]
|
|
||||||
pub tolerance: Option<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extrude a sketch along a path.
|
/// Extrude a sketch along a path.
|
||||||
pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (data, sketch): (SweepData, Sketch) = args.get_data_and_sketch()?;
|
let sketch = args.get_unlabeled_kw_arg("sketch")?;
|
||||||
|
let path: SweepPath = args.get_kw_arg("path")?;
|
||||||
|
let sectional = args.get_kw_arg_opt("sectional")?;
|
||||||
|
let tolerance = args.get_kw_arg_opt("tolerance")?;
|
||||||
|
|
||||||
let value = inner_sweep(data, sketch, exec_state, args).await?;
|
let value = inner_sweep(sketch, path, sectional, tolerance, exec_state, args).await?;
|
||||||
Ok(KclValue::Solid { value })
|
Ok(KclValue::Solid { value })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,9 +72,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
/// radius = 2,
|
/// radius = 2,
|
||||||
/// }, %)
|
/// }, %)
|
||||||
/// |> hole(pipeHole, %)
|
/// |> hole(pipeHole, %)
|
||||||
/// |> sweep({
|
/// |> sweep(path = sweepPath)
|
||||||
/// path: sweepPath,
|
|
||||||
/// }, %)
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -104,15 +92,25 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
|||||||
/// // Create a spring by sweeping around the helix path.
|
/// // Create a spring by sweeping around the helix path.
|
||||||
/// springSketch = startSketchOn('YZ')
|
/// springSketch = startSketchOn('YZ')
|
||||||
/// |> circle({ center = [0, 0], radius = 1 }, %)
|
/// |> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
/// |> sweep({ path = helixPath }, %)
|
/// |> sweep(path = helixPath)
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "sweep",
|
name = "sweep",
|
||||||
feature_tree_operation = true,
|
feature_tree_operation = true,
|
||||||
|
keywords = true,
|
||||||
|
unlabeled_first = true,
|
||||||
|
args = {
|
||||||
|
sketch = { docs = "The sketch that should be swept in space" },
|
||||||
|
path = { docs = "The path to sweep the sketch along" },
|
||||||
|
sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
|
||||||
|
tolerance = { docs = "Tolerance for this operation" },
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
async fn inner_sweep(
|
async fn inner_sweep(
|
||||||
data: SweepData,
|
|
||||||
sketch: Sketch,
|
sketch: Sketch,
|
||||||
|
path: SweepPath,
|
||||||
|
sectional: Option<bool>,
|
||||||
|
tolerance: Option<f64>,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
args: Args,
|
args: Args,
|
||||||
) -> Result<Box<Solid>, KclError> {
|
) -> Result<Box<Solid>, KclError> {
|
||||||
@ -121,12 +119,12 @@ async fn inner_sweep(
|
|||||||
id,
|
id,
|
||||||
ModelingCmd::from(mcmd::Sweep {
|
ModelingCmd::from(mcmd::Sweep {
|
||||||
target: sketch.id.into(),
|
target: sketch.id.into(),
|
||||||
trajectory: match data.path {
|
trajectory: match path {
|
||||||
SweepPath::Sketch(sketch) => sketch.id.into(),
|
SweepPath::Sketch(sketch) => sketch.id.into(),
|
||||||
SweepPath::Helix(helix) => helix.value.into(),
|
SweepPath::Helix(helix) => helix.value.into(),
|
||||||
},
|
},
|
||||||
sectional: data.sectional.unwrap_or(false),
|
sectional: sectional.unwrap_or(false),
|
||||||
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
|
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -161,7 +161,9 @@ impl Node<NonCodeNode> {
|
|||||||
NonCodeValue::NewLine => "\n\n".to_string(),
|
NonCodeValue::NewLine => "\n\n".to_string(),
|
||||||
NonCodeValue::Annotation { name, properties } => {
|
NonCodeValue::Annotation { name, properties } => {
|
||||||
let mut result = "@".to_owned();
|
let mut result = "@".to_owned();
|
||||||
result.push_str(&name.name);
|
if let Some(name) = name {
|
||||||
|
result.push_str(&name.name);
|
||||||
|
}
|
||||||
if let Some(properties) = properties {
|
if let Some(properties) = properties {
|
||||||
result.push('(');
|
result.push('(');
|
||||||
result.push_str(
|
result.push_str(
|
||||||
@ -793,7 +795,7 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{parsing::ast::types::FormatOptions, source_range::ModuleId};
|
use crate::{parsing::ast::types::FormatOptions, ModuleId};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_recast_if_else_if_same() {
|
fn test_recast_if_else_if_same() {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/simulation_tests.rs
|
source: kcl/src/simulation_tests.rs
|
||||||
description: Program memory after executing angled_line.kcl
|
description: Program memory after executing angled_line.kcl
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
"environments": [
|
"environments": [
|
||||||
|