Compare commits
30 Commits
kurt-delet
...
nightly-v2
Author | SHA1 | Date | |
---|---|---|---|
0e9d37c0a0 | |||
9c18060d73 | |||
e2be66b024 | |||
1bb372b642 | |||
627fbda671 | |||
74bdb72edc | |||
a9182bf357 | |||
5f14023404 | |||
9a3ac64603 | |||
67e60cb832 | |||
110037df79 | |||
30b1dae38a | |||
f20fc5b467 | |||
1bfc3a0a3c | |||
f6e975db84 | |||
b82eec85fd | |||
5dc4213295 | |||
61807e7629 | |||
357bbffce5 | |||
6ac9c49773 | |||
020497cde2 | |||
688852a5df | |||
6c635bd70d | |||
1c0a38a1e2 | |||
019cb815f9 | |||
c8653beae7 | |||
9ea3cb51ba | |||
e0de0493ab | |||
11cac0c30e | |||
4de50edf5a |
1
.github/workflows/e2e-tests.yml
vendored
@ -3,7 +3,6 @@ on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
|
@ -9,7 +9,7 @@ Repeat a 2-dimensional sketch along some dimension, with a dynamic amount
|
||||
of distance between each repetition, some specified number of times.
|
||||
|
||||
```js
|
||||
patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet) -> [Sketch]
|
||||
patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet, use_original?: bool) -> [Sketch]
|
||||
```
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ patternLinear2d(data: LinearPattern2dData, sketch_set: SketchSet) -> [Sketch]
|
||||
|----------|------|-------------|----------|
|
||||
| `data` | [`LinearPattern2dData`](/docs/kcl/types/LinearPattern2dData) | Data for a linear pattern on a 2D sketch. | Yes |
|
||||
| `sketch_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
|
||||
| `use_original` | `bool` | | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
@ -9,7 +9,7 @@ Repeat a 3-dimensional solid along a linear path, with a dynamic amount
|
||||
of distance between each repetition, some specified number of times.
|
||||
|
||||
```js
|
||||
patternLinear3d(data: LinearPattern3dData, solid_set: SolidSet) -> [Solid]
|
||||
patternLinear3d(data: LinearPattern3dData, solid_set: SolidSet, use_original?: bool) -> [Solid]
|
||||
```
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ patternLinear3d(data: LinearPattern3dData, solid_set: SolidSet) -> [Solid]
|
||||
|----------|------|-------------|----------|
|
||||
| `data` | [`LinearPattern3dData`](/docs/kcl/types/LinearPattern3dData) | Data for a linear pattern on a 3D model. | Yes |
|
||||
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes |
|
||||
| `use_original` | `bool` | | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
@ -35,7 +35,7 @@ The transform function returns a transform object. All properties of the object
|
||||
- `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local")
|
||||
|
||||
```js
|
||||
patternTransform(total_instances: integer, transform_function: FunctionParam, solid_set: SolidSet) -> [Solid]
|
||||
patternTransform(total_instances: integer, transform_function: FunctionParam, solid_set: SolidSet, use_original?: bool) -> [Solid]
|
||||
```
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ patternTransform(total_instances: integer, transform_function: FunctionParam, so
|
||||
| `total_instances` | `integer` | | Yes |
|
||||
| `transform_function` | `FunctionParam` | | Yes |
|
||||
| `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes |
|
||||
| `use_original` | `bool` | | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
@ -9,7 +9,7 @@ Just like patternTransform, but works on 2D sketches not 3D solids.
|
||||
|
||||
|
||||
```js
|
||||
patternTransform2d(total_instances: integer, transform_function: FunctionParam, solid_set: SketchSet) -> [Sketch]
|
||||
patternTransform2d(total_instances: integer, transform_function: FunctionParam, solid_set: SketchSet, use_original?: bool) -> [Sketch]
|
||||
```
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ patternTransform2d(total_instances: integer, transform_function: FunctionParam,
|
||||
| `total_instances` | `integer` | | Yes |
|
||||
| `transform_function` | `FunctionParam` | | Yes |
|
||||
| `solid_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes |
|
||||
| `use_original` | `bool` | | No |
|
||||
|
||||
### Returns
|
||||
|
||||
|
22406
docs/kcl/std.json
@ -20,5 +20,6 @@ Data for a circular pattern on a 2D sketch.
|
||||
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
|
||||
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
|
||||
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
|
||||
| `useOriginal` |`boolean`| If the target being patterned is itself a pattern, then, should you use the original solid, or the pattern? | No |
|
||||
|
||||
|
||||
|
@ -21,5 +21,6 @@ Data for a circular pattern on a 3D model.
|
||||
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
|
||||
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
|
||||
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
|
||||
| `useOriginal` |`boolean`| If the target being patterned is itself a pattern, then, should you use the original solid, or the pattern? | No |
|
||||
|
||||
|
||||
|
@ -22,6 +22,7 @@ A sketch is a collection of paths.
|
||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No |
|
||||
| `originalId` |`string`| | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
@ -31,6 +31,7 @@ A sketch is a collection of paths.
|
||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||
| `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No |
|
||||
| `originalId` |`string`| | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
@ -20,6 +20,7 @@ export class ToolbarFixture {
|
||||
shellButton!: Locator
|
||||
revolveButton!: Locator
|
||||
offsetPlaneButton!: Locator
|
||||
helixButton!: Locator
|
||||
startSketchBtn!: Locator
|
||||
lineBtn!: Locator
|
||||
rectangleBtn!: Locator
|
||||
@ -49,6 +50,7 @@ export class ToolbarFixture {
|
||||
this.shellButton = page.getByTestId('shell')
|
||||
this.revolveButton = page.getByTestId('revolve')
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
this.helixButton = page.getByTestId('helix')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
this.lineBtn = page.getByTestId('line')
|
||||
this.rectangleBtn = page.getByTestId('corner-rectangle')
|
||||
|
@ -27,7 +27,7 @@ test.describe('Onboarding tests', () => {
|
||||
},
|
||||
cleanProjectDir: true,
|
||||
},
|
||||
async ({ context, page, homePage }) => {
|
||||
async ({ page, homePage }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
@ -68,7 +68,7 @@ test.describe('Onboarding tests', () => {
|
||||
},
|
||||
cleanProjectDir: true,
|
||||
},
|
||||
async ({ page, homePage }, testInfo) => {
|
||||
async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
const viewportSize = { width: 1200, height: 500 }
|
||||
@ -154,7 +154,7 @@ test.describe('Onboarding tests', () => {
|
||||
)
|
||||
|
||||
test(
|
||||
'Click through each onboarding step',
|
||||
'Click through each onboarding step and back',
|
||||
{
|
||||
appSettings: {
|
||||
app: {
|
||||
@ -187,15 +187,21 @@ test.describe('Onboarding tests', () => {
|
||||
).toBeVisible()
|
||||
|
||||
const nextButton = page.getByTestId('onboarding-next')
|
||||
const prevButton = page.getByTestId('onboarding-prev')
|
||||
|
||||
while ((await nextButton.innerText()) !== 'Finish') {
|
||||
await nextButton.hover()
|
||||
await nextButton.click()
|
||||
}
|
||||
|
||||
// Finish the onboarding
|
||||
await nextButton.hover()
|
||||
await nextButton.click()
|
||||
while ((await prevButton.innerText()) !== 'Dismiss') {
|
||||
await prevButton.hover()
|
||||
await prevButton.click()
|
||||
}
|
||||
|
||||
// Dismiss the onboarding
|
||||
await prevButton.hover()
|
||||
await prevButton.click()
|
||||
|
||||
// Test that the onboarding pane is gone
|
||||
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
|
||||
@ -269,7 +275,7 @@ test.describe('Onboarding tests', () => {
|
||||
cleanProjectDir: true,
|
||||
},
|
||||
|
||||
async ({ context, page, homePage }) => {
|
||||
async ({ page, homePage }) => {
|
||||
const u = await getUtils(page)
|
||||
const badCode = `// This is bad code we shouldn't see`
|
||||
|
||||
@ -336,10 +342,10 @@ test.describe('Onboarding tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// Test that the text in this step is correct
|
||||
const avatarLocator = await page
|
||||
const avatarLocator = page
|
||||
.getByTestId('user-sidebar-toggle')
|
||||
.locator('img')
|
||||
const onboardingOverlayLocator = await page
|
||||
const onboardingOverlayLocator = page
|
||||
.getByTestId('onboarding-content')
|
||||
.locator('div')
|
||||
.nth(1)
|
||||
@ -447,7 +453,7 @@ test.fixme(
|
||||
},
|
||||
cleanProjectDir: true,
|
||||
},
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
async ({ context, page }) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const routerTemplateDir = join(dir, 'router-template-slate')
|
||||
await fsp.mkdir(routerTemplateDir, { recursive: true })
|
||||
@ -486,10 +492,6 @@ test.fixme(
|
||||
})
|
||||
|
||||
await test.step('Navigate into project', async () => {
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Your Projects' })
|
||||
).toBeVisible()
|
||||
|
@ -775,6 +775,71 @@ openSketch = startSketchOn('XY')
|
||||
})
|
||||
})
|
||||
|
||||
test('Helix point-and-click', async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 620, y: 257 }
|
||||
const expectedOutput = `helix001 = helix(revolutions = 1, angleStart = 360, counterClockWise = false, radius = 5, axis = 'X', length = 5)`
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
await test.step(`Look for the red of the default plane`, async () => {
|
||||
await scene.expectPixelColor([96, 52, 52], testPoint, 15)
|
||||
})
|
||||
await test.step(`Go through the command bar flow`, async () => {
|
||||
await toolbar.helixButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'revolutions',
|
||||
currentArgValue: '1',
|
||||
headerArguments: {
|
||||
AngleStart: '',
|
||||
Axis: '',
|
||||
CounterClockWise: '',
|
||||
Length: '',
|
||||
Radius: '',
|
||||
Revolutions: '',
|
||||
},
|
||||
highlightedHeaderArg: 'revolutions',
|
||||
commandName: 'Helix',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await editor.expectEditor.toContain(expectedOutput)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [expectedOutput],
|
||||
highlightedCode: '',
|
||||
})
|
||||
// Red plane is now gone, white helix is there
|
||||
await scene.expectPixelColor([250, 250, 250], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete offset plane via feature tree selection', async () => {
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
// Red plane is back
|
||||
await scene.expectPixelColor([96, 52, 52], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
const loftPointAndClickCases = [
|
||||
{ shouldPreselect: true },
|
||||
{ shouldPreselect: false },
|
||||
@ -964,7 +1029,7 @@ sketch002 = startSketchOn('XZ')
|
||||
testPoint.x - 50,
|
||||
testPoint.y
|
||||
)
|
||||
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
|
||||
const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)'
|
||||
|
||||
await test.step(`Look for sketch001`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
@ -1890,7 +1955,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
|
||||
const testPoint = { x: 575, y: 200 }
|
||||
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const shellDeclaration =
|
||||
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
|
||||
"shell001 = shell(extrude001, faces = ['end'], thickness = 5)"
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
|
||||
@ -1990,8 +2055,7 @@ extrude001 = extrude(sketch001, length = 40)
|
||||
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
|
||||
const mutatedCode = 'xLine(-40, %, $seg01)'
|
||||
const shellDeclaration =
|
||||
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
|
||||
const formattedOutLastLine = '}, extrude001)'
|
||||
"shell001 = shell(extrude001, faces = ['end', seg01], thickness = 5)"
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||
@ -2034,7 +2098,7 @@ extrude001 = extrude(sketch001, length = 40)
|
||||
await editor.expectEditor.toContain(shellDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [formattedOutLastLine],
|
||||
activeLines: [shellDeclaration],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
|
||||
@ -2088,9 +2152,8 @@ extrude002 = extrude(sketch002, length = 50)
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 550, y: 295 }
|
||||
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${
|
||||
hasExtrudesInPipe ? 'sketch002' : 'extrude002'
|
||||
})`
|
||||
const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002'
|
||||
const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)`
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
@ -2158,7 +2221,7 @@ extrude002 = extrude(sketch002, length = 50)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(-2000, %)
|
||||
sweep001 = sweep({ path = sketch002 }, sketch001)
|
||||
sweep001 = sweep(sketch001, path = sketch002)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
|
@ -35,106 +35,108 @@ sketch003 = startSketchOn('XY')
|
||||
extrude003 = extrude(sketch003, length = 20)
|
||||
`
|
||||
|
||||
test.describe('Check the happy path, for basic changing color', () => {
|
||||
const cases = [
|
||||
{
|
||||
desc: 'User accepts change',
|
||||
shouldReject: false,
|
||||
},
|
||||
{
|
||||
desc: 'User rejects change',
|
||||
shouldReject: true,
|
||||
},
|
||||
] as const
|
||||
for (const { desc, shouldReject } of cases) {
|
||||
test(`${desc}`, async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
page,
|
||||
scene,
|
||||
}) => {
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
|
||||
test.fixme('Check the happy path, for basic changing color', () => {
|
||||
const cases = [
|
||||
{
|
||||
desc: 'User accepts change',
|
||||
shouldReject: false,
|
||||
},
|
||||
{
|
||||
desc: 'User rejects change',
|
||||
shouldReject: true,
|
||||
},
|
||||
] as const
|
||||
for (const { desc, shouldReject } of cases) {
|
||||
test(`${desc}`, async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
page,
|
||||
scene,
|
||||
}) => {
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
const greenCheckCoords = { x: 565, y: 345 }
|
||||
const body2WallCoords = { x: 609, y: 153 }
|
||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||
body1CapCoords.x,
|
||||
body1CapCoords.y
|
||||
)
|
||||
const yellow: [number, number, number] = [179, 179, 131]
|
||||
const green: [number, number, number] = [108, 152, 75]
|
||||
const notGreen: [number, number, number] = [132, 132, 132]
|
||||
const body2NotGreen: [number, number, number] = [88, 88, 88]
|
||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||
const successToast = page.getByText('Prompt to edit successful')
|
||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
||||
const rejectBtn = page.getByRole('button', { name: 'close Reject' })
|
||||
const body1CapCoords = { x: 571, y: 351 }
|
||||
const greenCheckCoords = { x: 565, y: 345 }
|
||||
const body2WallCoords = { x: 609, y: 153 }
|
||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||
body1CapCoords.x,
|
||||
body1CapCoords.y
|
||||
)
|
||||
const yellow: [number, number, number] = [179, 179, 131]
|
||||
const green: [number, number, number] = [108, 152, 75]
|
||||
const notGreen: [number, number, number] = [132, 132, 132]
|
||||
const body2NotGreen: [number, number, number] = [88, 88, 88]
|
||||
const submittingToast = page.getByText(
|
||||
'Submitting to Text-to-CAD API...'
|
||||
)
|
||||
const successToast = page.getByText('Prompt to edit successful')
|
||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
||||
const rejectBtn = page.getByRole('button', { name: 'close Reject' })
|
||||
|
||||
await test.step('wait for scene to load select body and check selection came through', async () => {
|
||||
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
||||
await clickBody1Cap()
|
||||
await scene.expectPixelColor(yellow, body1CapCoords, 20)
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
|
||||
diagnostics: [],
|
||||
await test.step('wait for scene to load select body and check selection came through', async () => {
|
||||
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
||||
await clickBody1Cap()
|
||||
await scene.expectPixelColor(yellow, body1CapCoords, 20)
|
||||
await editor.expectState({
|
||||
highlightedCode: '',
|
||||
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('fire off edit prompt', async () => {
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
// being specific about the color with a hex means asserting pixel color is more stable
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('make this neon green please, use #39FF14')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(submittingToast).toBeVisible()
|
||||
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while
|
||||
await expect(successToast).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('verify initial change', async () => {
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
|
||||
await editor.expectEditor.toContain('appearance({')
|
||||
})
|
||||
|
||||
if (!shouldReject) {
|
||||
await test.step('check accept works and can be "undo"ed', async () => {
|
||||
await acceptBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
await test.step('fire off edit prompt', async () => {
|
||||
await cmdBar.openCmdBar('promptToEdit')
|
||||
// being specific about the color with a hex means asserting pixel color is more stable
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.fill('make this neon green please, use #39FF14')
|
||||
await page.waitForTimeout(100)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(submittingToast).toBeVisible()
|
||||
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while
|
||||
await expect(successToast).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('verify initial change', async () => {
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||
await editor.expectEditor.toContain('appearance({')
|
||||
|
||||
// ctrl-z works after accepting
|
||||
await page.keyboard.down('ControlOrMeta')
|
||||
await page.keyboard.press('KeyZ')
|
||||
await page.keyboard.up('ControlOrMeta')
|
||||
await editor.expectEditor.not.toContain('appearance({')
|
||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
||||
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
|
||||
await editor.expectEditor.toContain('appearance(')
|
||||
})
|
||||
} else {
|
||||
await test.step('check reject works', async () => {
|
||||
await rejectBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
|
||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
||||
await editor.expectEditor.not.toContain('appearance({')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
if (!shouldReject) {
|
||||
await test.step('check accept works and can be "undo"ed', async () => {
|
||||
await acceptBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
|
||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
||||
await editor.expectEditor.toContain('appearance(')
|
||||
|
||||
// ctrl-z works after accepting
|
||||
await page.keyboard.down('ControlOrMeta')
|
||||
await page.keyboard.press('KeyZ')
|
||||
await page.keyboard.up('ControlOrMeta')
|
||||
await editor.expectEditor.not.toContain('appearance(')
|
||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
||||
})
|
||||
} else {
|
||||
await test.step('check reject works', async () => {
|
||||
await rejectBtn.click()
|
||||
await expect(successToast).not.toBeVisible()
|
||||
|
||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
||||
await editor.expectEditor.not.toContain('appearance(')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('bad path', { tag: ['@skipWin'] }, () => {
|
||||
test(`bad edit prompt`, async ({
|
||||
context,
|
||||
homePage,
|
||||
|
@ -253,7 +253,7 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
|>
|
||||
|
||||
example = extrude(exampleSketch, length = 5)
|
||||
shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)`
|
||||
shell(exampleSketch, faces = ['end'], thickness = 0.25)`
|
||||
)
|
||||
})
|
||||
|
||||
@ -314,6 +314,7 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
)
|
||||
|
||||
test('when engine fails export we handle the failure and alert the user', async ({
|
||||
scene,
|
||||
page,
|
||||
homePage,
|
||||
}) => {
|
||||
@ -383,10 +384,7 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// Now try exporting
|
||||
|
||||
|
@ -1187,14 +1187,12 @@ sweepSketch = startSketchOn('XY')
|
||||
angleStart = 0,
|
||||
radius = 2
|
||||
}, %)
|
||||
|> sweep({
|
||||
path = sweepPath,
|
||||
}, %)
|
||||
|> appearance({
|
||||
|> sweep(path = sweepPath)
|
||||
|> appearance(
|
||||
color = "#bb00ff",
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
}, %)
|
||||
)
|
||||
`
|
||||
)
|
||||
})
|
||||
@ -1235,14 +1233,12 @@ sweepSketch = startSketchOn('XY')
|
||||
angleStart = 0,
|
||||
radius = 2
|
||||
}, %)
|
||||
|> sweep({
|
||||
path = sweepPath,
|
||||
}, %)
|
||||
|> appearance({
|
||||
|> sweep(path = sweepPath)
|
||||
|> appearance(
|
||||
color = "#bb00ff",
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
}, %)
|
||||
)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 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: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@ -896,4 +896,53 @@ test.describe('Testing settings', () => {
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(`Change inline units setting`, async ({
|
||||
page,
|
||||
homePage,
|
||||
context,
|
||||
editor,
|
||||
}) => {
|
||||
const initialInlineUnits = 'yd'
|
||||
const editedInlineUnits = { short: 'mm', long: 'Millimeters' }
|
||||
const inlineSettingsString = (s: string) =>
|
||||
`@settings(defaultLengthUnit = ${s})`
|
||||
const unitsIndicator = page.getByRole('button', {
|
||||
name: 'Current units are:',
|
||||
})
|
||||
const unitsChangeButton = (name: string) =>
|
||||
page.getByRole('button', { name, exact: true })
|
||||
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const bracketDir = join(dir, 'project-000')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cube.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Initial units from settings`, async () => {
|
||||
await homePage.openProject('project-000')
|
||||
await expect(unitsIndicator).toHaveText('Current units are: in')
|
||||
})
|
||||
|
||||
await test.step(`Manually write inline settings`, async () => {
|
||||
await editor.openPane()
|
||||
await editor.replaceCode(
|
||||
`fn cube`,
|
||||
`${inlineSettingsString(initialInlineUnits)}
|
||||
fn cube`
|
||||
)
|
||||
await expect(unitsIndicator).toContainText(initialInlineUnits)
|
||||
})
|
||||
|
||||
await test.step(`Change units setting via lower-right control`, async () => {
|
||||
await unitsIndicator.click()
|
||||
await unitsChangeButton(editedInlineUnits.long).click()
|
||||
await expect(
|
||||
page.getByText(`Updated per-file units to ${editedInlineUnits.short}`)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -3,7 +3,7 @@ import { getUtils, createProject } from './test-utils'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
test.describe('Text-to-CAD tests', () => {
|
||||
test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
|
||||
test('basic lego happy case', async ({ page, homePage }) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
|
@ -85,7 +85,7 @@
|
||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"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/main/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)",
|
||||
"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",
|
||||
@ -158,8 +158,8 @@
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/isomorphic-fetch": "^0.0.39",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^22.7.8",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/react": "^18.3.4",
|
||||
@ -200,7 +200,7 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.19.1",
|
||||
"typescript-eslint": "^8.23.0",
|
||||
"vite": "^5.4.12",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
|
@ -59,7 +59,9 @@ UnaryOp { AddOp | BangOp }
|
||||
|
||||
ObjectProperty { PropertyName (":" | Equals) expression }
|
||||
|
||||
ArgumentList { "(" commaSep<expression> ")" }
|
||||
LabeledArgument { ArgumentLabel Equals expression }
|
||||
|
||||
ArgumentList { "(" commaSep<LabeledArgument | expression> ")" }
|
||||
|
||||
type[@isGroup=Type] {
|
||||
@specialize[@name=PrimitiveType]<
|
||||
@ -74,6 +76,8 @@ VariableDefinition { identifier }
|
||||
|
||||
VariableName { identifier }
|
||||
|
||||
ArgumentLabel { identifier }
|
||||
|
||||
@skip { whitespace | LineComment | BlockComment }
|
||||
|
||||
kw<term> { @specialize[@name={term}]<identifier, term> }
|
||||
|
85
packages/codemirror-lang-kcl/test/call.txt
Normal file
@ -0,0 +1,85 @@
|
||||
# empty
|
||||
|
||||
f()
|
||||
|
||||
==>
|
||||
Program(ExpressionStatement(CallExpression(VariableName,
|
||||
ArgumentList)))
|
||||
|
||||
# single anon arg
|
||||
|
||||
f(1)
|
||||
|
||||
==>
|
||||
Program(ExpressionStatement(CallExpression(VariableName,
|
||||
ArgumentList(Number))))
|
||||
|
||||
# deprecated multiple anon args
|
||||
|
||||
f(1, 2)
|
||||
|
||||
==>
|
||||
Program(ExpressionStatement(CallExpression(VariableName,
|
||||
ArgumentList(Number,
|
||||
Number))))
|
||||
|
||||
# deprecated trailing %
|
||||
|
||||
startSketchOn('XY')
|
||||
|> line([thickness, 0], %)
|
||||
|
||||
==>
|
||||
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
|
||||
ArgumentList(String)),
|
||||
PipeOperator,
|
||||
CallExpression(VariableName,
|
||||
ArgumentList(ArrayExpression(VariableName,
|
||||
Number),
|
||||
PipeSubstitution)))))
|
||||
|
||||
# % and named arg
|
||||
|
||||
startSketchOn('XY')
|
||||
|> line(%, end = [thickness, 0])
|
||||
|
||||
==>
|
||||
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
|
||||
ArgumentList(String)),
|
||||
PipeOperator,
|
||||
CallExpression(VariableName,
|
||||
ArgumentList(PipeSubstitution,
|
||||
LabeledArgument(ArgumentLabel,
|
||||
Equals,
|
||||
ArrayExpression(VariableName,
|
||||
Number)))))))
|
||||
|
||||
# implied % and named arg
|
||||
|
||||
startSketchOn('XY')
|
||||
|> line(end = [thickness, 0])
|
||||
|
||||
==>
|
||||
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
|
||||
ArgumentList(String)),
|
||||
PipeOperator,
|
||||
CallExpression(VariableName,
|
||||
ArgumentList(LabeledArgument(ArgumentLabel,
|
||||
Equals,
|
||||
ArrayExpression(VariableName,
|
||||
Number)))))))
|
||||
|
||||
# multiple named arg
|
||||
|
||||
ngon(plane = "XY", numSides = 5, radius = pentR)
|
||||
|
||||
==>
|
||||
Program(ExpressionStatement(CallExpression(VariableName,
|
||||
ArgumentList(LabeledArgument(ArgumentLabel,
|
||||
Equals,
|
||||
String),
|
||||
LabeledArgument(ArgumentLabel,
|
||||
Equals,
|
||||
Number),
|
||||
LabeledArgument(ArgumentLabel,
|
||||
Equals,
|
||||
VariableName)))))
|
@ -29,7 +29,7 @@
|
||||
"vscode-uri": "^3.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
|
@ -109,10 +109,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||
|
||||
"@types/node@^22.10.6":
|
||||
version "22.10.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.6.tgz#5c6795e71635876039f853cbccd59f523d9e4239"
|
||||
integrity sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==
|
||||
"@types/node@^22.13.1":
|
||||
version "22.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
|
||||
integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
|
@ -1 +1,212 @@
|
||||
404: Not Found
|
||||
[
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "80/20 Rail",
|
||||
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "a-parametric-bearing-pillow-block/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "A Parametric Bearing Pillow Block",
|
||||
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Ball Bearing",
|
||||
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Shelf Bracket",
|
||||
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Car Wheel Assembly",
|
||||
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Hollow Dodecahedron",
|
||||
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "enclosure/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Enclosure",
|
||||
"description": "An enclosure body and sealing lid for storing items"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Flange",
|
||||
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Flange with XY coordinates",
|
||||
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
|
||||
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "food-service-spatula/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Food Service Spatula",
|
||||
"description": "Use these spatulas for mixing, flipping, and scraping."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "french-press/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "French Press",
|
||||
"description": "A french press immersion coffee maker"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "gear/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Spur Gear",
|
||||
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "gear-rack/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "100mm Gear Rack",
|
||||
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "hex-nut/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Hex nut",
|
||||
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "i-beam/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "I-beam",
|
||||
"description": "A structural metal beam with an I shaped cross section. Often used in construction"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "kitt/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Kitt",
|
||||
"description": "The beloved KittyCAD mascot in a voxelized style."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "lego/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Lego Brick",
|
||||
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "mounting-plate/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Mounting Plate",
|
||||
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "multi-axis-robot/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Robot Arm",
|
||||
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe",
|
||||
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe-flange-assembly/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe and Flange Assembly",
|
||||
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe-with-bend/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe with bend",
|
||||
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "poopy-shoe/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Poopy Shoe",
|
||||
"description": "poop shute for bambu labs printer - optimized for printing."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "router-template-cross-bar/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Router template for a cross bar",
|
||||
"description": "A guide for routing a notch into a cross bar."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "router-template-slate/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Router template for a slate",
|
||||
"description": "A guide for routing a slate for a cross bar."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Sheet Metal Bracket",
|
||||
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Socket Head Cap Screw",
|
||||
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "walkie-talkie/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Walkie Talkie",
|
||||
"description": "A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings."
|
||||
},
|
||||
{
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "washer/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Washer",
|
||||
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
|
||||
}
|
||||
]
|
@ -57,7 +57,7 @@ function CommandComboBox({
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
(event.metaKey && event.key === 'k') ||
|
||||
(event.key === 'Backspace' && !event.currentTarget.value)
|
||||
event.key === 'Escape'
|
||||
) {
|
||||
event.preventDefault()
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
|
@ -2,11 +2,15 @@ import { Dialog } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
|
||||
|
||||
const DownloadAppBanner = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const [isBannerDismissed, setIsBannerDismissed] = useState(
|
||||
settings.context.app.dismissWebBanner.current
|
||||
settings.context.app.dismissWebBanner.current || hasCreateFileParam
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -168,7 +168,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
height: 'auto',
|
||||
}}
|
||||
minWidth={200}
|
||||
maxWidth={800}
|
||||
maxWidth={window.innerWidth - 10}
|
||||
handleWrapperClass="sidebar-resize-handles"
|
||||
handleClasses={{
|
||||
right:
|
||||
|
@ -104,7 +104,7 @@ function ProjectMenuPopover({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
useSettingsAuthContext()
|
||||
const token = useToken()
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
const commands = useSelector(commandBarActor, commandsSelector)
|
||||
@ -193,14 +193,13 @@ function ProjectMenuPopover({
|
||||
{
|
||||
id: 'share-link',
|
||||
Element: 'button',
|
||||
children: 'Share link to file',
|
||||
disabled: IS_NIGHTLY_OR_DEBUG || !findCommand(shareCommandInfo),
|
||||
children: 'Share current part (via Zoo link)',
|
||||
disabled: !(IS_NIGHTLY_OR_DEBUG && findCommand(shareCommandInfo)),
|
||||
onClick: async () => {
|
||||
await copyFileShareLink({
|
||||
token: token ?? '',
|
||||
code: codeManager.code,
|
||||
name: project?.name || '',
|
||||
units: settings.context.modeling.defaultUnit.current,
|
||||
})
|
||||
},
|
||||
},
|
||||
@ -263,7 +262,7 @@ function ProjectMenuPopover({
|
||||
as={Fragment}
|
||||
>
|
||||
<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
|
||||
shadow-lg`}
|
||||
>
|
||||
|
@ -30,15 +30,7 @@ import {
|
||||
FILE_EXT,
|
||||
PROJECT_ENTRYPOINT,
|
||||
} from 'lib/constants'
|
||||
import { DeepPartial } from 'lib/types'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import {
|
||||
loadAndValidateSettings,
|
||||
projectConfigurationToSettingsPayload,
|
||||
saveSettings,
|
||||
setSettingsAtLevel,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { Project } from 'lib/project'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
@ -86,7 +78,7 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
||||
setSearchParams(searchParams)
|
||||
}, [searchParams, setSearchParams])
|
||||
const {
|
||||
settings: { context: settings, send: settingsSend },
|
||||
settings: { context: settings },
|
||||
} = useSettingsAuthContext()
|
||||
|
||||
const [state, send, actor] = useMachine(
|
||||
@ -132,17 +124,10 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
||||
clearImportSearchParams()
|
||||
codeManager.updateCodeStateEditor(input.code || '')
|
||||
await codeManager.writeToFile()
|
||||
|
||||
settingsSend({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
value: input.units,
|
||||
},
|
||||
})
|
||||
await kclManager.executeCode(true)
|
||||
|
||||
return {
|
||||
message: 'File and units overwritten successfully',
|
||||
message: 'File overwritten successfully',
|
||||
fileName: input.name,
|
||||
projectName: '',
|
||||
}
|
||||
@ -392,16 +377,6 @@ const ProjectsContextDesktop = ({
|
||||
? input.name
|
||||
: input.name + FILE_EXT
|
||||
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)
|
||||
if (needsInterpolated) {
|
||||
@ -414,28 +389,10 @@ const ProjectsContextDesktop = ({
|
||||
|
||||
// Create the project around the file if newProject
|
||||
if (input.method === 'newProject') {
|
||||
await createNewProjectDirectory(
|
||||
projectName,
|
||||
input.code,
|
||||
unitsConfiguration
|
||||
)
|
||||
await createNewProjectDirectory(projectName, input.code)
|
||||
message = `Project "${projectName}" created successfully with link contents`
|
||||
} else {
|
||||
let projectPath = window.electron.join(
|
||||
settings.app.projectDirectory.current,
|
||||
projectName
|
||||
)
|
||||
|
||||
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
|
||||
|
@ -302,7 +302,7 @@ export const Stream = () => {
|
||||
return
|
||||
}
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: entity_id, types: ['path', 'solid2d', 'segment'] },
|
||||
{ key: entity_id, types: ['path', 'solid2d', 'segment', 'helix'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(path)) {
|
||||
|
@ -1,9 +1,31 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { changeKclSettings, unitLengthToUnitLen } from 'lang/wasm'
|
||||
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export function UnitsMenu() {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const [hasPerFileLengthUnit, setHasPerFileLengthUnit] = useState(
|
||||
Boolean(kclManager.fileSettings.defaultLengthUnit)
|
||||
)
|
||||
const [lengthSetting, setLengthSetting] = useState(
|
||||
kclManager.fileSettings.defaultLengthUnit ||
|
||||
settings.context.modeling.defaultUnit.current
|
||||
)
|
||||
useEffect(() => {
|
||||
setHasPerFileLengthUnit(Boolean(kclManager.fileSettings.defaultLengthUnit))
|
||||
setLengthSetting(
|
||||
kclManager.fileSettings.defaultLengthUnit ||
|
||||
settings.context.modeling.defaultUnit.current
|
||||
)
|
||||
}, [
|
||||
kclManager.fileSettings.defaultLengthUnit,
|
||||
settings.context.modeling.defaultUnit.current,
|
||||
])
|
||||
return (
|
||||
<Popover className="relative pointer-events-auto">
|
||||
{({ close }) => (
|
||||
@ -18,7 +40,7 @@ export function UnitsMenu() {
|
||||
<div className="absolute w-[1px] h-[1em] bg-primary right-0 top-1/2 -translate-y-1/2"></div>
|
||||
</div>
|
||||
<span className="sr-only">Current units are: </span>
|
||||
{settings.context.modeling.defaultUnit.current}
|
||||
{lengthSetting}
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
|
||||
@ -31,18 +53,40 @@ export function UnitsMenu() {
|
||||
<button
|
||||
className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
|
||||
onClick={() => {
|
||||
settings.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
value: unit,
|
||||
},
|
||||
})
|
||||
if (hasPerFileLengthUnit) {
|
||||
const newCode = changeKclSettings(codeManager.code, {
|
||||
defaultLengthUnits: unitLengthToUnitLen(unit),
|
||||
defaultAngleUnits: { type: 'Degrees' },
|
||||
})
|
||||
if (err(newCode)) {
|
||||
toast.error(
|
||||
`Failed to set per-file units: ${newCode.message}`
|
||||
)
|
||||
} else {
|
||||
codeManager.updateCodeStateEditor(newCode)
|
||||
Promise.all([
|
||||
codeManager.writeToFile(),
|
||||
kclManager.executeCode(),
|
||||
])
|
||||
.then(() => {
|
||||
toast.success(`Updated per-file units to ${unit}`)
|
||||
})
|
||||
.catch(reportRejection)
|
||||
}
|
||||
} else {
|
||||
settings.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'project',
|
||||
value: unit,
|
||||
},
|
||||
})
|
||||
}
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<span className="flex-1">{baseUnitLabels[unit]}</span>
|
||||
{unit === settings.context.modeling.defaultUnit.current && (
|
||||
{unit === lengthSetting && (
|
||||
<span className="text-chalkboard-60">current</span>
|
||||
)}
|
||||
</button>
|
||||
|
@ -6,7 +6,6 @@ import { useSettingsAuthContext } from './useSettingsAuthContext'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { FileLinkParams } from 'lib/links'
|
||||
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||
import { baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||
|
||||
// For initializing the command arguments, we actually want `method` to be undefined
|
||||
// so that we don't skip it in the command palette.
|
||||
@ -37,13 +36,7 @@ export function useCreateFileLinkQuery(
|
||||
code: base64ToString(
|
||||
decodeURIComponent(searchParams.get('code') ?? '')
|
||||
),
|
||||
|
||||
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 = {
|
||||
@ -55,7 +48,6 @@ export function useCreateFileLinkQuery(
|
||||
? settings.context.projects.defaultProjectName.current
|
||||
: DEFAULT_FILE_NAME,
|
||||
code: params.code || '',
|
||||
units: params.units,
|
||||
method: isDesktop() ? undefined : 'existingProject',
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ import {
|
||||
SourceRange,
|
||||
topLevelRange,
|
||||
} from 'lang/wasm'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { getNodeFromPath, getSettingsAnnotation } from './queryAst'
|
||||
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { markOnce } from 'lib/performance'
|
||||
@ -35,6 +35,7 @@ import {
|
||||
ModelingCmdReq_type,
|
||||
} from '@kittycad/lib/dist/types/src/models'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'
|
||||
|
||||
interface ExecuteArgs {
|
||||
ast?: Node<Program>
|
||||
@ -70,6 +71,7 @@ export class KclManager {
|
||||
private _wasmInitFailed = true
|
||||
private _hasErrors = false
|
||||
private _switchedFiles = false
|
||||
private _fileSettings: KclSettingsAnnotation = {}
|
||||
|
||||
engineCommandManager: EngineCommandManager
|
||||
|
||||
@ -368,6 +370,13 @@ export class KclManager {
|
||||
await this.disableSketchMode()
|
||||
}
|
||||
|
||||
let fileSettings = getSettingsAnnotation(ast)
|
||||
if (err(fileSettings)) {
|
||||
console.error(fileSettings)
|
||||
fileSettings = {}
|
||||
}
|
||||
this.fileSettings = fileSettings
|
||||
|
||||
this.logs = logs
|
||||
this.errors = errors
|
||||
// Do not add the errors since the program was interrupted and the error is not a real KCL error
|
||||
@ -413,14 +422,7 @@ export class KclManager {
|
||||
|
||||
// NOTE: this always updates the code state and editor.
|
||||
// DO NOT CALL THIS from codemirror ever.
|
||||
async executeAstMock(
|
||||
ast: Program = this._ast,
|
||||
{
|
||||
updates,
|
||||
}: {
|
||||
updates: 'none' | 'artifactRanges'
|
||||
} = { updates: 'none' }
|
||||
) {
|
||||
async executeAstMock(ast: Program = this._ast) {
|
||||
await this.ensureWasmInit()
|
||||
|
||||
const newCode = recast(ast)
|
||||
@ -450,34 +452,6 @@ export class KclManager {
|
||||
this.lastSuccessfulProgramMemory = execState.memory
|
||||
this.lastSuccessfulOperations = execState.operations
|
||||
}
|
||||
if (updates !== 'artifactRanges') return
|
||||
|
||||
// TODO the below seems like a work around, I wish there's a comment explaining exactly what
|
||||
// problem this solves, but either way we should strive to remove it.
|
||||
Array.from(this.engineCommandManager.artifactGraph).forEach(
|
||||
([commandId, artifact]) => {
|
||||
if (!('codeRef' in artifact && artifact.codeRef)) return
|
||||
const _node1 = getNodeFromPath<Node<CallExpression | CallExpressionKw>>(
|
||||
this.ast,
|
||||
artifact.codeRef.pathToNode,
|
||||
['CallExpression', 'CallExpressionKw']
|
||||
)
|
||||
if (err(_node1)) return
|
||||
const { node } = _node1
|
||||
if (node.type !== 'CallExpression' && node.type !== 'CallExpressionKw')
|
||||
return
|
||||
const [oldStart, oldEnd] = artifact.codeRef.range
|
||||
if (oldStart === 0 && oldEnd === 0) return
|
||||
if (oldStart === node.start && oldEnd === node.end) return
|
||||
this.engineCommandManager.artifactGraph.set(commandId, {
|
||||
...artifact,
|
||||
codeRef: {
|
||||
...artifact.codeRef,
|
||||
range: topLevelRange(node.start, node.end),
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
cancelAllExecutions() {
|
||||
this._cancelTokens.forEach((_, key) => {
|
||||
@ -699,6 +673,14 @@ export class KclManager {
|
||||
_isAstEmpty(ast: Node<Program>) {
|
||||
return ast.start === 0 && ast.end === 0 && ast.body.length === 0
|
||||
}
|
||||
|
||||
get fileSettings() {
|
||||
return this._fileSettings
|
||||
}
|
||||
|
||||
set fileSettings(settings: KclSettingsAnnotation) {
|
||||
this._fileSettings = settings
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSelectionFilter: EntityType_type[] = [
|
||||
|
@ -55,6 +55,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
],
|
||||
id: expect.any(String),
|
||||
artifactId: expect.any(String),
|
||||
originalId: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
},
|
||||
@ -98,6 +99,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
originalId: expect.any(String),
|
||||
artifactId: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
@ -203,6 +205,7 @@ const sk2 = startSketchOn('XY')
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
originalId: expect.any(String),
|
||||
artifactId: expect.any(String),
|
||||
__meta: expect.any(Array),
|
||||
on: expect.any(Object),
|
||||
@ -308,6 +311,7 @@ const sk2 = startSketchOn('XY')
|
||||
],
|
||||
sketch: {
|
||||
id: expect.any(String),
|
||||
originalId: expect.any(String),
|
||||
artifactId: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
|
@ -221,6 +221,7 @@ const newVar = myVar + 1`
|
||||
},
|
||||
],
|
||||
id: expect.any(String),
|
||||
originalId: expect.any(String),
|
||||
artifactId: expect.any(String),
|
||||
units: {
|
||||
type: 'Mm',
|
||||
|
@ -28,7 +28,14 @@ try {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
child_process.spawnSync('git', ['clone', URL_GIT_KCL_SAMPLES, DIR_KCL_SAMPLES])
|
||||
child_process.spawnSync('git', [
|
||||
'clone',
|
||||
'--single-branch',
|
||||
'--branch',
|
||||
'achalmers/kw-appearance',
|
||||
URL_GIT_KCL_SAMPLES,
|
||||
DIR_KCL_SAMPLES,
|
||||
])
|
||||
|
||||
// @ts-expect-error
|
||||
let files = await fs.readdir(DIR_KCL_SAMPLES)
|
||||
|
@ -431,10 +431,11 @@ export function addSweep(
|
||||
} {
|
||||
const modifiedAst = structuredClone(node)
|
||||
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
|
||||
const sweep = createCallExpressionStdLib('sweep', [
|
||||
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
|
||||
const sweep = createCallExpressionStdLibKw(
|
||||
'sweep',
|
||||
createIdentifier(profileDeclarator.id.name),
|
||||
])
|
||||
[createLabeledArg('path', createIdentifier(pathDeclarator.id.name))]
|
||||
)
|
||||
const declaration = createVariableDeclaration(name, sweep)
|
||||
modifiedAst.body.push(declaration)
|
||||
const pathToNode: PathToNode = [
|
||||
@ -442,8 +443,9 @@ export function addSweep(
|
||||
[modifiedAst.body.length - 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
['arguments', 'CallExpressionKw'],
|
||||
[0, ARG_INDEX_FIELD],
|
||||
['arg', LABELED_ARG_FIELD],
|
||||
]
|
||||
|
||||
return {
|
||||
@ -683,6 +685,63 @@ export function addOffsetPlane({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a helix to the AST
|
||||
*/
|
||||
export function addHelix({
|
||||
node,
|
||||
revolutions,
|
||||
angleStart,
|
||||
counterClockWise,
|
||||
radius,
|
||||
axis,
|
||||
length,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
revolutions: Expr
|
||||
angleStart: Expr
|
||||
counterClockWise: boolean
|
||||
radius: Expr
|
||||
axis: string
|
||||
length: Expr
|
||||
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||
const modifiedAst = structuredClone(node)
|
||||
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.HELIX)
|
||||
const variable = createVariableDeclaration(
|
||||
name,
|
||||
createCallExpressionStdLibKw(
|
||||
'helix',
|
||||
null, // Not in a pipeline
|
||||
[
|
||||
createLabeledArg('revolutions', revolutions),
|
||||
createLabeledArg('angleStart', angleStart),
|
||||
createLabeledArg('counterClockWise', createLiteral(counterClockWise)),
|
||||
createLabeledArg('radius', radius),
|
||||
createLabeledArg('axis', createLiteral(axis)),
|
||||
createLabeledArg('length', length),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
// TODO: figure out smart insertion than just appending at the end
|
||||
const argIndex = 0
|
||||
modifiedAst.body.push(variable)
|
||||
const pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[modifiedAst.body.length - 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpressionKw'],
|
||||
[argIndex, ARG_INDEX_FIELD],
|
||||
['arg', LABELED_ARG_FIELD],
|
||||
]
|
||||
|
||||
return {
|
||||
modifiedAst,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a modified clone of an AST with a named constant inserted into the body
|
||||
*/
|
||||
@ -1316,6 +1375,7 @@ export async function deleteFromSelection(
|
||||
varDec.node.init.type === 'PipeExpression') ||
|
||||
selection.artifact?.type === 'sweep' ||
|
||||
selection.artifact?.type === 'plane' ||
|
||||
selection.artifact?.type === 'helix' ||
|
||||
!selection.artifact // aka expected to be a shell at this point
|
||||
) {
|
||||
let extrudeNameToDelete = ''
|
||||
@ -1323,7 +1383,8 @@ export async function deleteFromSelection(
|
||||
if (
|
||||
selection.artifact &&
|
||||
selection.artifact.type !== 'sweep' &&
|
||||
selection.artifact.type !== 'plane'
|
||||
selection.artifact.type !== 'plane' &&
|
||||
selection.artifact.type !== 'helix'
|
||||
) {
|
||||
const varDecName = varDec.node.id.name
|
||||
traverse(astClone, {
|
||||
@ -1362,13 +1423,17 @@ export async function deleteFromSelection(
|
||||
if (!pathToNode) return new Error('Could not find extrude variable')
|
||||
} else {
|
||||
pathToNode = selection.codeRef.pathToNode
|
||||
const extrudeVarDec = getNodeFromPath<VariableDeclarator>(
|
||||
astClone,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(extrudeVarDec)) return extrudeVarDec
|
||||
extrudeNameToDelete = extrudeVarDec.node.id.name
|
||||
if (varDec.node.type !== 'VariableDeclarator') {
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
astClone,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExp)) return callExp
|
||||
extrudeNameToDelete = callExp.node.callee.name
|
||||
} else {
|
||||
extrudeNameToDelete = varDec.node.id.name
|
||||
}
|
||||
}
|
||||
|
||||
const expressionIndex = pathToNode[1][0] as number
|
||||
|
@ -46,6 +46,7 @@ export function revolveSketch(
|
||||
if (err(sketchNode)) return sketchNode
|
||||
|
||||
let generatedAxis
|
||||
let axisDeclaration: PathToNode | null = null
|
||||
|
||||
if (axisOrEdge === 'Edge') {
|
||||
const pathToAxisSelection = getNodePathFromSourceRange(
|
||||
@ -70,6 +71,13 @@ export function revolveSketch(
|
||||
const axisSelection = edge?.graphSelections[0]?.artifact
|
||||
if (!axisSelection) return new Error('Generated axis selection is missing.')
|
||||
generatedAxis = getEdgeTagCall(tag, axisSelection)
|
||||
if (
|
||||
axisSelection.type === 'segment' ||
|
||||
axisSelection.type === 'path' ||
|
||||
axisSelection.type === 'edgeCut'
|
||||
) {
|
||||
axisDeclaration = axisSelection.codeRef.pathToNode
|
||||
}
|
||||
} else {
|
||||
generatedAxis = createLiteral(axis)
|
||||
}
|
||||
@ -139,18 +147,34 @@ export function revolveSketch(
|
||||
const sketchIndexInPathToNode =
|
||||
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
||||
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
|
||||
if (typeof sketchIndexInBody !== 'number')
|
||||
return new Error('expected sketchIndexInBody to be a number')
|
||||
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
||||
let insertIndex = sketchIndexInBody
|
||||
|
||||
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 = [
|
||||
['body', ''],
|
||||
[sketchIndexInBody + 1, 'index'],
|
||||
[insertIndex + 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
return {
|
||||
modifiedAst: clonedAst,
|
||||
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
|
||||
|
@ -17,6 +17,8 @@ import {
|
||||
createObjectExpression,
|
||||
createArrayExpression,
|
||||
createVariableDeclaration,
|
||||
createCallExpressionStdLibKw,
|
||||
createLabeledArg,
|
||||
} from 'lang/modifyAst'
|
||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
@ -121,13 +123,14 @@ export function addShell({
|
||||
}
|
||||
|
||||
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SHELL)
|
||||
const shell = createCallExpressionStdLib('shell', [
|
||||
createObjectExpression({
|
||||
faces: createArrayExpression(expressions),
|
||||
thickness,
|
||||
}),
|
||||
const shell = createCallExpressionStdLibKw(
|
||||
'shell',
|
||||
createIdentifier(extrudeNode.node.id.name),
|
||||
])
|
||||
[
|
||||
createLabeledArg('faces', createArrayExpression(expressions)),
|
||||
createLabeledArg('thickness', thickness),
|
||||
]
|
||||
)
|
||||
const declaration = createVariableDeclaration(name, shell)
|
||||
|
||||
// TODO: check if we should append at the end like here or right after the extrude
|
||||
@ -137,8 +140,7 @@ export function addShell({
|
||||
[modifiedAst.body.length - 1, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
['unlabeled', 'CallExpressionKw'],
|
||||
]
|
||||
return {
|
||||
modifiedAst,
|
||||
|
@ -23,6 +23,9 @@ import {
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
recast,
|
||||
kclSettings,
|
||||
unitLenToUnitLength,
|
||||
unitAngToUnitAngle,
|
||||
} from './wasm'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||
@ -38,6 +41,7 @@ import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { findKwArg } from './util'
|
||||
import { codeRefFromRange } from './std/artifactGraph'
|
||||
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'
|
||||
|
||||
export const LABELED_ARG_FIELD = 'LabeledArg -> Arg'
|
||||
export const ARG_INDEX_FIELD = 'arg index'
|
||||
@ -866,3 +870,24 @@ export function getObjExprProperty(
|
||||
if (index === -1) return null
|
||||
return { expr: node.properties[index].value, index }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given KCL, returns the settings annotation object if it exists.
|
||||
*/
|
||||
export function getSettingsAnnotation(
|
||||
kcl: string | Node<Program>
|
||||
): KclSettingsAnnotation | Error {
|
||||
const metaSettings = kclSettings(kcl)
|
||||
if (err(metaSettings)) return metaSettings
|
||||
|
||||
const settings: KclSettingsAnnotation = {}
|
||||
// No settings in the KCL.
|
||||
if (!metaSettings) return settings
|
||||
|
||||
settings.defaultLengthUnit = unitLenToUnitLength(
|
||||
metaSettings.defaultLengthUnits
|
||||
)
|
||||
settings.defaultAngleUnit = unitAngToUnitAngle(metaSettings.defaultAngleUnits)
|
||||
|
||||
return settings
|
||||
}
|
||||
|
@ -1406,6 +1406,8 @@ export class EngineCommandManager extends EventTarget {
|
||||
commandId: string
|
||||
}
|
||||
settings: SettingsViaQueryString
|
||||
width: number = 1337
|
||||
height: number = 1337
|
||||
|
||||
/**
|
||||
* Export intent traxcks the intent of the export. If it is null there is no
|
||||
@ -1511,6 +1513,9 @@ export class EngineCommandManager extends EventTarget {
|
||||
return
|
||||
}
|
||||
|
||||
this.width = width
|
||||
this.height = height
|
||||
|
||||
// If we already have an engine connection, just need to resize the stream.
|
||||
if (this.engineConnection) {
|
||||
this.handleResize({
|
||||
@ -1823,6 +1828,9 @@ export class EngineCommandManager extends EventTarget {
|
||||
return
|
||||
}
|
||||
|
||||
this.width = streamWidth
|
||||
this.height = streamHeight
|
||||
|
||||
const resizeCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
default_project_settings,
|
||||
base64_decode,
|
||||
clear_scene_and_bust_cache,
|
||||
kcl_settings,
|
||||
change_kcl_settings,
|
||||
reloadModule,
|
||||
} from 'lib/wasm_lib_wrapper'
|
||||
@ -58,6 +59,9 @@ import { Artifact } from './std/artifactGraph'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
|
||||
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
|
||||
import { UnitAngle, UnitLength } from 'wasm-lib/kcl/bindings/ModelingCmd'
|
||||
import { UnitLen } from 'wasm-lib/kcl/bindings/UnitLen'
|
||||
import { UnitAngle as UnitAng } from 'wasm-lib/kcl/bindings/UnitAngle'
|
||||
|
||||
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
@ -857,8 +861,35 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the meta settings for the kcl file.
|
||||
/// Returns the new kcl string with the updated settings.
|
||||
/**
|
||||
* Get the meta settings for the KCL. If no settings were set in the file,
|
||||
* returns null.
|
||||
*/
|
||||
export function kclSettings(
|
||||
kcl: string | Node<Program>
|
||||
): MetaSettings | null | Error {
|
||||
let program: Node<Program>
|
||||
if (typeof kcl === 'string') {
|
||||
const parseResult = parse(kcl)
|
||||
if (err(parseResult)) return parseResult
|
||||
if (!resultIsOk(parseResult)) {
|
||||
return new Error(`parse result had errors`, { cause: parseResult })
|
||||
}
|
||||
program = parseResult.program
|
||||
} else {
|
||||
program = kcl
|
||||
}
|
||||
try {
|
||||
return kcl_settings(JSON.stringify(program))
|
||||
} catch (e) {
|
||||
return new Error('Caught error getting kcl settings', { cause: e })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the meta settings for the kcl file.
|
||||
* @returns the new kcl string with the updated settings.
|
||||
*/
|
||||
export function changeKclSettings(
|
||||
kcl: string,
|
||||
settings: MetaSettings
|
||||
@ -866,7 +897,59 @@ export function changeKclSettings(
|
||||
try {
|
||||
return change_kcl_settings(kcl, JSON.stringify(settings))
|
||||
} catch (e) {
|
||||
console.error('Caught error changing kcl settings: ' + e)
|
||||
return new Error('Caught error changing kcl settings: ' + e)
|
||||
console.error('Caught error changing kcl settings', e)
|
||||
return new Error('Caught error changing kcl settings', { cause: e })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a `UnitLength_type` to a `UnitLen`
|
||||
*/
|
||||
export function unitLengthToUnitLen(input: UnitLength): UnitLen {
|
||||
switch (input) {
|
||||
case 'm':
|
||||
return { type: 'M' }
|
||||
case 'cm':
|
||||
return { type: 'Cm' }
|
||||
case 'yd':
|
||||
return { type: 'Yards' }
|
||||
case 'ft':
|
||||
return { type: 'Feet' }
|
||||
case 'in':
|
||||
return { type: 'Inches' }
|
||||
default:
|
||||
return { type: 'Mm' }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `UnitLen` to `UnitLength_type`.
|
||||
*/
|
||||
export function unitLenToUnitLength(input: UnitLen): UnitLength {
|
||||
switch (input.type) {
|
||||
case 'M':
|
||||
return 'm'
|
||||
case 'Cm':
|
||||
return 'cm'
|
||||
case 'Yards':
|
||||
return 'yd'
|
||||
case 'Feet':
|
||||
return 'ft'
|
||||
case 'Inches':
|
||||
return 'in'
|
||||
default:
|
||||
return 'mm'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `UnitAngle` to `UnitAngle_type`.
|
||||
*/
|
||||
export function unitAngToUnitAngle(input: UnitAng): UnitAngle {
|
||||
switch (input.type) {
|
||||
case 'Radians':
|
||||
return 'radians'
|
||||
default:
|
||||
return 'degrees'
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +76,14 @@ export type ModelingCommandSchema = {
|
||||
plane: Selections
|
||||
distance: KclCommandValue
|
||||
}
|
||||
Helix: {
|
||||
revolutions: KclCommandValue
|
||||
angleStart: KclCommandValue
|
||||
counterClockWise: boolean
|
||||
radius: KclCommandValue
|
||||
axis: string
|
||||
length: KclCommandValue
|
||||
}
|
||||
'change tool': {
|
||||
tool: SketchTool
|
||||
}
|
||||
@ -447,6 +455,53 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
},
|
||||
},
|
||||
},
|
||||
Helix: {
|
||||
description: 'Create a helix or spiral in 3D about an axis.',
|
||||
icon: 'helix',
|
||||
status: 'development',
|
||||
needsReview: true,
|
||||
args: {
|
||||
revolutions: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: '1',
|
||||
required: true,
|
||||
warningMessage:
|
||||
'The helix workflow is new and under tested. Please break it and report issues.',
|
||||
},
|
||||
angleStart: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: KCL_DEFAULT_DEGREE,
|
||||
required: true,
|
||||
},
|
||||
counterClockWise: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: [
|
||||
{ name: 'True', isCurrent: false, value: true },
|
||||
{ name: 'False', isCurrent: true, value: false },
|
||||
],
|
||||
},
|
||||
radius: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: KCL_DEFAULT_LENGTH,
|
||||
required: true,
|
||||
},
|
||||
axis: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: [
|
||||
{ name: 'X Axis', isCurrent: true, value: 'X' },
|
||||
{ name: 'Y Axis', isCurrent: false, value: 'Y' },
|
||||
{ name: 'Z Axis', isCurrent: false, value: 'Z' },
|
||||
],
|
||||
},
|
||||
length: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: KCL_DEFAULT_LENGTH,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Fillet: {
|
||||
description: 'Fillet edge',
|
||||
icon: 'fillet3d',
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||
import { projectsMachine } from 'machines/projectsMachine'
|
||||
|
||||
export type ProjectsCommandSchema = {
|
||||
@ -23,7 +21,6 @@ export type ProjectsCommandSchema = {
|
||||
'Import file from URL': {
|
||||
name: string
|
||||
code?: string
|
||||
units: UnitLength_type
|
||||
method: 'newProject' | 'existingProject'
|
||||
projectName?: string
|
||||
}
|
||||
@ -157,15 +154,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
|
||||
},
|
||||
},
|
||||
units: {
|
||||
inputType: 'options',
|
||||
required: false,
|
||||
skip: true,
|
||||
options: baseUnitsUnion.map((unit) => ({
|
||||
name: baseUnitLabels[unit],
|
||||
value: unit,
|
||||
})),
|
||||
},
|
||||
},
|
||||
reviewMessage(commandBarContext) {
|
||||
return isDesktop()
|
||||
|
@ -58,6 +58,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
||||
SEGMENT: 'seg',
|
||||
REVOLVE: 'revolve',
|
||||
PLANE: 'plane',
|
||||
HELIX: 'helix',
|
||||
} as const
|
||||
/** The default KCL length expression */
|
||||
export const KCL_DEFAULT_LENGTH = `5`
|
||||
|
@ -12,104 +12,68 @@ wallMountL = 2 // inches
|
||||
shelfDepth = 12 // Shelf is 12 inches in depth from the wall
|
||||
moment = shelfDepth * p // assume the force is applied at the end of the shelf to be conservative (lb-in)
|
||||
|
||||
|
||||
filletRadius = .375 // inches
|
||||
extFilletRadius = .25 // inches
|
||||
mountingHoleDiameter = 0.5 // inches
|
||||
|
||||
|
||||
// Calculate required thickness of bracket
|
||||
thickness = sqrt(moment * factorOfSafety * 6 / (sigmaAllow * width)) // this is the calculation of two brackets holding up the shelf (inches)
|
||||
|
||||
filletRadius = .25
|
||||
extFilletRadius = filletRadius + thickness
|
||||
mountingHoleDiameter = 0.5
|
||||
|
||||
// Sketch the bracket body and fillet the inner and outer edges of the bend
|
||||
bracketLeg1Sketch = startSketchOn('XY')
|
||||
sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(end = [shelfMountL - filletRadius, 0], tag = $fillet1)
|
||||
|> line(end = [0, width], tag = $fillet2)
|
||||
|> line(end = [-shelfMountL + filletRadius, 0])
|
||||
|> xLine(shelfMountL - thickness, %, $seg01)
|
||||
|> yLine(thickness, %, $seg02)
|
||||
|> xLine(-shelfMountL, %, $seg03)
|
||||
|> yLine(-wallMountL, %, $seg04)
|
||||
|> xLine(thickness, %, $seg05)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg06)
|
||||
|> close()
|
||||
|> hole(circle({
|
||||
center = [1, 1],
|
||||
radius = mountingHoleDiameter / 2
|
||||
}, %), %)
|
||||
|> hole(circle({
|
||||
center = [shelfMountL - 1.5, width - 1],
|
||||
radius = mountingHoleDiameter / 2
|
||||
}, %), %)
|
||||
|> hole(circle({
|
||||
center = [1, width - 1],
|
||||
radius = mountingHoleDiameter / 2
|
||||
}, %), %)
|
||||
|> hole(circle({
|
||||
center = [shelfMountL - 1.5, 1],
|
||||
radius = mountingHoleDiameter / 2
|
||||
}, %), %)
|
||||
|
||||
// Extrude the leg 2 bracket sketch
|
||||
bracketLeg1Extrude = extrude(bracketLeg1Sketch, length = thickness)
|
||||
|> extrude(%, length = width)
|
||||
|> fillet({
|
||||
radius = extFilletRadius,
|
||||
tags = [
|
||||
getNextAdjacentEdge(fillet1),
|
||||
getNextAdjacentEdge(fillet2)
|
||||
]
|
||||
tags = [getNextAdjacentEdge(seg03)]
|
||||
}, %)
|
||||
|
||||
// Sketch the fillet arc
|
||||
filletSketch = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(end = [0, thickness])
|
||||
|> arc({
|
||||
angleEnd = 180,
|
||||
angleStart = 90,
|
||||
radius = filletRadius + thickness
|
||||
}, %)
|
||||
|> line(end = [thickness, 0])
|
||||
|> arc({
|
||||
angleEnd = 90,
|
||||
angleStart = 180,
|
||||
radius = filletRadius
|
||||
}, %)
|
||||
|
||||
// Sketch the bend
|
||||
filletExtrude = extrude(filletSketch, length = -width)
|
||||
|
||||
// Create a custom plane for the leg that sits on the wall
|
||||
customPlane = {
|
||||
plane = {
|
||||
origin = { x = -filletRadius, y = 0, z = 0 },
|
||||
xAxis = { x = 0, y = 1, z = 0 },
|
||||
yAxis = { x = 0, y = 0, z = 1 },
|
||||
zAxis = { x = 1, y = 0, z = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// Create a sketch for the second leg
|
||||
bracketLeg2Sketch = startSketchOn(customPlane)
|
||||
|> startProfileAt([0, -filletRadius], %)
|
||||
|> line(end = [width, 0])
|
||||
|> line(end = [0, -wallMountL], tag = $fillet3)
|
||||
|> line(end = [-width, 0], tag = $fillet4)
|
||||
|> close()
|
||||
|> hole(circle({
|
||||
center = [1, -1.5],
|
||||
radius = mountingHoleDiameter / 2
|
||||
}, %), %)
|
||||
|> hole(circle({
|
||||
center = [5, -1.5],
|
||||
radius = mountingHoleDiameter / 2
|
||||
}, %), %)
|
||||
|
||||
// Extrude the second leg
|
||||
bracketLeg2Extrude = extrude(bracketLeg2Sketch, length = -thickness)
|
||||
|> fillet({
|
||||
radius = extFilletRadius,
|
||||
tags = [
|
||||
getNextAdjacentEdge(fillet3),
|
||||
getNextAdjacentEdge(fillet4)
|
||||
]
|
||||
radius = filletRadius,
|
||||
tags = [getNextAdjacentEdge(seg06)]
|
||||
}, %)
|
||||
|> fillet({
|
||||
radius = filletRadius,
|
||||
tags = [seg02, getOppositeEdge(seg02)],
|
||||
}, %)
|
||||
|> fillet({
|
||||
radius = filletRadius,
|
||||
tags = [seg05, getOppositeEdge(seg05)],
|
||||
}, %)
|
||||
|
||||
sketch002 = startSketchOn(sketch001, seg03)
|
||||
|> circle({
|
||||
center = [-1.25, 1],
|
||||
radius = mountingHoleDiameter / 2,
|
||||
}, %)
|
||||
|> patternLinear2d({
|
||||
instances = 2,
|
||||
distance = 2.5,
|
||||
axis = [-1, 0],
|
||||
}, %)
|
||||
|> patternLinear2d({
|
||||
instances = 2,
|
||||
distance = 4,
|
||||
axis = [0, 1],
|
||||
}, %)
|
||||
|> extrude(%, length = -thickness-.01)
|
||||
|
||||
sketch003 = startSketchOn(sketch001, seg04)
|
||||
|> circle({
|
||||
center = [1, -1],
|
||||
radius = mountingHoleDiameter / 2,
|
||||
}, %)
|
||||
|> patternLinear2d({
|
||||
instances = 2,
|
||||
distance = 4,
|
||||
axis = [1, 0],
|
||||
}, %)
|
||||
|> extrude(%, length = -thickness-0.1)
|
||||
`
|
||||
|
||||
/**
|
||||
|
@ -136,7 +136,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
},
|
||||
{
|
||||
name: 'share-file-link',
|
||||
displayName: 'Share file',
|
||||
displayName: 'Share current part (via Zoo link)',
|
||||
hide: IS_NIGHTLY_OR_DEBUG ? undefined : 'desktop',
|
||||
description: 'Create a link that contains a copy of the current file.',
|
||||
groupId: 'code',
|
||||
@ -147,7 +147,6 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
token: commandProps.authToken,
|
||||
code: codeManager.code,
|
||||
name: commandProps.projectData.project?.name || '',
|
||||
units: commandProps.settings.defaultUnit,
|
||||
}).catch(reportRejection)
|
||||
},
|
||||
},
|
||||
|
@ -5,13 +5,12 @@ describe(`link creation tests`, () => {
|
||||
test(`createCreateFileUrl happy path`, async () => {
|
||||
const code = `extrusionDistance = 12`
|
||||
const name = `test`
|
||||
const units = `mm`
|
||||
|
||||
// Converted with external online tools
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
@ -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 { stringToBase64 } from './base64'
|
||||
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from 'env'
|
||||
@ -7,7 +6,6 @@ import { err } from './trap'
|
||||
export interface FileLinkParams {
|
||||
code: string
|
||||
name: string
|
||||
units: UnitLength_type
|
||||
}
|
||||
|
||||
export async function copyFileShareLink(
|
||||
@ -46,12 +44,11 @@ export async function copyFileShareLink(
|
||||
* With the additional step of asking the user if they want to
|
||||
* 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
|
||||
const searchParams = new URLSearchParams({
|
||||
[CREATE_FILE_URL_PARAM]: String(true),
|
||||
name,
|
||||
units,
|
||||
code: stringToBase64(code),
|
||||
[ASK_TO_OPEN_QUERY_PARAM]: String(true),
|
||||
})
|
||||
|
@ -819,8 +819,8 @@ export async function sendSelectEventToEngine(
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el,
|
||||
streamWidth: el.clientWidth,
|
||||
streamHeight: el.clientHeight,
|
||||
streamWidth: engineCommandManager.width,
|
||||
streamHeight: engineCommandManager.height,
|
||||
})
|
||||
const res = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
|
@ -4,6 +4,10 @@ import { AtLeast, PathValue, Paths } from 'lib/types'
|
||||
import { CommandArgumentConfig } from 'lib/commandTypes'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
|
||||
import {
|
||||
UnitAngle_type,
|
||||
UnitLength_type,
|
||||
} from '@kittycad/lib/dist/types/src/models'
|
||||
import { CameraOrbitType } from 'wasm-lib/kcl/bindings/CameraOrbitType'
|
||||
|
||||
export interface SettingsViaQueryString {
|
||||
@ -138,3 +142,12 @@ type RecursiveSettingsPayloads<T> = {
|
||||
}
|
||||
|
||||
export type SaveSettingsPayload = RecursiveSettingsPayloads<typeof settings>
|
||||
|
||||
/**
|
||||
* Annotation names for default units are defined on rust side in
|
||||
* src/wasm-lib/kcl/src/execution/annotations.rs
|
||||
*/
|
||||
export interface KclSettingsAnnotation {
|
||||
defaultLengthUnit?: UnitLength_type
|
||||
defaultAngleUnit?: UnitAngle_type
|
||||
}
|
||||
|
@ -290,9 +290,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
],
|
||||
{
|
||||
id: 'helix',
|
||||
onClick: () => console.error('Helix not yet implemented'),
|
||||
onClick: () => {
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Helix', groupId: 'modeling' },
|
||||
})
|
||||
},
|
||||
hotkey: 'H',
|
||||
icon: 'helix',
|
||||
status: 'kcl-only',
|
||||
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
|
||||
title: 'Helix',
|
||||
description: 'Create a helix or spiral in 3D about an axis.',
|
||||
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }],
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
default_project_settings as DefaultProjectSettings,
|
||||
base64_decode as Base64Decode,
|
||||
clear_scene_and_bust_cache as ClearSceneAndBustCache,
|
||||
kcl_settings as KclSettings,
|
||||
change_kcl_settings as ChangeKclSettings,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
|
||||
@ -111,6 +112,9 @@ export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = (
|
||||
) => {
|
||||
return getModule().clear_scene_and_bust_cache(...args)
|
||||
}
|
||||
export const kcl_settings: typeof KclSettings = (...args) => {
|
||||
return getModule().kcl_settings(...args)
|
||||
}
|
||||
export const change_kcl_settings: typeof ChangeKclSettings = (...args) => {
|
||||
return getModule().change_kcl_settings(...args)
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import {
|
||||
} from 'components/Toolbar/EqualLength'
|
||||
import { revolveSketch } from 'lang/modifyAst/addRevolve'
|
||||
import {
|
||||
addHelix,
|
||||
addOffsetPlane,
|
||||
addSweep,
|
||||
deleteFromSelection,
|
||||
@ -276,6 +277,7 @@ export type ModelingMachineEvent =
|
||||
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
||||
| { type: 'Chamfer'; data?: ModelingCommandSchema['Chamfer'] }
|
||||
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
||||
| { type: 'Helix'; data: ModelingCommandSchema['Helix'] }
|
||||
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
|
||||
| { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
|
||||
| {
|
||||
@ -1615,6 +1617,73 @@ export const modelingMachine = setup({
|
||||
}
|
||||
}
|
||||
),
|
||||
helixAstMod: fromPromise(
|
||||
async ({
|
||||
input,
|
||||
}: {
|
||||
input: ModelingCommandSchema['Helix'] | undefined
|
||||
}) => {
|
||||
if (!input) return new Error('No input provided')
|
||||
// Extract inputs
|
||||
const ast = kclManager.ast
|
||||
const {
|
||||
revolutions,
|
||||
angleStart,
|
||||
counterClockWise,
|
||||
radius,
|
||||
axis,
|
||||
length,
|
||||
} = input
|
||||
|
||||
for (const variable of [revolutions, angleStart, radius, length]) {
|
||||
// Insert the variable if it exists
|
||||
if (
|
||||
'variableName' in variable &&
|
||||
variable.variableName &&
|
||||
variable.insertIndex !== undefined
|
||||
) {
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(
|
||||
variable.insertIndex,
|
||||
0,
|
||||
variable.variableDeclarationAst
|
||||
)
|
||||
ast.body = newBody
|
||||
}
|
||||
}
|
||||
|
||||
const valueOrVariable = (variable: KclCommandValue) =>
|
||||
'variableName' in variable
|
||||
? variable.variableIdentifierAst
|
||||
: variable.valueAst
|
||||
|
||||
const result = addHelix({
|
||||
node: ast,
|
||||
revolutions: valueOrVariable(revolutions),
|
||||
angleStart: valueOrVariable(angleStart),
|
||||
counterClockWise,
|
||||
radius: valueOrVariable(radius),
|
||||
axis,
|
||||
length: valueOrVariable(length),
|
||||
})
|
||||
|
||||
const updateAstResult = await kclManager.updateAst(
|
||||
result.modifiedAst,
|
||||
true,
|
||||
{
|
||||
focusPath: [result.pathToNode],
|
||||
}
|
||||
)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updateAstResult.newAst
|
||||
)
|
||||
|
||||
if (updateAstResult?.selections) {
|
||||
editorManager.selectRange(updateAstResult?.selections)
|
||||
}
|
||||
}
|
||||
),
|
||||
sweepAstMod: fromPromise(
|
||||
async ({
|
||||
input,
|
||||
@ -1974,6 +2043,11 @@ export const modelingMachine = setup({
|
||||
reenter: true,
|
||||
},
|
||||
|
||||
Helix: {
|
||||
target: 'Applying helix',
|
||||
reenter: true,
|
||||
},
|
||||
|
||||
'Prompt-to-edit': 'Applying Prompt-to-edit',
|
||||
},
|
||||
|
||||
@ -2734,6 +2808,19 @@ export const modelingMachine = setup({
|
||||
},
|
||||
},
|
||||
|
||||
'Applying helix': {
|
||||
invoke: {
|
||||
src: 'helixAstMod',
|
||||
id: 'helixAstMod',
|
||||
input: ({ event }) => {
|
||||
if (event.type !== 'Helix') return undefined
|
||||
return event.data
|
||||
},
|
||||
onDone: ['idle'],
|
||||
onError: ['idle'],
|
||||
},
|
||||
},
|
||||
|
||||
'Applying sweep': {
|
||||
invoke: {
|
||||
src: 'sweepAstMod',
|
||||
|
@ -306,7 +306,6 @@ export const projectsMachine = setup({
|
||||
return {
|
||||
code: '',
|
||||
name: '',
|
||||
units: 'mm',
|
||||
method: 'existingProject',
|
||||
projects: context.projects,
|
||||
}
|
||||
@ -314,7 +313,6 @@ export const projectsMachine = setup({
|
||||
return {
|
||||
code: event.data.code || '',
|
||||
name: event.data.name,
|
||||
units: event.data.units,
|
||||
method: event.data.method,
|
||||
projectName: event.data.projectName,
|
||||
projects: context.projects,
|
||||
|
@ -329,6 +329,7 @@ ipcMain.handle('kittycad', (event, data) => {
|
||||
)(data.args)
|
||||
})
|
||||
|
||||
// Used to find other devices on the local network, e.g. 3D printers, CNC machines, etc.
|
||||
ipcMain.handle('find_machine_api', () => {
|
||||
const timeoutAfterMs = 5000
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -339,7 +340,6 @@ ipcMain.handle('find_machine_api', () => {
|
||||
console.error(error)
|
||||
resolve(null)
|
||||
})
|
||||
console.log('Looking for machine API...')
|
||||
bonjourEt.find(
|
||||
{ protocol: 'tcp', type: 'machine-api' },
|
||||
(service: Service) => {
|
||||
|
@ -26,7 +26,7 @@ export default function Units() {
|
||||
<div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<SettingsSection
|
||||
@ -72,9 +72,7 @@ export default function Units() {
|
||||
</SettingsSection>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.CAMERA}
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Streaming"
|
||||
dismissClassName="right-auto left-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,19 +1,17 @@
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, kbdClasses } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||
import { COMMAND_PALETTE_HOTKEY } from 'components/CommandBar/CommandBar'
|
||||
|
||||
export default function CmdK() {
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.USER_MENU)
|
||||
const platformName = usePlatform()
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<h2 className="text-2xl font-bold">Command Bar</h2>
|
||||
@ -38,12 +36,7 @@ export default function CmdK() {
|
||||
. You can control settings, authentication, and file management from
|
||||
the command bar, as well as a growing number of modeling commands.
|
||||
</p>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.COMMAND_K}
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: User Menu"
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.COMMAND_K} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,22 +1,14 @@
|
||||
import {
|
||||
kbdClasses,
|
||||
OnboardingButtons,
|
||||
useDemoCode,
|
||||
useDismiss,
|
||||
useNextClick,
|
||||
} from '.'
|
||||
import { kbdClasses, OnboardingButtons, useDemoCode } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
|
||||
export default function OnboardingCodeEditor() {
|
||||
useDemoCode()
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.PARAMETRIC_MODELING)
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<section className="flex-1 overflow-y-auto">
|
||||
@ -73,12 +65,7 @@ export default function OnboardingCodeEditor() {
|
||||
pressing <kbd className={kbdClasses}>Shift + C</kbd>.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.EDITOR}
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Parametric Modeling"
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.EDITOR} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
|
||||
export default function Export() {
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.SKETCHING)
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<section className="flex-1">
|
||||
@ -52,12 +49,7 @@ export default function Export() {
|
||||
!
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.EXPORT}
|
||||
next={next}
|
||||
dismiss={dismiss}
|
||||
nextText="Next: Sketching"
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.EXPORT} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OnboardingButtons, useDemoCode, useDismiss } from '.'
|
||||
import { OnboardingButtons, useDemoCode } from '.'
|
||||
import { useEffect } from 'react'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
@ -7,7 +7,6 @@ import { sceneInfra } from 'lib/singletons'
|
||||
|
||||
export default function FutureWork() {
|
||||
const { send } = useModelingContext()
|
||||
const dismiss = useDismiss()
|
||||
|
||||
// Reset the code, the camera, and the modeling state
|
||||
useDemoCode()
|
||||
@ -19,7 +18,7 @@ export default function FutureWork() {
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50">
|
||||
<div className="max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
|
||||
<div className="relative max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
|
||||
<h1 className="text-2xl font-bold">Future Work</h1>
|
||||
<p className="my-4">
|
||||
We have curves, cuts, multi-profile sketch mode, and many more CAD
|
||||
@ -59,9 +58,6 @@ export default function FutureWork() {
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.FUTURE_WORK}
|
||||
className="mt-6"
|
||||
dismiss={dismiss}
|
||||
next={dismiss}
|
||||
nextText="Finish"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,23 +1,15 @@
|
||||
import {
|
||||
OnboardingButtons,
|
||||
kbdClasses,
|
||||
useDemoCode,
|
||||
useDismiss,
|
||||
useNextClick,
|
||||
} from '.'
|
||||
import { OnboardingButtons, kbdClasses, useDemoCode } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { bracketWidthConstantLine } from 'lib/exampleKcl'
|
||||
|
||||
export default function OnboardingInteractiveNumbers() {
|
||||
useDemoCode()
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.COMMAND_K)
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<section className="flex-1 overflow-y-auto mb-6">
|
||||
@ -88,12 +80,7 @@ export default function OnboardingInteractiveNumbers() {
|
||||
your ideas for how to make it better.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.INTERACTIVE_NUMBERS}
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Command Bar"
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.INTERACTIVE_NUMBERS} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, useDemoCode } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
@ -8,13 +8,12 @@ import { isDesktop } from 'lib/isDesktop'
|
||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
|
||||
/**
|
||||
* Show either a welcome screen or a warning screen
|
||||
@ -39,7 +38,7 @@ interface OnboardingResetWarningProps {
|
||||
function OnboardingResetWarning(props: OnboardingResetWarningProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
|
||||
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
<div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
{!isDesktop() ? (
|
||||
<OnboardingWarningWeb {...props} />
|
||||
) : (
|
||||
@ -52,7 +51,6 @@ function OnboardingResetWarning(props: OnboardingResetWarningProps) {
|
||||
|
||||
function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
|
||||
const navigate = useNavigate()
|
||||
const dismiss = useDismiss()
|
||||
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { context: fileContext } = useFileContext()
|
||||
const { onProjectClose, onProjectOpen } = useLspContext()
|
||||
@ -81,17 +79,28 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
className="mt-6"
|
||||
dismiss={dismiss}
|
||||
next={toSync(onAccept, reportRejection)}
|
||||
nextText="Make a new project"
|
||||
onNextOverride={() => {
|
||||
onAccept().catch(reportRejection)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
|
||||
const dismiss = useDismiss()
|
||||
useEffect(() => {
|
||||
async function beforeNavigate() {
|
||||
// We do want to update both the state and editor here.
|
||||
codeManager.updateCodeStateEditor(bracket)
|
||||
await codeManager.writeToFile()
|
||||
|
||||
await kclManager.executeCode(true)
|
||||
props.setShouldShowWarning(false)
|
||||
}
|
||||
return () => {
|
||||
beforeNavigate().catch(reportRejection)
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-3xl font-bold text-warn-80 dark:text-warn-10">
|
||||
@ -101,19 +110,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
|
||||
We see you have some of your own code written in this project. Please
|
||||
save it somewhere else before continuing the onboarding.
|
||||
</p>
|
||||
<OnboardingButtons
|
||||
className="mt-6"
|
||||
dismiss={dismiss}
|
||||
next={toSync(async () => {
|
||||
// We do want to update both the state and editor here.
|
||||
codeManager.updateCodeStateEditor(bracket)
|
||||
await codeManager.writeToFile()
|
||||
|
||||
await kclManager.executeCode(true)
|
||||
props.setShouldShowWarning(false)
|
||||
}, reportRejection)}
|
||||
nextText="Overwrite code and continue"
|
||||
/>
|
||||
<OnboardingButtons className="mt-6" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -136,12 +133,10 @@ function OnboardingIntroductionInner() {
|
||||
(theme.current === Themes.System && getSystemTheme() === Themes.Light)
|
||||
? '-dark'
|
||||
: ''
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.CAMERA)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
|
||||
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
<div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
|
||||
<img
|
||||
src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
|
||||
@ -192,9 +187,6 @@ function OnboardingIntroductionInner() {
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.INDEX}
|
||||
className="mt-6"
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Mouse Controls"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons, useDemoCode } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
@ -21,14 +21,12 @@ export default function OnboardingParametricModeling() {
|
||||
(theme === Themes.System && getSystemTheme() === Themes.Light)
|
||||
? '-dark'
|
||||
: ''
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.INTERACTIVE_NUMBERS)
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<section className="flex-1 overflow-y-auto mb-6">
|
||||
@ -77,12 +75,7 @@ export default function OnboardingParametricModeling() {
|
||||
</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.PARAMETRIC_MODELING}
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Interactive Numbers"
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.PARAMETRIC_MODELING} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
|
||||
export default function ProjectMenu() {
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.EXPORT)
|
||||
const onDesktop = isDesktop()
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<section className="flex-1">
|
||||
@ -57,12 +55,7 @@ export default function ProjectMenu() {
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.PROJECT_MENU}
|
||||
next={next}
|
||||
dismiss={dismiss}
|
||||
nextText="Next: Export"
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.PROJECT_MENU} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useEffect } from 'react'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
|
||||
export default function Sketching() {
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.FUTURE_WORK)
|
||||
|
||||
useEffect(() => {
|
||||
async function clearEditor() {
|
||||
// We do want to update both the state and editor here.
|
||||
@ -22,7 +19,7 @@ export default function Sketching() {
|
||||
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<h1 className="text-2xl font-bold">Sketching</h1>
|
||||
@ -45,9 +42,6 @@ export default function Sketching() {
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.SKETCHING}
|
||||
className="mt-6"
|
||||
next={next}
|
||||
dismiss={dismiss}
|
||||
nextText="Next: Future Work"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
|
||||
export default function Streaming() {
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.EDITOR)
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-start items-center inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<section className="flex-1 overflow-y-auto">
|
||||
@ -44,9 +41,7 @@ export default function Streaming() {
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.STREAMING}
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Code Editor"
|
||||
dismissClassName="right-auto left-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||
import { OnboardingButtons } from '.'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useUser } from 'machines/appMachine'
|
||||
|
||||
export default function UserMenu() {
|
||||
const user = useUser()
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.PROJECT_MENU)
|
||||
const [avatarErrored, setAvatarErrored] = useState(false)
|
||||
|
||||
const errorOrNoImage = !user?.image || avatarErrored
|
||||
@ -32,7 +30,7 @@ export default function UserMenu() {
|
||||
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
|
||||
}
|
||||
>
|
||||
<section className="flex-1">
|
||||
@ -48,12 +46,7 @@ export default function UserMenu() {
|
||||
only apply to the current project.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={onboardingPaths.USER_MENU}
|
||||
dismiss={dismiss}
|
||||
next={next}
|
||||
nextText="Next: Project Menu"
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.USER_MENU} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -26,6 +26,9 @@ import { reportRejection } from 'lib/trap'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
export const kbdClasses =
|
||||
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
||||
@ -163,58 +166,99 @@ export function useStepNumber(
|
||||
: onboardingRoutes.findIndex(
|
||||
(r) => r.path === makeUrlPathRelative(slug)
|
||||
) + 1
|
||||
: undefined
|
||||
: 1
|
||||
}
|
||||
|
||||
export function OnboardingButtons({
|
||||
next,
|
||||
nextText,
|
||||
dismiss,
|
||||
currentSlug,
|
||||
className,
|
||||
dismissClassName,
|
||||
onNextOverride,
|
||||
...props
|
||||
}: {
|
||||
next: () => void
|
||||
nextText?: string
|
||||
dismiss: () => void
|
||||
currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
|
||||
className?: string
|
||||
dismissClassName?: string
|
||||
onNextOverride?: () => void
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const dismiss = useDismiss()
|
||||
const stepNumber = useStepNumber(currentSlug)
|
||||
const previousStep =
|
||||
!stepNumber || stepNumber === 0 ? null : onboardingRoutes[stepNumber - 2]
|
||||
const goToPrevious = useNextClick(
|
||||
onboardingPaths.INDEX + (previousStep?.path ?? '')
|
||||
)
|
||||
const nextStep =
|
||||
!stepNumber || stepNumber === onboardingRoutes.length
|
||||
? null
|
||||
: onboardingRoutes[stepNumber]
|
||||
const goToNext = useNextClick(onboardingPaths.INDEX + (nextStep?.path ?? ''))
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'flex items-center justify-between ' + (className ?? '')}
|
||||
{...props}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
<>
|
||||
<button
|
||||
onClick={dismiss}
|
||||
iconStart={{
|
||||
icon: 'close',
|
||||
className: 'text-chalkboard-10',
|
||||
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
|
||||
}}
|
||||
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
|
||||
className={
|
||||
'group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ' +
|
||||
dismissClassName
|
||||
}
|
||||
data-testid="onboarding-dismiss"
|
||||
>
|
||||
Dismiss
|
||||
</ActionButton>
|
||||
{stepNumber !== undefined && (
|
||||
<p className="font-mono text-xs text-center m-0">
|
||||
{stepNumber} / {onboardingRoutes.length}
|
||||
</p>
|
||||
)}
|
||||
<ActionButton
|
||||
autoFocus
|
||||
Element="button"
|
||||
onClick={next}
|
||||
iconStart={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }}
|
||||
className="dark:hover:bg-chalkboard-80/50"
|
||||
data-testid="onboarding-next"
|
||||
<CustomIcon
|
||||
name="close"
|
||||
className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
|
||||
/>
|
||||
<Tooltip position="bottom" delay={500}>
|
||||
Dismiss <kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
|
||||
</Tooltip>
|
||||
</button>
|
||||
<div
|
||||
className={'flex items-center justify-between ' + (className ?? '')}
|
||||
{...props}
|
||||
>
|
||||
{nextText ?? 'Next'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
previousStep?.path || previousStep?.index
|
||||
? goToPrevious()
|
||||
: dismiss()
|
||||
}
|
||||
iconStart={{
|
||||
icon: previousStep ? 'arrowLeft' : 'close',
|
||||
className: 'text-chalkboard-10',
|
||||
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
|
||||
}}
|
||||
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
|
||||
data-testid="onboarding-prev"
|
||||
>
|
||||
{previousStep ? `Back` : 'Dismiss'}
|
||||
</ActionButton>
|
||||
{stepNumber !== undefined && (
|
||||
<p className="font-mono text-xs text-center m-0">
|
||||
{stepNumber} / {onboardingRoutes.length}
|
||||
</p>
|
||||
)}
|
||||
<ActionButton
|
||||
autoFocus
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
if (nextStep?.path) {
|
||||
onNextOverride ? onNextOverride() : goToNext()
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}}
|
||||
iconStart={{
|
||||
icon: nextStep ? 'arrowRight' : 'checkmark',
|
||||
bgClassName: 'dark:bg-chalkboard-80',
|
||||
}}
|
||||
className="dark:hover:bg-chalkboard-80/50"
|
||||
data-testid="onboarding-next"
|
||||
>
|
||||
{nextStep ? `Next` : 'Finish'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,8 @@ export const PACKAGE_NAME = isDesktop()
|
||||
|
||||
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) {
|
||||
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
|
||||
|
12
src/wasm-lib/Cargo.lock
generated
@ -2014,9 +2014,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miette"
|
||||
version = "7.4.0"
|
||||
version = "7.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64"
|
||||
checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"backtrace-ext",
|
||||
@ -2034,9 +2034,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miette-derive"
|
||||
version = "7.4.0"
|
||||
version = "7.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67"
|
||||
checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3273,9 +3273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.137"
|
||||
version = "1.0.138"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
dependencies = [
|
||||
"indexmap 2.7.1",
|
||||
"itoa",
|
||||
|
@ -15,7 +15,7 @@ gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
kittycad.workspace = true
|
||||
lazy_static = "1.5.0"
|
||||
serde_json = "1.0.135"
|
||||
serde_json = "1.0.138"
|
||||
tokio = { version = "1.41.1", features = ["sync"] }
|
||||
toml = "0.8.19"
|
||||
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
||||
|
@ -22,10 +22,15 @@ copy-exec-test-into-sim-test test_name:
|
||||
zoo kcl fmt -w kcl/tests/{{test_name}}/input.kcl
|
||||
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":
|
||||
mkdir kcl/tests/{{test_name}}
|
||||
touch kcl/tests/{{test_name}}/input.kcl
|
||||
# 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
|
||||
|
||||
# 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.
|
||||
{{cita}} -p kcl-lib -- simulation_tests::{{test_name}}::parse
|
||||
{{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" }
|
||||
pico-args = "0.5.0"
|
||||
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"] }
|
||||
|
||||
[lints]
|
||||
|
@ -37,7 +37,7 @@ kittycad = { workspace = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
lazy_static = "1.5.0"
|
||||
measurements = "0.11.0"
|
||||
miette = "7.2.0"
|
||||
miette = "7.5.0"
|
||||
mime_guess = "2.0.5"
|
||||
parse-display = "0.9.1"
|
||||
pyo3 = { version = "0.22.6", optional = true }
|
||||
@ -56,7 +56,7 @@ schemars = { version = "0.8.17", features = [
|
||||
"preserve_order",
|
||||
] }
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.135"
|
||||
serde_json = "1.0.138"
|
||||
sha2 = "0.10.8"
|
||||
tabled = { version = "0.15.0", optional = true }
|
||||
thiserror = "2.0.0"
|
||||
@ -116,7 +116,7 @@ expectorate = "1.1.0"
|
||||
handlebars = "6.3.0"
|
||||
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
||||
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
|
||||
miette = { version = "7.2.0", features = ["fancy"] }
|
||||
miette = { version = "7.5.0", features = ["fancy"] }
|
||||
pretty_assertions = "1.4.1"
|
||||
tokio = { version = "1.41.1", features = ["rt-multi-thread", "macros", "time"] }
|
||||
twenty-twenty = "0.8.0"
|
||||
|
@ -118,7 +118,7 @@ impl StdLibFnArg {
|
||||
} else if self.type_ == "KclValue" && self.required {
|
||||
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}"))))
|
||||
}
|
||||
|
||||
@ -136,6 +136,7 @@ impl StdLibFnArg {
|
||||
schema: &schemars::schema::Schema,
|
||||
index: usize,
|
||||
in_keyword_fn: bool,
|
||||
name: &str,
|
||||
) -> Result<Option<(usize, String)>> {
|
||||
match schema {
|
||||
schemars::schema::Schema::Object(o) => {
|
||||
@ -149,6 +150,10 @@ impl StdLibFnArg {
|
||||
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 (!in_keyword_fn && *nullable) || (in_keyword_fn && !self.include_in_snippet) {
|
||||
return Ok(None);
|
||||
@ -192,13 +197,9 @@ impl StdLibFnArg {
|
||||
continue;
|
||||
}
|
||||
|
||||
if prop_name == "color" {
|
||||
fn_docs.push_str(&format!("\t{} = ${{{}:\"#ff0000\"}},\n", prop_name, i));
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((new_index, snippet)) = self.get_autocomplete_snippet_from_schema(prop, i, false)? {
|
||||
if let Some((new_index, snippet)) =
|
||||
self.get_autocomplete_snippet_from_schema(prop, i, false, name)?
|
||||
{
|
||||
fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet));
|
||||
i = new_index + 1;
|
||||
}
|
||||
@ -223,7 +224,8 @@ impl StdLibFnArg {
|
||||
.get_autocomplete_snippet_from_schema(
|
||||
items,
|
||||
index + (v as usize),
|
||||
in_keyword_fn
|
||||
in_keyword_fn,
|
||||
name
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
@ -238,7 +240,7 @@ impl StdLibFnArg {
|
||||
index,
|
||||
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"))?
|
||||
.1
|
||||
),
|
||||
@ -250,7 +252,7 @@ impl StdLibFnArg {
|
||||
index,
|
||||
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"))?
|
||||
.1
|
||||
),
|
||||
@ -293,7 +295,7 @@ impl StdLibFnArg {
|
||||
return Ok(Some((index, parsed_enum_values[0].to_string())));
|
||||
} else if let Some(item) = items.iter().next() {
|
||||
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;
|
||||
fn_docs.push_str(&snippet);
|
||||
@ -302,7 +304,7 @@ impl StdLibFnArg {
|
||||
} else if let Some(items) = &subschemas.any_of {
|
||||
if let Some(item) = items.iter().next() {
|
||||
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;
|
||||
fn_docs.push_str(&snippet);
|
||||
@ -1018,12 +1020,7 @@ mod tests {
|
||||
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"appearance({
|
||||
color = ${0:"#
|
||||
.to_owned()
|
||||
+ "\"#"
|
||||
+ r#"ff0000"},
|
||||
}, ${1:%})${}"#
|
||||
r#"appearance(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})${}"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -1038,12 +1035,7 @@ mod tests {
|
||||
fn get_autocomplete_snippet_sweep() {
|
||||
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
|
||||
let snippet = sweep_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"sweep({
|
||||
path = ${0:sketch000},
|
||||
}, ${1:%})${}"#
|
||||
);
|
||||
assert_eq!(snippet, r#"sweep(${0:%}, path = ${1:sketch000})${}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -5,7 +5,8 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
use crate::{
|
||||
execution::{ArtifactCommand, ArtifactGraph, Operation},
|
||||
lsp::IntoDiagnostic,
|
||||
source_range::{ModuleId, SourceRange},
|
||||
source_range::SourceRange,
|
||||
ModuleId,
|
||||
};
|
||||
|
||||
/// How did the KCL execution fail
|
||||
|
@ -10,6 +10,7 @@ use crate::{
|
||||
pub(crate) const SETTINGS: &str = "settings";
|
||||
pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
|
||||
pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
|
||||
pub(super) const NO_PRELUDE: &str = "no_prelude";
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) enum AnnotationScope {
|
||||
@ -23,7 +24,7 @@ pub(super) fn expect_properties<'a>(
|
||||
) -> Result<&'a [Node<ObjectProperty>], KclError> {
|
||||
match annotation {
|
||||
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(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Empty `{for_key}` annotation"),
|
||||
|
@ -269,6 +269,17 @@ pub struct EdgeCutEdge {
|
||||
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)]
|
||||
#[ts(export_to = "Artifact.ts")]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
@ -295,6 +306,7 @@ pub enum Artifact {
|
||||
SweepEdge(SweepEdge),
|
||||
EdgeCut(EdgeCut),
|
||||
EdgeCutEdge(EdgeCutEdge),
|
||||
Helix(Helix),
|
||||
}
|
||||
|
||||
impl Artifact {
|
||||
@ -312,6 +324,7 @@ impl Artifact {
|
||||
Artifact::SweepEdge(a) => a.id,
|
||||
Artifact::EdgeCut(a) => a.id,
|
||||
Artifact::EdgeCutEdge(a) => a.id,
|
||||
Artifact::Helix(a) => a.id,
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,6 +344,7 @@ impl Artifact {
|
||||
Artifact::SweepEdge(_) => None,
|
||||
Artifact::EdgeCut(a) => Some(&a.code_ref),
|
||||
Artifact::EdgeCutEdge(_) => None,
|
||||
Artifact::Helix(a) => Some(&a.code_ref),
|
||||
}
|
||||
}
|
||||
|
||||
@ -350,6 +364,7 @@ impl Artifact {
|
||||
Artifact::SweepEdge(_) => Some(new),
|
||||
Artifact::EdgeCut(a) => a.merge(new),
|
||||
Artifact::EdgeCutEdge(_) => Some(new),
|
||||
Artifact::Helix(_) => Some(new),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -924,6 +939,25 @@ fn artifacts_to_update(
|
||||
}
|
||||
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::EdgeCut(a) => vec![a.consumed_edge_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.
|
||||
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::SweepEdge(_)
|
||||
| Artifact::EdgeCut(_)
|
||||
| Artifact::EdgeCutEdge(_) => false,
|
||||
| Artifact::EdgeCutEdge(_)
|
||||
| Artifact::Helix(_) => false,
|
||||
};
|
||||
if !grouped {
|
||||
ungrouped.push(id);
|
||||
@ -341,6 +347,14 @@ impl ArtifactGraph {
|
||||
Artifact::EdgeCutEdge(_edge_cut_edge) => {
|
||||
writeln!(output, "{prefix}{}[EdgeCutEdge]", id)?;
|
||||
}
|
||||
Artifact::Helix(helix) => {
|
||||
writeln!(
|
||||
output,
|
||||
"{prefix}{}[\"Helix<br>{:?}\"]",
|
||||
id,
|
||||
code_ref_display(&helix.code_ref)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -522,6 +536,9 @@ impl ArtifactGraph {
|
||||
Artifact::EdgeCutEdge(_edge_cut_edge) => {
|
||||
writeln!(output, "{prefix}EdgeCutEdge")?;
|
||||
}
|
||||
Artifact::Helix(_) => {
|
||||
writeln!(output, "{prefix}Helix")?;
|
||||
}
|
||||
}
|
||||
|
||||
if ids_seen.contains(&artifact.id()) {
|
||||
|
@ -128,7 +128,7 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
|
||||
properties: new_properties,
|
||||
},
|
||||
) => {
|
||||
name.digest == new_name.digest
|
||||
name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
|
||||
&& properties
|
||||
.as_ref()
|
||||
.map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
|
||||
@ -242,7 +242,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
@ -273,7 +273,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
@ -285,7 +285,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program_old, ctx, _) = parse_execute(old).await.unwrap();
|
||||
|
||||
@ -318,7 +318,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
@ -330,7 +330,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, ctx, _) = parse_execute(old).await.unwrap();
|
||||
|
||||
@ -363,7 +363,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
|
||||
|
||||
let new = r#"// Remove the end face for the extrusion.
|
||||
firstSketch = startSketchOn('XY')
|
||||
@ -375,7 +375,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, ctx, _) = parse_execute(old).await.unwrap();
|
||||
|
||||
@ -409,7 +409,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
@ -451,7 +451,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
@ -486,7 +486,7 @@ firstSketch = startSketchOn('XY')
|
||||
|> extrude(length = 6)
|
||||
|
||||
// Remove the end face for the extrusion.
|
||||
shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
|
||||
|
@ -10,17 +10,17 @@ use crate::{
|
||||
annotations,
|
||||
cad_op::{OpArg, Operation},
|
||||
state::ModuleState,
|
||||
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModuleRepr, ProgramMemory,
|
||||
TagEngineInfo, TagIdentifier,
|
||||
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ProgramMemory, TagEngineInfo,
|
||||
TagIdentifier,
|
||||
},
|
||||
fs::FileSystem,
|
||||
modules::{ModuleId, ModulePath, ModuleRepr},
|
||||
parsing::ast::types::{
|
||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
||||
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
|
||||
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression,
|
||||
PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator,
|
||||
},
|
||||
source_range::{ModuleId, SourceRange},
|
||||
source_range::SourceRange,
|
||||
std::{
|
||||
args::{Arg, KwArgs},
|
||||
FunctionKind,
|
||||
@ -38,7 +38,8 @@ impl ExecutorContext {
|
||||
annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>,
|
||||
scope: annotations::AnnotationScope,
|
||||
exec_state: &mut ExecState,
|
||||
) -> Result<(), KclError> {
|
||||
) -> Result<bool, KclError> {
|
||||
let mut no_prelude = false;
|
||||
for (annotation, source_range) in annotations {
|
||||
if annotation.annotation_name() == Some(annotations::SETTINGS) {
|
||||
if scope == annotations::AnnotationScope::Module {
|
||||
@ -48,7 +49,7 @@ impl ExecutorContext {
|
||||
.settings
|
||||
.update_from_annotation(annotation, source_range)?;
|
||||
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?;
|
||||
}
|
||||
} 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
|
||||
}
|
||||
Ok(())
|
||||
Ok(no_prelude)
|
||||
}
|
||||
|
||||
/// Execute an AST's program.
|
||||
@ -71,22 +82,32 @@ impl ExecutorContext {
|
||||
exec_state: &mut ExecState,
|
||||
body_type: BodyType,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
self.handle_annotations(
|
||||
program
|
||||
.non_code_meta
|
||||
.start_nodes
|
||||
.iter()
|
||||
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
|
||||
annotations::AnnotationScope::Module,
|
||||
exec_state,
|
||||
)
|
||||
.await?;
|
||||
if body_type == BodyType::Root {
|
||||
let _no_prelude = self
|
||||
.handle_annotations(
|
||||
program
|
||||
.non_code_meta
|
||||
.start_nodes
|
||||
.iter()
|
||||
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
|
||||
annotations::AnnotationScope::Module,
|
||||
exec_state,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut last_expr = None;
|
||||
// Iterate over the body of the program.
|
||||
for statement in &program.body {
|
||||
for (i, statement) in program.body.iter().enumerate() {
|
||||
match statement {
|
||||
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 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 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
|
||||
.execute_expr(
|
||||
&variable_declaration.declaration.init,
|
||||
@ -231,63 +260,45 @@ impl ExecutorContext {
|
||||
exec_state: &mut ExecState,
|
||||
source_range: SourceRange,
|
||||
) -> Result<ModuleId, KclError> {
|
||||
let resolved_path = ModulePath::from_import_path(path, &self.settings.project_directory);
|
||||
match path {
|
||||
ImportPath::Kcl { filename } => {
|
||||
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
||||
project_dir.join(filename)
|
||||
} else {
|
||||
std::path::PathBuf::from(filename)
|
||||
};
|
||||
ImportPath::Kcl { .. } => {
|
||||
exec_state.global.mod_loader.cycle_check(&resolved_path, source_range)?;
|
||||
|
||||
if exec_state.mod_local.import_stack.contains(&resolved_path) {
|
||||
return Err(KclError::ImportCycle(KclErrorDetails {
|
||||
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.id_for_module(&resolved_path) {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
|
||||
return Ok(*id);
|
||||
}
|
||||
|
||||
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());
|
||||
let id = exec_state.next_module_id();
|
||||
let source = resolved_path.source(&self.fs, source_range).await?;
|
||||
// TODO handle parsing errors properly
|
||||
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
|
||||
let repr = ModuleRepr::Kcl(parsed);
|
||||
|
||||
Ok(exec_state.add_module(id, resolved_path, repr))
|
||||
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed));
|
||||
Ok(id)
|
||||
}
|
||||
ImportPath::Foreign { path } => {
|
||||
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
||||
project_dir.join(path)
|
||||
} else {
|
||||
std::path::PathBuf::from(path)
|
||||
};
|
||||
|
||||
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
|
||||
return Ok(*id);
|
||||
ImportPath::Foreign { .. } => {
|
||||
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
let geom = super::import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?;
|
||||
let repr = ModuleRepr::Foreign(geom);
|
||||
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
|
||||
Ok(exec_state.add_module(id, resolved_path, repr))
|
||||
let id = exec_state.next_module_id();
|
||||
let geom =
|
||||
super::import::import_foreign(resolved_path.expect_path(), None, exec_state, self, source_range)
|
||||
.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!(
|
||||
"circular import of modules is not allowed: {} -> {}",
|
||||
exec_state
|
||||
.mod_local
|
||||
.global
|
||||
.mod_loader
|
||||
.import_stack
|
||||
.iter()
|
||||
.map(|p| p.as_path().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" -> "),
|
||||
info.path.display()
|
||||
info.path
|
||||
),
|
||||
source_ranges: vec![source_range],
|
||||
})),
|
||||
ModuleRepr::Kcl(program) => {
|
||||
let mut local_state = ModuleState {
|
||||
import_stack: exec_state.mod_local.import_stack.clone(),
|
||||
..ModuleState::new(&self.settings)
|
||||
};
|
||||
local_state.import_stack.push(info.path.clone());
|
||||
let mut local_state = ModuleState::new(&self.settings);
|
||||
exec_state.global.mod_loader.enter_module(&info.path);
|
||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||
let original_execution = self.engine.replace_execution_kind(exec_kind);
|
||||
|
||||
@ -332,7 +341,8 @@ impl ExecutorContext {
|
||||
|
||||
let new_units = exec_state.length_unit();
|
||||
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.replace_execution_kind(original_execution);
|
||||
@ -345,7 +355,7 @@ impl ExecutorContext {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Error loading imported file. Open it to view more details. {}: {}",
|
||||
info.path.display(),
|
||||
info.path,
|
||||
err.message()
|
||||
),
|
||||
source_ranges: vec![source_range],
|
||||
|
@ -32,6 +32,16 @@ impl Geometry {
|
||||
Geometry::Solid(e) => e.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// If this geometry is the result of a pattern, then return the ID of
|
||||
/// the original sketch which was patterned.
|
||||
/// Equivalent to the `id()` method if this isn't a pattern.
|
||||
pub fn original_id(&self) -> uuid::Uuid {
|
||||
match self {
|
||||
Geometry::Sketch(s) => s.original_id,
|
||||
Geometry::Solid(e) => e.sketch.original_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of geometry.
|
||||
@ -419,6 +429,8 @@ pub struct Sketch {
|
||||
/// The original id of the sketch. This stays the same even if the sketch is
|
||||
/// is sketched on face etc.
|
||||
pub artifact_id: ArtifactId,
|
||||
#[ts(skip)]
|
||||
pub original_id: uuid::Uuid,
|
||||
pub units: UnitLen,
|
||||
/// Metadata.
|
||||
#[serde(rename = "__meta")]
|
||||
|
@ -9,7 +9,9 @@ use cache::OldAstState;
|
||||
pub use cad_op::Operation;
|
||||
pub use exec_ast::FunctionParam;
|
||||
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;
|
||||
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
|
||||
use kcmc::{
|
||||
@ -34,7 +36,7 @@ use crate::{
|
||||
fs::FileManager,
|
||||
parsing::ast::types::{Expr, FunctionExpression, Node, NodeRef, Program},
|
||||
settings::types::UnitLength,
|
||||
source_range::{ModuleId, SourceRange},
|
||||
source_range::SourceRange,
|
||||
std::{args::Arg, StdLib},
|
||||
ExecError, KclErrorWithOutputs,
|
||||
};
|
||||
@ -162,25 +164,6 @@ pub enum BodyType {
|
||||
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.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
|
||||
#[ts(export)]
|
||||
@ -791,7 +774,7 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::errors::KclErrorDetails;
|
||||
use crate::{errors::KclErrorDetails, ModuleId};
|
||||
|
||||
/// Convenience function to get a JSON value from memory and unwrap.
|
||||
#[track_caller]
|
||||
@ -1590,10 +1573,10 @@ let w = f() + f()
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 40.14)
|
||||
|> shell({
|
||||
faces: [seg01],
|
||||
thickness: 3.14,
|
||||
}, %)
|
||||
|> shell(
|
||||
thickness = 3.14,
|
||||
faces = [seg01]
|
||||
)
|
||||
"#;
|
||||
|
||||
let ctx = crate::test_server::new_context(UnitLength::Mm, true, None)
|
||||
@ -1620,10 +1603,10 @@ let w = f() + f()
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 40.14)
|
||||
|> shell({
|
||||
faces: [seg01],
|
||||
thickness: 3.14,
|
||||
}, %)
|
||||
|> shell(
|
||||
faces = [seg01],
|
||||
thickness = 3.14,
|
||||
)
|
||||
"#;
|
||||
|
||||
// Execute a slightly different program again.
|
||||
|
@ -9,10 +9,11 @@ use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{
|
||||
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,
|
||||
source_range::{ModuleId, SourceRange},
|
||||
source_range::SourceRange,
|
||||
};
|
||||
|
||||
/// State for executing a program.
|
||||
@ -29,7 +30,7 @@ pub struct GlobalState {
|
||||
/// The stable artifact ID generator.
|
||||
pub id_generator: IdGenerator,
|
||||
/// 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.
|
||||
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
|
||||
/// Output map of UUIDs to artifacts.
|
||||
@ -45,6 +46,8 @@ pub struct GlobalState {
|
||||
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
|
||||
/// Output artifact graph.
|
||||
pub artifact_graph: ArtifactGraph,
|
||||
/// Module loader.
|
||||
pub mod_loader: ModuleLoader,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@ -59,9 +62,6 @@ pub struct ModuleState {
|
||||
pub pipe_value: Option<KclValue>,
|
||||
/// Identifiers that have been exported from the current module.
|
||||
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
|
||||
/// the Feature Tree.
|
||||
pub operations: Vec<Operation>,
|
||||
@ -124,15 +124,21 @@ impl ExecState {
|
||||
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));
|
||||
|
||||
self.global.path_to_source_id.insert(path.clone(), id);
|
||||
|
||||
let module_info = ModuleInfo { id, repr, path };
|
||||
self.global.module_infos.insert(id, module_info);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
pub fn length_unit(&self) -> UnitLen {
|
||||
@ -154,6 +160,7 @@ impl GlobalState {
|
||||
artifact_commands: Default::default(),
|
||||
artifact_responses: Default::default(),
|
||||
artifact_graph: Default::default(),
|
||||
mod_loader: Default::default(),
|
||||
};
|
||||
|
||||
let root_id = ModuleId::default();
|
||||
@ -162,11 +169,11 @@ impl GlobalState {
|
||||
root_id,
|
||||
ModuleInfo {
|
||||
id: root_id,
|
||||
path: root_path.clone(),
|
||||
path: ModulePath::Local(root_path.clone()),
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -178,7 +185,6 @@ impl ModuleState {
|
||||
dynamic_state: Default::default(),
|
||||
pipe_value: Default::default(),
|
||||
module_exports: Default::default(),
|
||||
import_stack: Default::default(),
|
||||
operations: Default::default(),
|
||||
settings: MetaSettings {
|
||||
default_length_units: exec_settings.units.into(),
|
||||
|
@ -65,6 +65,7 @@ mod fs;
|
||||
pub mod lint;
|
||||
mod log;
|
||||
mod lsp;
|
||||
mod modules;
|
||||
mod parsing;
|
||||
mod settings;
|
||||
#[cfg(test)]
|
||||
@ -87,9 +88,10 @@ pub use lsp::{
|
||||
copilot::Backend as CopilotLspBackend,
|
||||
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
|
||||
};
|
||||
pub use modules::ModuleId;
|
||||
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
|
||||
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.
|
||||
// Ideally we wouldn't export these things at all, they should only be used for testing.
|
||||
@ -159,8 +161,8 @@ impl Program {
|
||||
}
|
||||
|
||||
/// Get the meta settings for the kcl file from the annotations.
|
||||
pub fn get_meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
|
||||
self.ast.get_meta_settings()
|
||||
pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
|
||||
self.ast.meta_settings()
|
||||
}
|
||||
|
||||
/// Change the meta settings for the kcl file.
|
||||
|
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,
|
||||
properties,
|
||||
} => {
|
||||
hasher.update(name.compute_digest());
|
||||
if let Some(name) = name {
|
||||
hasher.update(name.compute_digest());
|
||||
}
|
||||
if let Some(properties) = properties {
|
||||
hasher.update(properties.len().to_ne_bytes());
|
||||
for property in properties.iter_mut() {
|
||||
|
@ -4,7 +4,7 @@ pub mod types;
|
||||
|
||||
use crate::{
|
||||
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject},
|
||||
source_range::ModuleId,
|
||||
ModuleId,
|
||||
};
|
||||
|
||||
impl BodyItem {
|
||||
|