fix: merging main?

This commit is contained in:
Kevin Nadro
2025-02-07 16:08:03 -06:00
129 changed files with 16362 additions and 3745 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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')

View File

@ -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()

View File

@ -782,6 +782,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 },
@ -972,7 +1037,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')
@ -1891,7 +1956,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)
@ -1992,8 +2057,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)
@ -2036,7 +2100,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)
@ -2090,9 +2154,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')
@ -2160,7 +2223,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)

View File

@ -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)`
)
})

View File

@ -1187,9 +1187,7 @@ sweepSketch = startSketchOn('XY')
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> sweep(path = sweepPath)
|> appearance({
color = "#bb00ff",
metalness = 90,
@ -1235,9 +1233,7 @@ sweepSketch = startSketchOn('XY')
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> sweep(path = sweepPath)
|> appearance({
color = "#bb00ff",
metalness = 90,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -3,80 +3,81 @@ import { test, expect } from './zoo-test'
import { commonPoints, getUtils } from './test-utils'
test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
test(
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await homePage.goToModelingScene()
const networkToggle = page.getByTestId('network-toggle')
const networkToggle = page.getByTestId('network-toggle')
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// Click the network widget
await networkWidget.click()
// Click the network widget
await networkWidget.click()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Click the network widget
await networkWidget.click()
// Click the network widget
await networkWidget.click()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
})
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
}
)
test('Engine disconnect & reconnect in sketch mode', async ({
page,

View File

@ -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-shell/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",
@ -159,7 +159,7 @@
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6",
"@types/mocha": "^10.0.10",
"@types/node": "^22.13.1",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",

View File

@ -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> }

View 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)))))

View File

@ -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' })

View File

@ -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 (

View File

@ -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:

View File

@ -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`}
>

View File

@ -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

View File

@ -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',
}

View File

@ -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',

View File

@ -221,6 +221,7 @@ const newVar = myVar + 1`
},
],
id: expect.any(String),
originalId: expect.any(String),
artifactId: expect.any(String),
units: {
type: 'Mm',

View File

@ -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-shell',
URL_GIT_KCL_SAMPLES,
DIR_KCL_SAMPLES,
])
// @ts-expect-error
let files = await fs.readdir(DIR_KCL_SAMPLES)

View File

@ -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
*/

View File

@ -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,

View File

@ -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',

View File

@ -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()

View File

@ -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`

View File

@ -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)
},
},

View File

@ -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)
})
})

View File

@ -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),
})

View File

@ -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' }],

View File

@ -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',

View File

@ -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,

View File

@ -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) => {

View File

@ -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>

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>

View File

@ -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>
)

View File

@ -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>

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>

View File

@ -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>

View File

@ -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>
)

View File

@ -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>
</>
)
}

View File

@ -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/${

View File

@ -1038,12 +1038,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]

View File

@ -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();

View File

@ -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")]

View File

@ -1590,10 +1590,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 +1620,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.

View File

@ -160,7 +160,7 @@ impl Program {
/// Get the meta settings for the kcl file from the annotations.
pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
self.ast.get_meta_settings()
self.ast.meta_settings()
}
/// Change the meta settings for the kcl file.

View File

@ -269,7 +269,7 @@ impl Node<Program> {
}
/// Get the annotations for the meta settings from the kcl file.
pub fn get_meta_settings(&self) -> Result<Option<crate::execution::MetaSettings>, KclError> {
pub fn meta_settings(&self) -> Result<Option<crate::execution::MetaSettings>, KclError> {
for annotation_node in self.annotations() {
let annotation = &annotation_node.value;
if annotation.annotation_name() == Some(annotations::SETTINGS) {
@ -3881,7 +3881,7 @@ const cylinder = startSketchOn('-XZ')
startSketchOn('XY')"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
let result = program.meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
@ -3897,7 +3897,7 @@ startSketchOn('XY')"#;
startSketchOn('XY')"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
let result = program.meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
@ -3914,7 +3914,7 @@ startSketchOn('XY')"#;
})
.unwrap();
let result = new_program.get_meta_settings().unwrap();
let result = new_program.meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
@ -3939,7 +3939,7 @@ startSketchOn('XY')
async fn test_parse_get_meta_settings_nothing_to_mm() {
let some_program_string = r#"startSketchOn('XY')"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
let result = program.meta_settings().unwrap();
assert!(result.is_none());
// Edit the ast.
@ -3950,7 +3950,7 @@ startSketchOn('XY')
})
.unwrap();
let result = new_program.get_meta_settings().unwrap();
let result = new_program.meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();

View File

@ -120,10 +120,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// |> close()
/// |> extrude(length = 6)
///
/// shell({
/// shell(
/// firstSketch,
/// faces = ['end'],
/// thickness = 0.25,
/// }, firstSketch)
/// )
/// |> appearance({
/// color = '#ff0000',
/// metalness = 90,
@ -147,10 +148,11 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// roughness = 90
/// }, %)
///
/// shell({
/// shell(
/// firstSketch,
/// faces = ['end'],
/// thickness = 0.25,
/// }, firstSketch)
/// )
/// ```
///
/// ```no_run
@ -252,9 +254,7 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// radius = 2,
/// }, %)
/// |> hole(pipeHole, %)
/// |> sweep({
/// path: sweepPath,
/// }, %)
/// |> sweep(path = sweepPath)
/// |> appearance({
/// color: "#ff0000",
/// metalness: 50,

View File

@ -356,7 +356,7 @@ impl Args {
Ok(numbers)
}
pub(crate) fn get_pattern_transform_args(&self) -> Result<(u32, FnAsArg<'_>, SolidSet), KclError> {
pub(crate) fn get_pattern_transform_args(&self) -> Result<(u32, FnAsArg<'_>, SolidSet, Option<bool>), KclError> {
FromArgs::from_args(self, 0)
}
@ -764,6 +764,10 @@ macro_rules! let_field_of {
($obj:ident, $field:ident?) => {
let $field = $obj.get(stringify!($field)).and_then(FromKclValue::from_kcl_val);
};
// Optional field but with a different string used as the key
($obj:ident, $field:ident? $key:literal) => {
let $field = $obj.get($key).and_then(FromKclValue::from_kcl_val);
};
// Mandatory field, but with a different string used as the key.
($obj:ident, $field:ident $key:literal) => {
let $field = $obj.get($key).and_then(FromKclValue::from_kcl_val)?;
@ -947,12 +951,14 @@ impl<'a> FromKclValue<'a> for super::patterns::CircularPattern3dData {
let_field_of!(obj, rotate_duplicates "rotateDuplicates");
let_field_of!(obj, axis);
let_field_of!(obj, center);
let_field_of!(obj, use_original? "useOriginal");
Some(Self {
instances,
axis,
center,
arc_degrees,
rotate_duplicates,
use_original,
})
}
}
@ -964,11 +970,13 @@ impl<'a> FromKclValue<'a> for super::patterns::CircularPattern2dData {
let_field_of!(obj, arc_degrees "arcDegrees");
let_field_of!(obj, rotate_duplicates "rotateDuplicates");
let_field_of!(obj, center);
let_field_of!(obj, use_original? "useOriginal");
Some(Self {
instances,
center,
arc_degrees,
rotate_duplicates,
use_original,
})
}
}
@ -1011,15 +1019,6 @@ impl<'a> FromKclValue<'a> for super::sketch::BezierData {
}
}
impl<'a> FromKclValue<'a> for super::shell::ShellData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, thickness);
let_field_of!(obj, faces);
Some(Self { thickness, faces })
}
}
impl<'a> FromKclValue<'a> for super::chamfer::ChamferData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
@ -1043,20 +1042,6 @@ impl<'a> FromKclValue<'a> for super::fillet::FilletData {
}
}
impl<'a> FromKclValue<'a> for super::sweep::SweepData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;
let_field_of!(obj, path);
let_field_of!(obj, sectional?);
let_field_of!(obj, tolerance?);
Some(Self {
path,
sectional,
tolerance,
})
}
}
impl<'a> FromKclValue<'a> for super::appearance::AppearanceData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?;

View File

@ -43,7 +43,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('YZ')
/// |> circle({ center = [0, 0], radius = 0.5 }, %)
/// |> sweep({ path = helixPath }, %)
/// |> sweep(path = helixPath)
/// ```
///
/// ```no_run
@ -64,7 +64,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('XY')
/// |> circle({ center = [0, 0], radius = 0.5 }, %)
/// |> sweep({ path = helixPath }, %)
/// |> sweep(path = helixPath)
/// ```
///
/// ```no_run
@ -86,7 +86,7 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('XY')
/// |> circle({ center = [0, 0], radius = 1 }, %)
/// |> sweep({ path = helixPath }, %)
/// |> sweep(path = helixPath)
/// ```
#[stdlib {
name = "helix",

View File

@ -63,7 +63,7 @@ pub struct LinearPattern3dData {
/// Repeat some 3D solid, changing each repetition slightly.
pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (num_repetitions, transform, extr) = args.get_pattern_transform_args()?;
let (num_repetitions, transform, extr, use_original) = args.get_pattern_transform_args()?;
let solids = inner_pattern_transform(
num_repetitions,
@ -75,6 +75,7 @@ pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result
memory: *transform.memory,
},
extr,
use_original,
exec_state,
&args,
)
@ -84,7 +85,7 @@ pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result
/// Repeat some 2D sketch, changing each repetition slightly.
pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (num_repetitions, transform, sketch): (u32, super::FnAsArg<'_>, SketchSet) =
let (num_repetitions, transform, sketch, use_original): (u32, super::FnAsArg<'_>, SketchSet, Option<bool>) =
super::args::FromArgs::from_args(&args, 0)?;
let sketches = inner_pattern_transform_2d(
@ -97,6 +98,7 @@ pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Res
memory: *transform.memory,
},
sketch,
use_original,
exec_state,
&args,
)
@ -295,6 +297,7 @@ async fn inner_pattern_transform<'a>(
total_instances: u32,
transform_function: FunctionParam<'a>,
solid_set: SolidSet,
use_original: Option<bool>,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<Box<Solid>>, KclError> {
@ -310,7 +313,7 @@ async fn inner_pattern_transform<'a>(
let t = make_transform::<Box<Solid>>(i, &transform_function, args.source_range, exec_state).await?;
transform.push(t);
}
execute_pattern_transform(transform, solid_set, exec_state, args).await
execute_pattern_transform(transform, solid_set, use_original.unwrap_or_default(), exec_state, args).await
}
/// Just like patternTransform, but works on 2D sketches not 3D solids.
@ -332,6 +335,7 @@ async fn inner_pattern_transform_2d<'a>(
total_instances: u32,
transform_function: FunctionParam<'a>,
solid_set: SketchSet,
use_original: Option<bool>,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<Box<Sketch>>, KclError> {
@ -347,12 +351,13 @@ async fn inner_pattern_transform_2d<'a>(
let t = make_transform::<Box<Sketch>>(i, &transform_function, args.source_range, exec_state).await?;
transform.push(t);
}
execute_pattern_transform(transform, solid_set, exec_state, args).await
execute_pattern_transform(transform, solid_set, use_original.unwrap_or_default(), exec_state, args).await
}
async fn execute_pattern_transform<T: GeometryTrait>(
transforms: Vec<Vec<Transform>>,
geo_set: T::Set,
use_original: bool,
exec_state: &mut ExecState,
args: &Args,
) -> Result<Vec<T>, KclError> {
@ -368,7 +373,7 @@ async fn execute_pattern_transform<T: GeometryTrait>(
let mut output = Vec::new();
for geo in starting {
let new = send_pattern_transform(transforms.clone(), &geo, exec_state, args).await?;
let new = send_pattern_transform(transforms.clone(), &geo, use_original, exec_state, args).await?;
output.extend(new)
}
Ok(output)
@ -379,6 +384,7 @@ async fn send_pattern_transform<T: GeometryTrait>(
// https://github.com/KittyCAD/modeling-app/issues/2821
transforms: Vec<Vec<Transform>>,
solid: &T,
use_original: bool,
exec_state: &mut ExecState,
args: &Args,
) -> Result<Vec<T>, KclError> {
@ -388,7 +394,7 @@ async fn send_pattern_transform<T: GeometryTrait>(
.send_modeling_cmd(
id,
ModelingCmd::from(mcmd::EntityLinearPatternTransform {
entity_id: solid.id(),
entity_id: if use_original { solid.original_id() } else { solid.id() },
transform: Default::default(),
transforms,
}),
@ -602,6 +608,7 @@ fn array_to_point2d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<P
trait GeometryTrait: Clone {
type Set: Into<Vec<Self>> + Clone;
fn id(&self) -> Uuid;
fn original_id(&self) -> Uuid;
fn set_id(&mut self, id: Uuid);
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError>;
async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: Self::Set) -> Result<(), KclError>;
@ -615,6 +622,9 @@ impl GeometryTrait for Box<Sketch> {
fn id(&self) -> Uuid {
self.id
}
fn original_id(&self) -> Uuid {
self.original_id
}
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
let Point2d { x, y } = array_to_point2d(val, source_ranges)?;
Ok(Point3d { x, y, z: 0.0 })
@ -634,6 +644,11 @@ impl GeometryTrait for Box<Solid> {
fn id(&self) -> Uuid {
self.id
}
fn original_id(&self) -> Uuid {
self.sketch.original_id
}
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
array_to_point3d(val, source_ranges)
}
@ -674,7 +689,8 @@ mod tests {
/// A linear pattern on a 2D sketch.
pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch_set): (LinearPattern2dData, SketchSet) = args.get_data_and_sketch_set()?;
let (data, sketch_set, use_original): (LinearPattern2dData, SketchSet, Option<bool>) =
super::args::FromArgs::from_args(&args, 0)?;
if data.axis == [0.0, 0.0] {
return Err(KclError::Semantic(KclErrorDetails {
@ -685,7 +701,7 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
}));
}
let sketches = inner_pattern_linear_2d(data, sketch_set, exec_state, args).await?;
let sketches = inner_pattern_linear_2d(data, sketch_set, use_original, exec_state, args).await?;
Ok(sketches.into())
}
@ -709,6 +725,7 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
async fn inner_pattern_linear_2d(
data: LinearPattern2dData,
sketch_set: SketchSet,
use_original: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Box<Sketch>>, KclError> {
@ -726,12 +743,20 @@ async fn inner_pattern_linear_2d(
}]
})
.collect();
execute_pattern_transform(transforms, sketch_set, exec_state, &args).await
execute_pattern_transform(
transforms,
sketch_set,
use_original.unwrap_or_default(),
exec_state,
&args,
)
.await
}
/// A linear pattern on a 3D model.
pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid_set): (LinearPattern3dData, SolidSet) = args.get_data_and_solid_set()?;
let (data, solid_set, use_original): (LinearPattern3dData, SolidSet, Option<bool>) =
super::args::FromArgs::from_args(&args, 0)?;
if data.axis == [0.0, 0.0, 0.0] {
return Err(KclError::Semantic(KclErrorDetails {
@ -742,7 +767,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
}));
}
let solids = inner_pattern_linear_3d(data, solid_set, exec_state, args).await?;
let solids = inner_pattern_linear_3d(data, solid_set, use_original, exec_state, args).await?;
Ok(solids.into())
}
@ -771,6 +796,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
async fn inner_pattern_linear_3d(
data: LinearPattern3dData,
solid_set: SolidSet,
use_original: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Box<Solid>>, KclError> {
@ -788,7 +814,14 @@ async fn inner_pattern_linear_3d(
}]
})
.collect();
execute_pattern_transform(transforms, solid_set, exec_state, &args).await
execute_pattern_transform(
transforms,
solid_set,
use_original.unwrap_or_default(),
exec_state,
&args,
)
.await
}
/// Data for a circular pattern on a 2D sketch.
@ -807,6 +840,10 @@ pub struct CircularPattern2dData {
pub arc_degrees: f64,
/// Whether or not to rotate the duplicates as they are copied.
pub rotate_duplicates: bool,
/// If the target being patterned is itself a pattern, then, should you use the original solid,
/// or the pattern?
#[serde(default)]
pub use_original: Option<bool>,
}
/// Data for a circular pattern on a 3D model.
@ -827,6 +864,10 @@ pub struct CircularPattern3dData {
pub arc_degrees: f64,
/// Whether or not to rotate the duplicates as they are copied.
pub rotate_duplicates: bool,
/// If the target being patterned is itself a pattern, then, should you use the original solid,
/// or the pattern?
#[serde(default)]
pub use_original: Option<bool>,
}
pub enum CircularPattern {
@ -889,6 +930,13 @@ impl CircularPattern {
CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
}
}
pub fn use_original(&self) -> bool {
match self {
CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
}
}
}
/// A circular pattern on a 2D sketch.
@ -1055,7 +1103,11 @@ async fn pattern_circular(
id,
ModelingCmd::from(mcmd::EntityCircularPattern {
axis: kcmc::shared::Point3d::from(data.axis()),
entity_id: geometry.id(),
entity_id: if data.use_original() {
geometry.original_id()
} else {
geometry.id()
},
center: kcmc::shared::Point3d {
x: LengthUnit(center[0]),
y: LengthUnit(center[1]),

View File

@ -4,8 +4,6 @@ use anyhow::Result;
use derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
@ -13,22 +11,13 @@ use crate::{
std::{sketch::FaceTag, Args},
};
/// Data for shells.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ShellData {
/// The thickness of the shell.
pub thickness: f64,
/// The faces you want removed.
pub faces: Vec<FaceTag>,
}
/// Create a shell.
pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid_set): (ShellData, SolidSet) = args.get_data_and_solid_set()?;
let solid_set = args.get_unlabeled_kw_arg("solidSet")?;
let thickness = args.get_kw_arg("thickness")?;
let faces = args.get_kw_arg("faces")?;
let result = inner_shell(data, solid_set, exec_state, args).await?;
let result = inner_shell(solid_set, thickness, faces, exec_state, args).await?;
Ok(result.into())
}
@ -47,10 +36,11 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 6)
///
/// // Remove the end face for the extrusion.
/// shell({
/// shell(
/// firstSketch,
/// faces = ['end'],
/// thickness = 0.25,
/// }, firstSketch)
/// )
/// ```
///
/// ```no_run
@ -64,10 +54,11 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 6)
///
/// // Remove the start face for the extrusion.
/// shell({
/// shell(
/// firstSketch,
/// faces = ['start'],
/// thickness = 0.25,
/// }, firstSketch)
/// )
/// ```
///
/// ```no_run
@ -81,10 +72,11 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 6)
///
/// // Remove a tagged face for the extrusion.
/// shell({
/// shell(
/// firstSketch,
/// faces = [myTag],
/// thickness = 0.25,
/// }, firstSketch)
/// )
/// ```
///
/// ```no_run
@ -98,10 +90,11 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 6)
///
/// // Remove a tagged face and the end face for the extrusion.
/// shell({
/// shell(
/// firstSketch,
/// faces = [myTag, 'end'],
/// thickness = 0.25,
/// }, firstSketch)
/// )
/// ```
///
/// ```no_run
@ -124,7 +117,7 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 50)
///
/// // We put "case" in the shell function to shell the entire object.
/// shell({ faces = ['start'], thickness = 5 }, case)
/// shell(case, faces = ['start'], thickness = 5)
/// ```
///
/// ```no_run
@ -147,7 +140,7 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 50)
///
/// // We put "thing1" in the shell function to shell the end face of the object.
/// shell({ faces = ['end'], thickness = 5 }, thing1)
/// shell(thing1, faces = ['end'], thickness = 5)
/// ```
///
/// ```no_run
@ -172,21 +165,29 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> extrude(length = 50)
///
/// // We put "thing1" and "thing2" in the shell function to shell the end face of the object.
/// shell({ faces = ['end'], thickness = 5 }, [thing1, thing2])
/// shell([thing1, thing2], faces = ['end'], thickness = 5)
/// ```
#[stdlib {
name = "shell",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "Which solid (or solids) to shell out"},
thickness = {docs = "The thickness of the shell"},
faces = {docs = "The faces you want removed"},
}
}]
async fn inner_shell(
data: ShellData,
solid_set: SolidSet,
thickness: f64,
faces: Vec<FaceTag>,
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidSet, KclError> {
if data.faces.is_empty() {
if faces.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected at least one face".to_string(),
message: "You must shell at least one face".to_string(),
source_ranges: vec![args.source_range],
}));
}
@ -194,7 +195,7 @@ async fn inner_shell(
let solids: Vec<Box<Solid>> = solid_set.clone().into();
if solids.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected at least one solid".to_string(),
message: "You must shell at least one solid".to_string(),
source_ranges: vec![args.source_range],
}));
}
@ -205,7 +206,7 @@ async fn inner_shell(
// If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?;
for tag in &data.faces {
for tag in &faces {
let extrude_plane_id = tag.get_face_id(solid, exec_state, &args, false).await?;
face_ids.push(extrude_plane_id);
@ -235,7 +236,7 @@ async fn inner_shell(
hollow: false,
face_ids,
object_id: solids[0].id,
shell_thickness: LengthUnit(data.thickness),
shell_thickness: LengthUnit(thickness),
}),
)
.await?;

View File

@ -1360,6 +1360,7 @@ pub(crate) async fn inner_start_profile_at(
let sketch = Sketch {
id: path_id,
original_id: path_id,
artifact_id: path_id.into(),
on: sketch_surface.clone(),
paths: vec![],

View File

@ -22,24 +22,14 @@ pub enum SweepPath {
Helix(Box<Helix>),
}
/// Data for a sweep.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct SweepData {
/// The path to sweep along.
pub path: SweepPath,
/// If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components.
pub sectional: Option<bool>,
/// Tolerance for the sweep operation.
#[serde(default)]
pub tolerance: Option<f64>,
}
/// Extrude a sketch along a path.
pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch): (SweepData, Sketch) = args.get_data_and_sketch()?;
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let path: SweepPath = args.get_kw_arg("path")?;
let sectional = args.get_kw_arg_opt("sectional")?;
let tolerance = args.get_kw_arg_opt("tolerance")?;
let value = inner_sweep(data, sketch, exec_state, args).await?;
let value = inner_sweep(sketch, path, sectional, tolerance, exec_state, args).await?;
Ok(KclValue::Solid { value })
}
@ -82,9 +72,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// radius = 2,
/// }, %)
/// |> hole(pipeHole, %)
/// |> sweep({
/// path: sweepPath,
/// }, %)
/// |> sweep(path = sweepPath)
/// ```
///
/// ```no_run
@ -104,15 +92,25 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('YZ')
/// |> circle({ center = [0, 0], radius = 1 }, %)
/// |> sweep({ path = helixPath }, %)
/// |> sweep(path = helixPath)
/// ```
#[stdlib {
name = "sweep",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
sketch = { docs = "The sketch that should be swept in space" },
path = { docs = "The path to sweep the sketch along" },
sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
tolerance = { docs = "Tolerance for this operation" },
}
}]
async fn inner_sweep(
data: SweepData,
sketch: Sketch,
path: SweepPath,
sectional: Option<bool>,
tolerance: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
@ -121,12 +119,12 @@ async fn inner_sweep(
id,
ModelingCmd::from(mcmd::Sweep {
target: sketch.id.into(),
trajectory: match data.path {
trajectory: match path {
SweepPath::Sketch(sketch) => sketch.id.into(),
SweepPath::Helix(helix) => helix.value.into(),
},
sectional: data.sectional.unwrap_or(false),
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
sectional: sectional.unwrap_or(false),
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
}),
)
.await?;

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing angled_line.kcl
snapshot_kind: text
---
{
"environments": [
@ -346,6 +347,7 @@ description: Program memory after executing angled_line.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -374,6 +374,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -904,6 +905,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -975,6 +977,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1412,6 +1415,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1878,6 +1882,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1949,6 +1954,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -468,6 +468,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -609,6 +610,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -200,6 +200,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -274,6 +274,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -695,6 +696,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -757,6 +759,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1332,6 +1335,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1394,6 +1398,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1517,6 +1522,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -2236,6 +2242,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -2298,6 +2305,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -2421,6 +2429,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -2483,6 +2492,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -2836,6 +2846,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -3202,6 +3213,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -3264,6 +3276,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -3779,6 +3792,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -3841,6 +3855,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -3964,6 +3979,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -4628,6 +4644,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -4690,6 +4707,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -4813,6 +4831,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -4875,6 +4894,7 @@ snapshot_kind: text
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_close_opposite.kcl
snapshot_kind: text
---
{
"environments": [
@ -422,6 +423,7 @@ description: Program memory after executing basic_fillet_cube_close_opposite.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_end.kcl
snapshot_kind: text
---
{
"environments": [
@ -353,6 +354,7 @@ description: Program memory after executing basic_fillet_cube_end.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_next_adjacent.kcl
snapshot_kind: text
---
{
"environments": [
@ -491,6 +492,7 @@ description: Program memory after executing basic_fillet_cube_next_adjacent.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_previous_adjacent.kcl
snapshot_kind: text
---
{
"environments": [
@ -491,6 +492,7 @@ description: Program memory after executing basic_fillet_cube_previous_adjacent.
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing basic_fillet_cube_start.kcl
snapshot_kind: text
---
{
"environments": [
@ -353,6 +354,7 @@ description: Program memory after executing basic_fillet_cube_start.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing big_number_angle_to_match_length_x.kcl
snapshot_kind: text
---
{
"environments": [
@ -253,6 +254,7 @@ description: Program memory after executing big_number_angle_to_match_length_x.k
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing big_number_angle_to_match_length_y.kcl
snapshot_kind: text
---
{
"environments": [
@ -253,6 +254,7 @@ description: Program memory after executing big_number_angle_to_match_length_y.k
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing circle_three_point.kcl
snapshot_kind: text
---
{
"environments": [
@ -126,6 +127,7 @@ description: Program memory after executing circle_three_point.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing cube.kcl
snapshot_kind: text
---
{
"environments": [
@ -944,6 +945,7 @@ description: Program memory after executing cube.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -2542,7 +2542,7 @@ snapshot_kind: text
"cmdId": "[uuid]",
"range": [
2047,
2110,
2102,
0
],
"command": {

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing fillet-and-shell.kcl
snapshot_kind: text
---
{
"Ok": {
@ -2582,68 +2583,45 @@ description: Result of parsing fillet-and-shell.kcl
"type": "ExpressionStatement"
},
{
"end": 2110,
"end": 2102,
"expression": {
"arguments": [
{
"end": 2103,
"properties": [
{
"end": 2072,
"key": {
"end": 2062,
"name": "faces",
"start": 2057,
"type": "Identifier"
},
"start": 2057,
"type": "ObjectProperty",
"value": {
"elements": [
{
"end": 2071,
"raw": "'end'",
"start": 2066,
"type": "Literal",
"type": "Literal",
"value": "end"
}
],
"end": 2072,
"start": 2065,
"type": "ArrayExpression",
"type": "ArrayExpression"
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "faces"
},
"arg": {
"elements": [
{
"end": 2073,
"raw": "'end'",
"start": 2068,
"type": "Literal",
"type": "Literal",
"value": "end"
}
},
{
"end": 2101,
"key": {
"end": 2085,
"name": "thickness",
"start": 2076,
"type": "Identifier"
},
"start": 2076,
"type": "ObjectProperty",
"value": {
"end": 2101,
"name": "caseThickness",
"start": 2088,
"type": "Identifier",
"type": "Identifier"
}
}
],
"start": 2053,
"type": "ObjectExpression",
"type": "ObjectExpression"
],
"end": 2074,
"start": 2067,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
},
{
"end": 2109,
"name": "case",
"start": 2105,
"type": "Identifier",
"type": "Identifier"
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "thickness"
},
"arg": {
"end": 2101,
"name": "caseThickness",
"start": 2088,
"type": "Identifier",
"type": "Identifier"
}
}
],
"callee": {
@ -2652,17 +2630,24 @@ description: Result of parsing fillet-and-shell.kcl
"start": 2047,
"type": "Identifier"
},
"end": 2110,
"end": 2102,
"start": 2047,
"type": "CallExpression",
"type": "CallExpression"
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"end": 2057,
"name": "case",
"start": 2053,
"type": "Identifier",
"type": "Identifier"
}
},
"start": 2047,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"end": 2111,
"end": 2103,
"nonCodeMeta": {
"nonCodeNodes": {
"1": [

View File

@ -74,7 +74,4 @@ m25Screw(border + rpizWidth / 2 + widthBetweenScrews / 2, 0 + border + rpizLengt
m25Screw(border + rpizWidth / 2 + widthBetweenScrews / 2, 0 + border + rpizLength / 2 - (lengthBetweenScrews / 2), screwHeight)
shell({
faces = ['end'],
thickness = caseThickness
}, case)
shell(case, faces = ['end'], thickness = caseThickness)

View File

@ -402,17 +402,17 @@ snapshot_kind: text
},
{
"labeledArgs": {
"data": {
"faces": {
"sourceRange": [
2053,
2103,
2067,
2074,
0
]
},
"solid_set": {
"thickness": {
"sourceRange": [
2105,
2109,
2088,
2101,
0
]
}
@ -420,10 +420,16 @@ snapshot_kind: text
"name": "shell",
"sourceRange": [
2047,
2110,
2102,
0
],
"type": "StdLibCall",
"unlabeledArg": null
"unlabeledArg": {
"sourceRange": [
2053,
2057,
0
]
}
}
]

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing fillet-and-shell.kcl
snapshot_kind: text
---
{
"environments": [
@ -504,6 +505,7 @@ description: Program memory after executing fillet-and-shell.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1787,6 +1789,7 @@ description: Program memory after executing fillet-and-shell.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -2389,6 +2392,7 @@ description: Program memory after executing fillet-and-shell.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -2674,6 +2678,7 @@ description: Program memory after executing fillet-and-shell.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing function_sketch.kcl
snapshot_kind: text
---
{
"environments": [
@ -622,6 +623,7 @@ description: Program memory after executing function_sketch.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing function_sketch_with_position.kcl
snapshot_kind: text
---
{
"environments": [
@ -608,6 +609,7 @@ description: Program memory after executing function_sketch_with_position.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing helix_ccw.kcl
snapshot_kind: text
---
{
"environments": [
@ -126,6 +127,7 @@ description: Program memory after executing helix_ccw.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing i_shape.kcl
snapshot_kind: text
---
{
"environments": [
@ -643,6 +644,7 @@ description: Program memory after executing i_shape.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1623,6 +1625,7 @@ description: Program memory after executing i_shape.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -1923,6 +1926,7 @@ description: Program memory after executing i_shape.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

View File

@ -1,6 +1,7 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing import_function_not_sketch.kcl
snapshot_kind: text
---
{
"environments": [
@ -471,6 +472,7 @@ description: Program memory after executing import_function_not_sketch.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},
@ -905,6 +907,7 @@ description: Program memory after executing import_function_not_sketch.kcl
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
},

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