Compare commits

..

1 Commits

Author SHA1 Message Date
9b901dfe51 test: verify useCalculateKclExpression cancellation 2025-05-16 19:53:57 -07:00
526 changed files with 35158 additions and 184839 deletions

View File

@ -88,7 +88,6 @@ jobs:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
ZOO_HOST: https://api.dev.zoo.dev
RUST_BACKTRACE: full
RUST_MIN_STACK: 10485760000
- name: Commit differences
if: steps.path-changes.outputs.outside-kcl-samples == 'false' && steps.cargo-test-kcl-samples.outcome == 'failure'
shell: bash
@ -120,7 +119,6 @@ jobs:
# Configure nextest when it's run by insta (via just).
NEXTEST_PROFILE: ci
RUST_BACKTRACE: full
RUST_MIN_STACK: 10485760000
- name: Build and archive tests
run: |
cd rust
@ -184,7 +182,6 @@ jobs:
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
ZOO_HOST: https://api.dev.zoo.dev
RUST_MIN_STACK: 10485760000
- name: Upload results
if: always()
run: .github/ci-cd-scripts/upload-results.sh
@ -241,7 +238,6 @@ jobs:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
ZOO_HOST: https://api.dev.zoo.dev
MODELING_APP_INTERNAL_SAMPLES_SECRET: ${{secrets.MODELING_APP_INTERNAL_SAMPLES_SECRET}}
RUST_MIN_STACK: 10485760000
run-wasm-tests:
name: Run wasm tests
strategy:

View File

@ -143,7 +143,7 @@ jobs:
- name: Install browsers
run: npm run playwright install --with-deps
- name: Test snapshots
- name: Capture snapshots
uses: nick-fields/retry@v3.0.2
with:
shell: bash
@ -158,17 +158,6 @@ jobs:
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
TARGET: web
- name: Update snapshots
if: always()
run: npm run test:snapshots -- --last-failed --update-snapshots
env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
TARGET: web
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
@ -184,7 +173,7 @@ jobs:
id: git-check
run: |
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
if git status | grep --quiet "Changes to be committed"
if git status | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi

View File

@ -42,6 +42,8 @@ The 3D view in Design Studio is just a video stream from our hosted geometry eng
We recommend downloading the latest application binary from our [releases](https://github.com/KittyCAD/modeling-app/releases) page. If you don't see your platform or architecture supported there, please file an issue.
If you'd like to try out upcoming changes sooner, you can also download those from our [nightly releases](https://zoo.dev/modeling-app/download/nightly) page.
## Developing
Finally, if you'd like to run a development build or contribute to the project, please visit our [contributor guide](CONTRIBUTING.md) to get started.

View File

@ -4,7 +4,7 @@ excerpt: "Documentation of the KCL language for the Zoo Design Studio."
layout: manual
---
This is a reference for KCL. If you are learning KCL, you may prefer the [guide](https://zoo.dev/docs/kcl-book/intro.html) which explains
This is a reference for KCL. If you are learning KCL, you may prefer the [guide]() which explains
things in a more tutorial fashion. See also our documentation of the [standard library](/docs/kcl-std).
## Topics

View File

@ -27,6 +27,9 @@ import increment from "util.kcl"
answer = increment(41)
```
Imported files _must_ be in the same project so that units are uniform across
modules. This means that it must be in the same directory.
Import statements must be at the top-level of a file. It is not allowed to have
an `import` statement inside a function or in the body of an ifelse.
@ -55,9 +58,6 @@ Imported symbols can be renamed for convenience or to avoid name collisions.
import increment as inc, decrement as dec from "util.kcl"
```
You can import files from the current directory or from subdirectories, but if importing from a
subdirectory you can only import `main.kcl`.
---
## Functions vs `clone`
@ -177,7 +177,7 @@ You can also import the whole module. This is useful if you want to use the
result of a module as a variable, like a part.
```norun
import "cube.kcl"
import "tests/inputs/cube.kcl" as cube
cube
|> translate(x=10)
```
@ -229,19 +229,6 @@ The final statement is what's important because it's the return value of the
entire module. The module is expected to return a single object that can be used
as a variable by the file that imports it.
The name of the file or subdirectory is used as the name of the variable within the importing program.
If you want to use a different name, you can do so by using the `as` keyword:
```kcl,norun
import "cube.kcl" // Introduces a new variable called `cube`.
import "cube.kcl" as block // Introduces a new variable called `block`.
import "cube/main.kcl" // Introduces a new variable called `cube`.
import "cube/main.kcl" as block // Introduces a new variable called `block`.
```
If the filename includes hyphens (`-`) or starts with an underscore (`_`), then you must specify a
variable name.
---
## Multiple instances of the same import
@ -254,7 +241,7 @@ If you want to have multiple instances of the same object, you can use the
[`clone`](/docs/kcl/clone) function. This will render a new instance of the object in memory.
```norun
import cube from "cube.kcl"
import cube from "tests/inputs/cube.kcl"
cube
|> translate(x=10)
@ -270,7 +257,7 @@ separate objects in memory, and can be manipulated independently.
Here is an example with a file from another CAD system:
```kcl
import "tests/inputs/cube.step"
import "tests/inputs/cube.step" as cube
cube
|> translate(x=10)

View File

@ -12,7 +12,7 @@ reduce(
@array: [any],
initial: any,
f: fn(any, accum: any): any,
): any
): [any]
```
Take a starting value. Then, for each element of an array, calculate the next value,
@ -28,7 +28,7 @@ using the previous value and the element.
### Returns
[`any`](/docs/kcl-std/types/std-types-any)
[`[any]`](/docs/kcl-std/types/std-types-any)
### Examples

View File

@ -11,7 +11,7 @@ Compute the length of the given leg.
legLen(
hypotenuse: number(Length),
leg: number(Length),
): number(Length)
): number(deg)
```
@ -25,7 +25,7 @@ legLen(
### Returns
[`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
[`number(deg)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,8 @@ mirror2d(
): Sketch
```
Only works on unclosed sketches for now.
Mirror occurs around a local sketch axis rather than a global axis.
### Arguments

View File

@ -65,7 +65,7 @@ layout: manual
* [`line`](/docs/kcl-std/line)
* [`loft`](/docs/kcl-std/loft)
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
* [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d)
* [`patternTransform2d`](/docs/kcl-std/patternTransform2d)
* [`polygon`](/docs/kcl-std/polygon)
* [`profileStart`](/docs/kcl-std/profileStart)
* [`profileStartX`](/docs/kcl-std/profileStartX)
@ -94,7 +94,7 @@ layout: manual
* [`intersect`](/docs/kcl-std/intersect)
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
* [`patternTransform`](/docs/kcl-std/functions/std-solid-patternTransform)
* [`patternTransform`](/docs/kcl-std/patternTransform)
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
* [`subtract`](/docs/kcl-std/subtract)
* [`union`](/docs/kcl-std/union)

View File

@ -30,7 +30,7 @@ This module contains functions for creating and manipulating sketches, and makin
* [`line`](/docs/kcl-std/line)
* [`loft`](/docs/kcl-std/loft)
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
* [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d)
* [`patternTransform2d`](/docs/kcl-std/patternTransform2d)
* [`polygon`](/docs/kcl-std/polygon)
* [`profileStart`](/docs/kcl-std/profileStart)
* [`profileStartX`](/docs/kcl-std/profileStartX)

View File

@ -18,7 +18,7 @@ This module contains functions for modifying solids, e.g., by adding a fillet or
* [`intersect`](/docs/kcl-std/intersect)
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
* [`patternTransform`](/docs/kcl-std/functions/std-solid-patternTransform)
* [`patternTransform`](/docs/kcl-std/patternTransform)
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
* [`subtract`](/docs/kcl-std/subtract)
* [`union`](/docs/kcl-std/union)

View File

@ -11,7 +11,7 @@ Contains frequently used constants, functions for interacting with the KittyCAD
The standard library is organised into modules (listed below), but most things are always available in KCL programs.
You might also want the [KCL language reference](/docs/kcl-lang) or the [KCL guide](https://zoo.dev/docs/kcl-book/intro.html).
You might also want the [KCL language reference](/docs/kcl-lang) or the [KCL guide]().
## Modules

View File

@ -12,8 +12,8 @@ patternCircular2d(
@sketchSet: [Sketch],
instances: number,
center: Point2d,
arcDegrees?: number,
rotateDuplicates?: bool,
arcDegrees: number,
rotateDuplicates: bool,
useOriginal?: bool,
): [Sketch]
```
@ -27,8 +27,8 @@ patternCircular2d(
| `sketchSet` | [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) | Which sketch(es) to pattern | Yes |
| `instances` | [`number`](/docs/kcl-std/types/std-types-number) | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `center` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | The center about which to make the pattern. This is a 2D vector. | Yes |
| `arcDegrees` | [`number`](/docs/kcl-std/types/std-types-number) | The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360. | No |
| `rotateDuplicates` | [`bool`](/docs/kcl-std/types/std-types-bool) | Whether or not to rotate the duplicates as they are copied. Defaults to true. | No |
| `arcDegrees` | [`number`](/docs/kcl-std/types/std-types-number) | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |
| `rotateDuplicates` | [`bool`](/docs/kcl-std/types/std-types-bool) | Whether or not to rotate the duplicates as they are copied. | Yes |
| `useOriginal` | [`bool`](/docs/kcl-std/types/std-types-bool) | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
### Returns

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,19 @@
---
title: "patternTransform2d"
subtitle: "Function in std::sketch"
excerpt: "Just like `patternTransform`, but works on 2D sketches not 3D solids."
excerpt: "Just like patternTransform, but works on 2D sketches not 3D solids."
layout: manual
---
Just like `patternTransform`, but works on 2D sketches not 3D solids.
Just like patternTransform, but works on 2D sketches not 3D solids.
```kcl
patternTransform2d(
@sketches: [Sketch; 1+],
instances: number(_),
transform: fn(number(_)): { },
useOriginal?: boolean,
): [Sketch; 1+]
@sketches: [Sketch],
instances: number,
transform: FunctionSource,
useOriginal?: bool,
): [Sketch]
```
@ -22,14 +22,14 @@ patternTransform2d(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketches` | [`[Sketch; 1+]`](/docs/kcl-std/types/std-types-Sketch) | The sketch(es) to duplicate. | Yes |
| `instances` | [`number(_)`](/docs/kcl-std/types/std-types-number) | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `transform` | [`fn(number(_)): { }`](/docs/kcl-std/types/std-types-fn) | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
| `useOriginal` | `boolean` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. | No |
| `sketches` | [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) | The sketch(es) to duplicate | Yes |
| `instances` | [`number`](/docs/kcl-std/types/std-types-number) | The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | Yes |
| `transform` | `FunctionSource` | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
| `useOriginal` | [`bool`](/docs/kcl-std/types/std-types-bool) | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
### Returns
[`[Sketch; 1+]`](/docs/kcl-std/types/std-types-Sketch)
[`[Sketch]`](/docs/kcl-std/types/std-types-Sketch)
### Examples

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -235,48 +235,6 @@ extrude001 = extrude(sketch001, length = 5)`
.first()
).toBeVisible()
})
test('KCL errors with functions show hints for the entire backtrace', async ({
page,
homePage,
scene,
cmdBar,
editor,
toolbar,
}) => {
await homePage.goToModelingScene()
await scene.settled(cmdBar)
const code = `fn check(@x) {
return assert(x, isGreaterThan = 0)
}
fn middle(@x) {
return check(x)
}
middle(1)
middle(0)
`
await test.step('Set the code with a KCL error', async () => {
await toolbar.openPane('code')
await editor.replaceCode('', code)
})
// This shows all the diagnostics in a way that doesn't require the mouse
// pointer hovering over a coordinate, which would be brittle.
await test.step('Open CodeMirror diagnostics list', async () => {
// Ensure keyboard focus is in the editor.
await page.getByText('fn check(').click()
await page.keyboard.press('ControlOrMeta+Shift+M')
})
await expect(
page.getByText(`assert failed: Expected 0 to be greater than 0 but it wasn't
check()
middle()`)
).toBeVisible()
// There should be one hint inside middle() and one at the top level.
await expect(page.getByText('Part of the error backtrace')).toHaveCount(2)
})
})
test(

View File

@ -36,29 +36,27 @@ test.describe('Command bar tests', () => {
await u.closeDebugPanel()
// Click the line of code for xLine.
await page.getByText(`startProfile(at = [-10, -10])`).click()
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
await page.waitForTimeout(200)
await page.getByText(`close()`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await toolbar.extrudeButton.click()
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Extrude',
currentArgKey: 'length',
currentArgValue: '5',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Profiles: '1 profile',
Sketches: '',
Length: '',
},
highlightedHeaderArg: 'length',
highlightedHeaderArg: 'sketches',
})
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
commandName: 'Extrude',
headerArguments: {
Profiles: '1 profile',
Sketches: '1 segment',
Length: '5',
},
})
@ -288,7 +286,7 @@ test.describe('Command bar tests', () => {
await cmdBar.cmdOptions.getByText('Extrude').click()
// Assert that we're on the selection step
await expect(page.getByRole('button', { name: 'Profiles' })).toBeDisabled()
await expect(page.getByRole('button', { name: 'sketches' })).toBeDisabled()
// Select a face
await page.mouse.move(700, 200)
await page.mouse.click(700, 200)
@ -401,6 +399,7 @@ test.describe('Command bar tests', () => {
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
@ -411,7 +410,7 @@ test.describe('Command bar tests', () => {
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'main.kcl',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
@ -422,7 +421,7 @@ test.describe('Command bar tests', () => {
commandName: 'Import file from URL',
headerArguments: {
Method: 'New project',
Name: 'main.kcl',
Name: 'test',
Code: '1 line',
},
})
@ -464,6 +463,7 @@ test.describe('Command bar tests', () => {
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
@ -474,7 +474,7 @@ test.describe('Command bar tests', () => {
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'main.kcl',
Name: 'test',
Code: '1 line',
},
highlightedHeaderArg: 'method',
@ -487,7 +487,7 @@ test.describe('Command bar tests', () => {
currentArgValue: '',
headerArguments: {
Method: 'Existing project',
Name: 'main.kcl',
Name: 'test',
ProjectName: '',
Code: '1 line',
},
@ -500,7 +500,7 @@ test.describe('Command bar tests', () => {
headerArguments: {
Method: 'Existing project',
ProjectName: 'testProjectDir',
Name: 'main.kcl',
Name: 'test',
Code: '1 line',
},
})
@ -510,7 +510,7 @@ test.describe('Command bar tests', () => {
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
await toolbar.openPane('files')
await toolbar.expectFileTreeState(['main-1.kcl', 'main.kcl'])
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
})
})
@ -661,56 +661,4 @@ c = 3 + a`
`a = 5b = a * amyParameter001 = ${newValue}c = 3 + a`
)
})
test('Command palette can be opened via query parameter', async ({
page,
homePage,
cmdBar,
}) => {
await page.goto(`${page.url()}/?cmd=app.theme&groupId=settings`)
await homePage.expectState({
projectCards: [],
sortBy: 'last-modified-desc',
})
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Settings · app · theme',
currentArgKey: 'value',
currentArgValue: '',
headerArguments: {
Level: 'user',
Value: '',
},
highlightedHeaderArg: 'value',
})
})
test('Text-to-CAD command can be closed with escape while in prompt', async ({
page,
homePage,
cmdBar,
}) => {
await homePage.expectState({
projectCards: [],
sortBy: 'last-modified-desc',
})
await homePage.textToCadBtn.click()
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Text-to-CAD Create',
currentArgKey: 'prompt',
currentArgValue: '',
headerArguments: {
Method: 'New project',
NewProjectName: 'untitled',
Prompt: '',
},
highlightedHeaderArg: 'prompt',
})
await page.keyboard.press('Escape')
await cmdBar.toBeClosed()
await cmdBar.expectState({
stage: 'commandBarClosed',
})
})
})

View File

@ -1131,15 +1131,14 @@ sketch001 = startSketchOn(XZ)
await page.waitForTimeout(100)
await page.getByText('startProfile(at = [4.61, -14.01])').click()
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
await page.waitForTimeout(200)
await toolbar.extrudeButton.click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Profiles: '1 profile',
Sketches: '1 face',
Length: '',
},
highlightedHeaderArg: 'length',
@ -1149,7 +1148,7 @@ sketch001 = startSketchOn(XZ)
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Profiles: '1 profile',
Sketches: '1 face',
Length: '5',
},
commandName: 'Extrude',
@ -1355,9 +1354,7 @@ sketch001 = startSketchOn(XZ)
const u = await getUtils(page)
const projectLink = page.getByRole('link', { name: 'cube' })
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', {
name: 'Reset view',
})
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
const locationToHaveColor = async (
position: { x: number; y: number },
color: [number, number, number]

View File

@ -238,26 +238,6 @@ test.describe('when using the file tree to', () => {
}
)
test(
`create new folders and that doesn't trigger a navigation`,
{ tag: ['@electron', '@macos', '@windows'] },
async ({ page, homePage, scene, toolbar, cmdBar }) => {
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await toolbar.openPane('files')
const { createNewFolder } = await getUtils(page, test)
await createNewFolder('folder')
await createNewFolder('folder.kcl')
await test.step(`Postcondition: folders are created and we didn't navigate`, async () => {
await toolbar.expectFileTreeState(['folder', 'folder.kcl', 'main.kcl'])
await expect(toolbar.fileName).toHaveText('main.kcl')
})
}
)
test(
'deleting all files recreates a default main.kcl with no code',
{ tag: '@electron' },

View File

@ -146,7 +146,9 @@ export class CmdBarFixture {
await this.cmdBarOpenBtn.click()
await expect(this.page.getByPlaceholder('Search commands')).toBeVisible()
if (selectCmd === 'promptToEdit') {
const promptEditCommand = this.selectOption({ name: 'Text-to-CAD Edit' })
const promptEditCommand = this.page.getByText(
'Use Zoo AI to edit your parts and code.'
)
await expect(promptEditCommand.first()).toBeVisible()
await promptEditCommand.first().scrollIntoViewIfNeeded()
await promptEditCommand.first().click()
@ -308,11 +310,6 @@ export class CmdBarFixture {
await expect(this.cmdBarElement).toBeVisible({ timeout: 10_000 })
}
async toBeClosed() {
// Check that the command bar is closed
await expect(this.cmdBarElement).not.toBeVisible({ timeout: 10_000 })
}
async expectArgValue(value: string) {
// Check the placeholder project name exists
const actualArgument = await this.cmdBarElement

View File

@ -26,7 +26,6 @@ export class HomePageFixture {
sortByNameBtn!: Locator
appHeader!: Locator
tutorialBtn!: Locator
textToCadBtn!: Locator
constructor(page: Page) {
this.page = page
@ -48,7 +47,6 @@ export class HomePageFixture {
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
this.appHeader = this.page.getByTestId('app-header')
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
this.textToCadBtn = this.page.getByTestId('home-text-to-cad')
}
private _serialiseSortBy = async (): Promise<
@ -123,13 +121,11 @@ export class HomePageFixture {
await projectCard.click()
}
/** Returns the project name in case caller has used the default and needs it */
goToModelingScene = async (name = 'testDefault') => {
goToModelingScene = async (name: string = 'testDefault') => {
// On web this is a no-op. There is no project view.
if (process.env.PLATFORM === 'web') return ''
if (process.env.PLATFORM === 'web') return
await this.createAndGoToProject(name)
return name
}
isNativeFileMenuCreated = async () => {

View File

@ -252,7 +252,7 @@ test.describe(
tronApp,
'Edit.Modify with Zoo Text-To-CAD'
)
await cmdBar.expectCommandName('Text-to-CAD Edit')
await cmdBar.expectCommandName('Prompt-to-edit')
})
await test.step('Modeling.Edit.Edit parameter', async () => {
await page.waitForTimeout(250)
@ -518,7 +518,7 @@ test.describe(
'Design.Create with Zoo Text-To-CAD'
)
await cmdBar.toBeOpened()
await cmdBar.expectCommandName('Text-to-CAD Create')
await cmdBar.expectCommandName('Text to CAD')
})
await test.step('Modeling.Design.Modify with Zoo Text-To-CAD', async () => {
@ -528,7 +528,7 @@ test.describe(
'Design.Modify with Zoo Text-To-CAD'
)
await cmdBar.toBeOpened()
await cmdBar.expectCommandName('Text-to-CAD Edit')
await cmdBar.expectCommandName('Prompt-to-edit')
})
await test.step('Modeling.Help.KCL code samples', async () => {

View File

@ -74,11 +74,20 @@ test.describe('Point-and-click tests', () => {
await test.step('do extrude flow and check extrude code is added to editor', async () => {
await toolbar.extrudeButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: { Sketches: '', Length: '' },
highlightedHeaderArg: 'sketches',
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: { Profiles: '1 profile', Length: '' },
headerArguments: { Sketches: '1 face', Length: '' },
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})
@ -89,7 +98,7 @@ test.describe('Point-and-click tests', () => {
await cmdBar.expectState({
stage: 'review',
headerArguments: { Profiles: '1 profile', Length: '5' },
headerArguments: { Sketches: '1 face', Length: '5' },
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
@ -1625,15 +1634,15 @@ sketch002 = startSketchOn(plane001)
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: { Profiles: '' },
highlightedHeaderArg: 'Profiles',
headerArguments: { Sketches: '' },
highlightedHeaderArg: 'sketches',
commandName: 'Loft',
})
await selectSketches()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Profiles: '2 profiles' },
headerArguments: { Sketches: '2 faces' },
commandName: 'Loft',
})
await cmdBar.submit()
@ -1645,9 +1654,18 @@ sketch002 = startSketchOn(plane001)
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: { Sketches: '' },
highlightedHeaderArg: 'sketches',
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Profiles: '2 profiles' },
headerArguments: { Sketches: '2 faces' },
commandName: 'Loft',
})
await cmdBar.submit()
@ -1812,10 +1830,10 @@ sketch002 = startSketchOn(XZ)
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '',
Sketches: '',
Path: '',
},
highlightedHeaderArg: 'Profiles',
highlightedHeaderArg: 'sketches',
stage: 'arguments',
})
await clickOnSketch1()
@ -1826,7 +1844,7 @@ sketch002 = startSketchOn(XZ)
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Sketches: '1 face',
Path: '',
},
highlightedHeaderArg: 'path',
@ -1839,7 +1857,7 @@ sketch002 = startSketchOn(XZ)
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Sketches: '1 face',
Path: '',
},
highlightedHeaderArg: 'path',
@ -1849,7 +1867,7 @@ sketch002 = startSketchOn(XZ)
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Profiles: '1 profile',
Sketches: '1 face',
Path: '1 segment',
Sectional: '',
},
@ -1950,10 +1968,10 @@ profile001 = ${circleCode}`
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '',
Sketches: '',
Path: '',
},
highlightedHeaderArg: 'Profiles',
highlightedHeaderArg: 'sketches',
stage: 'arguments',
})
await editor.scrollToText(circleCode)
@ -1965,7 +1983,7 @@ profile001 = ${circleCode}`
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Sketches: '1 face',
Path: '',
},
highlightedHeaderArg: 'path',
@ -1979,7 +1997,7 @@ profile001 = ${circleCode}`
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Sketches: '1 face',
Path: '',
},
highlightedHeaderArg: 'path',
@ -1989,7 +2007,7 @@ profile001 = ${circleCode}`
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Profiles: '1 profile',
Sketches: '1 face',
Path: '1 helix',
Sectional: '',
},
@ -2088,6 +2106,18 @@ extrude001 = extrude(sketch001, length = -12)
await test.step(`Apply fillet to the preselected edge`, async () => {
await page.waitForTimeout(100)
await toolbar.filletButton.click()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
@ -2617,6 +2647,18 @@ extrude001 = extrude(profile001, length = 5)
await test.step(`Apply fillet`, async () => {
await page.waitForTimeout(100)
await toolbar.filletButton.click()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Radius: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
@ -2722,6 +2764,19 @@ extrude001 = extrude(sketch001, length = -12)
await test.step(`Apply chamfer to the preselected edge`, async () => {
await page.waitForTimeout(100)
await toolbar.chamferButton.click()
await cmdBar.expectState({
commandName: 'Chamfer',
highlightedHeaderArg: 'selection',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Length: '',
},
stage: 'arguments',
})
await cmdBar.progressCmdBar()
await page.waitForTimeout(1000)
await cmdBar.expectState({
commandName: 'Chamfer',
highlightedHeaderArg: 'length',
@ -3205,6 +3260,8 @@ extrude001 = extrude(sketch001, length = 30)
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
await toolbar.shellButton.click()
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
@ -3634,17 +3691,16 @@ tag=$rectangleSegmentC002,
await scene.settled(cmdBar)
// select line of code
const codeToSelection = `startProfile(at = [-66.77, 84.81])`
const codeToSelection = `segAng(rectangleSegmentA002) - 90,`
// revolve
await editor.scrollToText(codeToSelection)
await page.getByText(codeToSelection).click()
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
await page.waitForTimeout(200)
await toolbar.revolveButton.click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
@ -4573,12 +4629,24 @@ path001 = startProfile(sketch001, at = [0, 0])
await test.step('Go through command bar flow', async () => {
await toolbar.extrudeButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
Length: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Profiles: '2 profiles',
Sketches: '2 faces',
Length: '',
},
highlightedHeaderArg: 'length',
@ -4589,7 +4657,7 @@ path001 = startProfile(sketch001, at = [0, 0])
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Profiles: '2 profiles',
Sketches: '2 faces',
Length: '1',
},
commandName: 'Extrude',
@ -4655,12 +4723,25 @@ path001 = startProfile(sketch001, at = [0, 0])
await test.step('Go through command bar flow', async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
Path: '',
Sectional: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Sweep',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Profiles: '2 profiles',
Sketches: '2 faces',
Path: '',
Sectional: '',
},
@ -4673,7 +4754,7 @@ path001 = startProfile(sketch001, at = [0, 0])
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Profiles: '2 profiles',
Sketches: '2 faces',
Path: '1 segment',
Sectional: '',
},
@ -4739,12 +4820,25 @@ path001 = startProfile(sketch001, at = [0, 0])
await test.step('Go through command bar flow', async () => {
await toolbar.closePane('code')
await toolbar.revolveButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
AxisOrEdge: '',
Angle: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Revolve',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'axisOrEdge',
currentArgValue: '',
headerArguments: {
Profiles: '2 profiles',
Sketches: '2 faces',
AxisOrEdge: '',
Angle: '',
},
@ -4760,7 +4854,7 @@ path001 = startProfile(sketch001, at = [0, 0])
currentArgKey: 'angle',
currentArgValue: '360',
headerArguments: {
Profiles: '2 profiles',
Sketches: '2 faces',
AxisOrEdge: 'Edge',
Edge: '1 segment',
Angle: '',
@ -4773,7 +4867,7 @@ path001 = startProfile(sketch001, at = [0, 0])
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Profiles: '2 profiles',
Sketches: '2 faces',
AxisOrEdge: 'Edge',
Edge: '1 segment',
Angle: '180',

View File

@ -2064,59 +2064,3 @@ test(
})
}
)
test(
'nested dir import works on windows',
{ tag: ['@electron', '@windows'] },
async ({ scene, cmdBar, context, page }, testInfo) => {
// Skip if on non-windows
if (process.platform !== 'win32') {
test.skip()
}
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
const nestedDir = path.join(bracketDir, 'nested')
await fsp.mkdir(nestedDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'),
path.join(nestedDir, 'main.kcl')
)
await fsp.writeFile(
path.join(bracketDir, 'main.kcl'),
`import 'nested\\main.kcl' as thing
thing`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [125, 125, 125]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
}
)

View File

@ -995,8 +995,8 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// click profile in code
await page.getByText(`startProfile(at = [-0.45, 0.87])`).click()
// click "line(end = [1.32, 0.38])"
await page.getByText(`line(end = [1.32, 0.38])`).click()
await page.waitForTimeout(100)
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeEnabled(
{ timeout: 10_000 }
@ -1014,13 +1014,14 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff
// click extrude
await toolbar.extrudeButton.click()
// sketch selection should already have been made.
// sketch selection should already have been made. "Sketches: 1 face" only show up when the selection has been made already
// otherwise the cmdbar would be waiting for a selection.
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: { Profiles: '1 profile', Length: '' },
headerArguments: { Sketches: '1 segment', Length: '' },
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -557,14 +557,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
})
},
createNewFolder: async (name: string) => {
return test?.step(`Create a folder named ${name}`, async () => {
await page.getByTestId('create-folder-button').click()
await page.getByTestId('tree-input-field').fill(name)
await page.keyboard.press('Enter')
})
},
cloneFile: async (name: string) => {
return test?.step(`Cloning file '${name}'`, async () => {
await page

View File

@ -4,10 +4,9 @@ import type { Page } from '@playwright/test'
import { createProject, getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
test.describe('Text-to-CAD tests', () => {
test('basic lego happy case', async ({ page, homePage, cmdBar }) => {
test('basic lego happy case', async ({ page, homePage }) => {
const u = await getUtils(page)
await test.step('Set up', async () => {
@ -16,11 +15,7 @@ test.describe('Text-to-CAD tests', () => {
await u.waitForPageLoad()
})
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
// Find the toast.
// Look out for the toast message
@ -61,7 +56,6 @@ test.describe('Text-to-CAD tests', () => {
test('success model, then ignore success toast, user can create new prompt from command bar', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -70,11 +64,7 @@ test.describe('Text-to-CAD tests', () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x6 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x6 lego')
// Find the toast.
// Look out for the toast message
@ -92,11 +82,7 @@ test.describe('Text-to-CAD tests', () => {
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
// Can send a new prompt from the command bar.
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
// Find the toast.
// Look out for the toast message
@ -114,7 +100,6 @@ test.describe('Text-to-CAD tests', () => {
test('you can reject text-to-cad output and it does nothing', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -123,11 +108,7 @@ test.describe('Text-to-CAD tests', () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
// Find the toast.
// Look out for the toast message
@ -160,7 +141,6 @@ test.describe('Text-to-CAD tests', () => {
test('sending a bad prompt fails, can dismiss', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -170,11 +150,7 @@ test.describe('Text-to-CAD tests', () => {
await u.waitForPageLoad()
const randomPrompt = `aslkdfja;` + Date.now() + `FFFFEIWJF`
await sendPromptFromCommandBarAndSetExistingProject(
page,
randomPrompt,
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, randomPrompt)
// Find the toast.
// Look out for the toast message
@ -212,7 +188,6 @@ test.describe('Text-to-CAD tests', () => {
test('sending a bad prompt fails, can start over from toast', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -222,7 +197,7 @@ test.describe('Text-to-CAD tests', () => {
await u.waitForPageLoad()
const badPrompt = 'akjsndladf lajbhflauweyfaaaljhr472iouafyvsssssss'
await sendPromptFromCommandBarAndSetExistingProject(page, badPrompt, cmdBar)
await sendPromptFromCommandBarTriggeredByButton(page, badPrompt)
// Find the toast.
// Look out for the toast message
@ -281,7 +256,6 @@ test.describe('Text-to-CAD tests', () => {
test('sending a bad prompt fails, can ignore toast, can start over from command bar', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -291,7 +265,7 @@ test.describe('Text-to-CAD tests', () => {
await u.waitForPageLoad()
const badPrompt = 'akjsndladflajbhflauweyf15;'
await sendPromptFromCommandBarAndSetExistingProject(page, badPrompt, cmdBar)
await sendPromptFromCommandBarTriggeredByButton(page, badPrompt)
// Find the toast.
// Look out for the toast message
@ -318,11 +292,7 @@ test.describe('Text-to-CAD tests', () => {
await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible()
// They should be able to try again from the command bar.
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
// Find the toast.
// Look out for the toast message
@ -340,40 +310,17 @@ test.describe('Text-to-CAD tests', () => {
test('ensure you can shift+enter in the prompt box', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
const projectName = await homePage.goToModelingScene()
await homePage.goToModelingScene()
await u.waitForPageLoad()
const promptWithNewline = `a 2x4\nlego`
await test.step('Get to the prompt step to test', async () => {
await cmdBar.openCmdBar()
await cmdBar.selectOption({ name: 'Text-to-CAD Create' }).click()
await cmdBar.currentArgumentInput.fill('existing')
await cmdBar.progressCmdBar()
await cmdBar.currentArgumentInput.fill(projectName)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Text-to-CAD Create',
stage: 'arguments',
currentArgKey: 'prompt',
currentArgValue: '',
highlightedHeaderArg: 'prompt',
headerArguments: {
Method: 'Existing project',
ProjectName: projectName,
Prompt: '',
},
})
})
await page.getByTestId('text-to-cad').click()
// Type the prompt.
await page.keyboard.type('a 2x4')
@ -407,7 +354,6 @@ test.describe('Text-to-CAD tests', () => {
test('can do many at once and get many prompts back, and interact with many', async ({
page,
homePage,
cmdBar,
}) => {
// Let this test run longer since we've seen it timeout.
test.setTimeout(180_000)
@ -419,23 +365,11 @@ test.describe('Text-to-CAD tests', () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x8 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x8 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x10 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x10 lego')
// Find the toast.
// Look out for the toast message
@ -506,7 +440,6 @@ test.describe('Text-to-CAD tests', () => {
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -515,16 +448,11 @@ test.describe('Text-to-CAD tests', () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
await sendPromptFromCommandBarAndSetExistingProject(
await sendPromptFromCommandBarTriggeredByButton(
page,
'alkjsdnlajshdbfjlhsbdf a;askjdnf',
cmdBar
'alkjsdnlajshdbfjlhsbdf a;askjdnf'
)
// Find the toast.
@ -598,9 +526,7 @@ async function _sendPromptFromCommandBar(page: Page, promptStr: string) {
const cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
const textToCadCommand = page.getByRole('option', {
name: 'Text-to-CAD Create',
})
const textToCadCommand = page.getByText('Use the Zoo Text-to-CAD API')
await expect(textToCadCommand.first()).toBeVisible()
// Click the Text-to-CAD command
await textToCadCommand.first().scrollIntoViewIfNeeded()
@ -618,67 +544,29 @@ async function _sendPromptFromCommandBar(page: Page, promptStr: string) {
})
}
async function sendPromptFromCommandBarAndSetExistingProject(
async function sendPromptFromCommandBarTriggeredByButton(
page: Page,
promptStr: string,
cmdBar: CmdBarFixture,
projectName = 'testDefault'
promptStr: string
) {
await page.waitForTimeout(1000)
await test.step(`Send prompt from command bar: ${promptStr}`, async () => {
await cmdBar.openCmdBar()
await cmdBar.selectOption({ name: 'Text-to-CAD Create' }).click()
await page.getByTestId('text-to-cad').click()
await cmdBar.expectState({
commandName: 'Text-to-CAD Create',
stage: 'arguments',
currentArgKey: 'method',
currentArgValue: '',
highlightedHeaderArg: 'method',
headerArguments: {
Method: '',
Prompt: '',
},
})
await cmdBar.currentArgumentInput.fill('existing')
await cmdBar.progressCmdBar()
// Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' })
await expect(prompt.first()).toBeVisible()
await cmdBar.expectState({
commandName: 'Text-to-CAD Create',
stage: 'arguments',
currentArgKey: 'projectName',
currentArgValue: '',
highlightedHeaderArg: 'projectName',
headerArguments: {
Method: 'Existing project',
ProjectName: '',
Prompt: '',
},
})
await cmdBar.currentArgumentInput.fill(projectName)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Text-to-CAD Create',
stage: 'arguments',
currentArgKey: 'prompt',
currentArgValue: '',
highlightedHeaderArg: 'prompt',
headerArguments: {
Method: 'Existing project',
ProjectName: projectName,
Prompt: '',
},
})
await cmdBar.currentArgumentInput.fill(promptStr)
await cmdBar.progressCmdBar()
// Type the prompt.
await page.keyboard.type(promptStr)
await page.waitForTimeout(200)
await page.keyboard.press('Enter')
})
}
test(
'Text-to-CAD functionality',
{ tag: '@electron' },
async ({ context, page, cmdBar }, testInfo) => {
async ({ context, page }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
@ -715,12 +603,7 @@ test(
await openKclCodePanel()
await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBarAndSetExistingProject(
page,
prompt,
cmdBar,
projectName
)
await sendPromptFromCommandBarTriggeredByButton(page, prompt)
// File is considered created if it shows up in the Project Files pane
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
expect(fileExists()).toBeTruthy()
@ -890,12 +773,12 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
)
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
await expect(page.getByTestId('app-header-file-name')).toContainText(
'main.kcl'
'2x2x2-cube.kcl'
)
await u.openFilePanel()
await expect(
page.getByTestId('file-tree-item').getByText('main.kcl')
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
).toBeVisible()
}
)
@ -1030,7 +913,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
)
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
await expect(page.getByTestId('app-header-file-name')).toContainText(
'main.kcl'
'2x2x2-cube.kcl'
)
await page.getByRole('button', { name: 'Reject' }).click()
@ -1078,7 +961,7 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
)
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
await expect(page.getByTestId('app-header-file-name')).toContainText(
'main.kcl'
'2x2x2-cube.kcl'
)
}
)
@ -1330,14 +1213,18 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
)
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
await expect(page.getByTestId('app-header-file-name')).toContainText(
'main.kcl'
'2x2x2-cube.kcl'
)
// Check file is created
await u.openFilePanel()
await expect(
page.getByTestId('file-tree-item').getByText('main.kcl')
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
).toBeVisible()
await expect(
page.getByTestId('file-tree-item').getByText('main.kcl')
).not.toBeVisible()
}
)

View File

@ -573,6 +573,7 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34])
await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await cmdBar.progressCmdBar()

View File

@ -37,8 +37,6 @@ When you submit a PR to add or modify KCL samples, images will be generated and
[![bottle](screenshots/bottle.png)](bottle/main.kcl)
#### [bracket](bracket/main.kcl) ([screenshot](screenshots/bracket.png))
[![bracket](screenshots/bracket.png)](bracket/main.kcl)
#### [brake-rotor](brake-rotor/main.kcl) ([screenshot](screenshots/brake-rotor.png))
[![brake-rotor](screenshots/brake-rotor.png)](brake-rotor/main.kcl)
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([screenshot](screenshots/car-wheel-assembly.png))
[![car-wheel-assembly](screenshots/car-wheel-assembly.png)](car-wheel-assembly/main.kcl)
#### [cold-plate](cold-plate/main.kcl) ([screenshot](screenshots/cold-plate.png))
@ -51,16 +49,12 @@ When you submit a PR to add or modify KCL samples, images will be generated and
[![countersunk-plate](screenshots/countersunk-plate.png)](countersunk-plate/main.kcl)
#### [cpu-cooler](cpu-cooler/main.kcl) ([screenshot](screenshots/cpu-cooler.png))
[![cpu-cooler](screenshots/cpu-cooler.png)](cpu-cooler/main.kcl)
#### [curtain-wall-anchor-plate](curtain-wall-anchor-plate/main.kcl) ([screenshot](screenshots/curtain-wall-anchor-plate.png))
[![curtain-wall-anchor-plate](screenshots/curtain-wall-anchor-plate.png)](curtain-wall-anchor-plate/main.kcl)
#### [cycloidal-gear](cycloidal-gear/main.kcl) ([screenshot](screenshots/cycloidal-gear.png))
[![cycloidal-gear](screenshots/cycloidal-gear.png)](cycloidal-gear/main.kcl)
#### [dodecahedron](dodecahedron/main.kcl) ([screenshot](screenshots/dodecahedron.png))
[![dodecahedron](screenshots/dodecahedron.png)](dodecahedron/main.kcl)
#### [enclosure](enclosure/main.kcl) ([screenshot](screenshots/enclosure.png))
[![enclosure](screenshots/enclosure.png)](enclosure/main.kcl)
#### [engine-valve](engine-valve/main.kcl) ([screenshot](screenshots/engine-valve.png))
[![engine-valve](screenshots/engine-valve.png)](engine-valve/main.kcl)
#### [exhaust-manifold](exhaust-manifold/main.kcl) ([screenshot](screenshots/exhaust-manifold.png))
[![exhaust-manifold](screenshots/exhaust-manifold.png)](exhaust-manifold/main.kcl)
#### [flange](flange/main.kcl) ([screenshot](screenshots/flange.png))
@ -109,8 +103,6 @@ When you submit a PR to add or modify KCL samples, images will be generated and
[![mounting-plate](screenshots/mounting-plate.png)](mounting-plate/main.kcl)
#### [multi-axis-robot](multi-axis-robot/main.kcl) ([screenshot](screenshots/multi-axis-robot.png))
[![multi-axis-robot](screenshots/multi-axis-robot.png)](multi-axis-robot/main.kcl)
#### [pdu-faceplate](pdu-faceplate/main.kcl) ([screenshot](screenshots/pdu-faceplate.png))
[![pdu-faceplate](screenshots/pdu-faceplate.png)](pdu-faceplate/main.kcl)
#### [pillow-block-bearing](pillow-block-bearing/main.kcl) ([screenshot](screenshots/pillow-block-bearing.png))
[![pillow-block-bearing](screenshots/pillow-block-bearing.png)](pillow-block-bearing/main.kcl)
#### [pipe](pipe/main.kcl) ([screenshot](screenshots/pipe.png))
@ -127,24 +119,16 @@ When you submit a PR to add or modify KCL samples, images will be generated and
[![router-template-cross-bar](screenshots/router-template-cross-bar.png)](router-template-cross-bar/main.kcl)
#### [router-template-slate](router-template-slate/main.kcl) ([screenshot](screenshots/router-template-slate.png))
[![router-template-slate](screenshots/router-template-slate.png)](router-template-slate/main.kcl)
#### [sash-window](sash-window/main.kcl) ([screenshot](screenshots/sash-window.png))
[![sash-window](screenshots/sash-window.png)](sash-window/main.kcl)
#### [sheet-metal-bracket](sheet-metal-bracket/main.kcl) ([screenshot](screenshots/sheet-metal-bracket.png))
[![sheet-metal-bracket](screenshots/sheet-metal-bracket.png)](sheet-metal-bracket/main.kcl)
#### [shepherds-hook-bolt](shepherds-hook-bolt/main.kcl) ([screenshot](screenshots/shepherds-hook-bolt.png))
[![shepherds-hook-bolt](screenshots/shepherds-hook-bolt.png)](shepherds-hook-bolt/main.kcl)
#### [socket-head-cap-screw](socket-head-cap-screw/main.kcl) ([screenshot](screenshots/socket-head-cap-screw.png))
[![socket-head-cap-screw](screenshots/socket-head-cap-screw.png)](socket-head-cap-screw/main.kcl)
#### [spinning-highrise-tower](spinning-highrise-tower/main.kcl) ([screenshot](screenshots/spinning-highrise-tower.png))
[![spinning-highrise-tower](screenshots/spinning-highrise-tower.png)](spinning-highrise-tower/main.kcl)
#### [spur-gear](spur-gear/main.kcl) ([screenshot](screenshots/spur-gear.png))
[![spur-gear](screenshots/spur-gear.png)](spur-gear/main.kcl)
#### [spur-reduction-gearset](spur-reduction-gearset/main.kcl) ([screenshot](screenshots/spur-reduction-gearset.png))
[![spur-reduction-gearset](screenshots/spur-reduction-gearset.png)](spur-reduction-gearset/main.kcl)
#### [surgical-drill-guide](surgical-drill-guide/main.kcl) ([screenshot](screenshots/surgical-drill-guide.png))
[![surgical-drill-guide](screenshots/surgical-drill-guide.png)](surgical-drill-guide/main.kcl)
#### [thermal-block-insert](thermal-block-insert/main.kcl) ([screenshot](screenshots/thermal-block-insert.png))
[![thermal-block-insert](screenshots/thermal-block-insert.png)](thermal-block-insert/main.kcl)
#### [tooling-nest-block](tooling-nest-block/main.kcl) ([screenshot](screenshots/tooling-nest-block.png))
[![tooling-nest-block](screenshots/tooling-nest-block.png)](tooling-nest-block/main.kcl)
#### [utility-sink](utility-sink/main.kcl) ([screenshot](screenshots/utility-sink.png))

View File

@ -1,179 +0,0 @@
// A 320mm vented brake disc (rotor), with straight vanes, 30mm thick. The disc bell should accommodate 5 M12 wheel studs on a 114.3mm pitch circle diameter.
@settings(defaultLengthUnit = mm)
// Define parameters.
dDisc = 320
dPitchCircle = 114.3
dBore = 64
nStuds = 5
dStudDrilling = 12.5 // M12
hFrictionSurface = 60
tDiscHalf = 10
// Vent parameters.
tVent = 10
wVent = 6
rVentFillet = 2
nVentBosses = 36
// Drilling parameters.
dDrillDia = 6
aBase = 90
aSweep = 30
nArcs = 12
// Bell parameters.
aDraftBell = 5
tBell = 5 // Wall thickness.
hBellAboveDiscFace = 40
hBellSubflush = 4
wUndercut = 8
fn drillHole(activeSketch, t) {
// Sketch a vent hole at line parameter value t on an arc drawn across the disc surface.
rInner = dDisc / 2 - hFrictionSurface
rOuter = dDisc / 2
aStart = aBase
aEnd = aBase - aSweep
// Linear interpolation of radius.
rCurrent = rInner + t * (rOuter - rInner)
// Linear interpolation of angle.
aCurrent = aStart + t * (aEnd - aStart)
// Calculate position.
xCenter = rCurrent * cos(aCurrent)
yCenter = rCurrent * sin(aCurrent)
// Draw.
drillCircle = circle(activeSketch, center = [xCenter, yCenter], radius = dDrillDia / 2)
return drillCircle
}
fn createDiscHalf(plane, dDiscParam, hFrictionSurfaceParam, tDiscHalfParam) {
// Create a disc half with a vent hole pattern.
sketchFace = startSketchOn(plane)
profileFace = circle(sketchFace, center = [0, 0], radius = dDiscParam / 2)
|> subtract2d(tool = circle(sketchFace, center = [0, 0], radius = dDiscParam / 2 - hFrictionSurfaceParam))
// Create three circles at t = 0, 0.5, and 1
hole1 = drillHole(activeSketch = sketchFace, t = 0.2)
hole2 = drillHole(activeSketch = sketchFace, t = 0.5)
hole3 = drillHole(activeSketch = sketchFace, t = 0.8)
// Pattern and cut.
holes = patternCircular2d(
[hole1, hole2, hole3],
instances = nArcs,
center = [0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
profileDrilled = subtract2d(profileFace, tool = holes)
// Extrude.
discHalf = extrude(profileFace, length = tDiscHalfParam)
return discHalf
}
// ---------------------------------------------------------------------------------------------------------------------
// Create inboard half.
discInboard = createDiscHalf(
plane = XY,
dDiscParam = dDisc,
hFrictionSurfaceParam = hFrictionSurface,
tDiscHalfParam = tDiscHalf,
)
// Create vents.
planeVent = offsetPlane(XY, offset = tDiscHalf)
sketchVent = startSketchOn(planeVent)
profileVent = startProfile(sketchVent, at = [-wVent, dDisc / 2])
|> angledLine(angle = 0, length = wVent, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = hFrictionSurface, tag = $seg02)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg03)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
ventPad = extrude(profileVent, length = tVent)
|> fillet(
radius = rVentFillet,
tags = [
getCommonEdge(faces = [seg01, rectangleSegmentA001]),
getCommonEdge(faces = [seg02, rectangleSegmentA001]),
getCommonEdge(faces = [seg01, seg03]),
getCommonEdge(faces = [seg03, seg02])
],
)
ventSet = patternCircular3d(
ventPad,
instances = nVentBosses,
axis = [0, 0, 1],
center = [0, 0, tDiscHalf],
arcDegrees = 360,
rotateDuplicates = true,
)
// Create outboard half.
planeOutboard = offsetPlane(XY, offset = tDiscHalf + tVent)
discOutboard = createDiscHalf(
plane = planeOutboard,
dDiscParam = dDisc,
hFrictionSurfaceParam = hFrictionSurface,
tDiscHalfParam = tDiscHalf,
)
// Now create bell.
rCenter = dDisc / 2 - hFrictionSurface - wUndercut
rBore = dBore / 2
lDraftExterior = hBellAboveDiscFace / tan(90 - aDraftBell)
lDraftInterior = (hBellAboveDiscFace - tBell) / tan(90 - aDraftBell)
// Inner and outer radius of outboard face of disc bell.
rOuter = rCenter - lDraftExterior - rBore
rInner = rOuter + lDraftExterior - (tBell + lDraftInterior)
sketchDiscBell = startSketchOn(-YZ)
bodyDiscBell = startProfile(
sketchDiscBell,
at = [
-dDisc / 2 + hFrictionSurface,
tDiscHalf * 2 + tVent
],
)
|> arc(
%,
angleStart = -180,
angleEnd = 0,
radius = wUndercut / 2,
)
|> line(end = [lDraftExterior, hBellAboveDiscFace])
|> xLine(length = rOuter, tag = $seg04)
|> yLine(length = -tBell)
|> xLine(length = -rInner)
|> line(end = [-lDraftInterior, -hBellAboveDiscFace])
|> line(end = [0, -2]) // Wall thickness.
|> xLine(length = -1 * (tBell + wUndercut))
|> close(%)
|> revolve(axis = Y)
// Drill lug holes.
sketchLugs = startSketchOn(bodyDiscBell, face = seg04)
profileStud = circle(sketchLugs, center = [0, dPitchCircle / 2], radius = dStudDrilling / 2)
|> patternCircular2d(
%,
instances = nStuds,
center = [0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
clearance = 2 // Some margin on negative extrude.
lugs = extrude(profileStud, length = -1 * (tBell + clearance))

View File

@ -1,155 +0,0 @@
// Curtain Wall Anchor Plate
// A structural steel L-plate used to anchor curtain wall systems to concrete slabs, with elongated holes for adjustability and bolts with nuts and base plates for secure fastening
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define parameters
slabPlateBaseLength = 300
slabPlateHookLength = 80
slabPlateWidth = 200
slabPlateThickness = 8
offsetSlabRail = 200
// Generate L-shaped anchor profile with base and hook flange
// Includes fillets at internal and external corners for strength and safety
fn lProfileFn(lengthBase, lengthHook, width, thickness) {
profilePlane = startSketchOn(offsetPlane(XZ, offset = -width / 2))
profileShape = startProfile(profilePlane, at = [0, 0])
|> yLine(length = lengthHook, tag = $hookOutside)
|> xLine(length = thickness)
|> yLine(length = thickness - lengthHook, tag = $hookInside)
|> xLine(length = lengthBase - thickness, tag = $baseInside)
|> yLine(length = -thickness)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $baseOutside)
|> close()
profileBody = extrude(profileShape, length = width)
|> fillet(
radius = thickness,
tags = [
getCommonEdge(faces = [baseInside, hookInside])
],
)
|> fillet(
radius = thickness * 2,
tags = [
getCommonEdge(faces = [baseOutside, hookOutside])
],
)
return profileBody
}
// Create a hexagonal shape used for bolt and nut heads
fn hexagonFn(plane, radius) {
shape = startProfile(plane, at = [-radius, 0])
|> angledLine(angle = 60, length = radius)
|> xLine(length = radius)
|> angledLine(angle = -60, length = radius)
|> angledLine(angle = -120, length = radius)
|> xLine(length = -radius)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
return shape
}
// Build a bolt with a hexagonal head and cylindrical shaft
fn boltFn(diameter, length) {
boltHeadPlane = startSketchOn(XY)
boltHeadShape = hexagonFn(plane = boltHeadPlane, radius = diameter)
boltHeadBody = extrude(boltHeadShape, length = diameter * 0.7)
boltPlane = startSketchOn(boltHeadBody, face = START)
boltShape = circle(boltPlane, center = [0, 0], radius = diameter / 2)
boltBody = extrude(boltShape, length = length)
return boltBody
}
// Construct a bolt assembly with base plate and hex nut
// Assembles all parts for realistic anchor simulation
fn boltWithPlateAndNutFn(diameter, length, gap) {
plateSide = diameter * 3
plateplane = startSketchOn(offsetPlane(XY, offset = -gap))
plateShape = startProfile(plateplane, at = [-plateSide / 2, -plateSide / 2])
|> yLine(length = plateSide)
|> xLine(length = plateSide)
|> yLine(length = -plateSide)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
plateBody = extrude(plateShape, length = -diameter * 0.3)
nutPlane = startSketchOn(plateBody, face = START)
boltHeadShape = hexagonFn(plane = nutPlane, radius = 12)
boltHeadBody = extrude(boltHeadShape, length = diameter * 0.7)
boltBody = boltFn(diameter = diameter, length = gap + diameter + 3)
mergedBody = union([boltHeadBody, boltBody])
return mergedBody
}
// Generate the plate geometry with a vertical hook for slab attachment
slabPlate = lProfileFn(
lengthBase = slabPlateBaseLength,
lengthHook = slabPlateHookLength,
width = slabPlateWidth,
thickness = slabPlateThickness,
)
// Define oblong holes for bolts, allowing positional adjustment
wideHoleWidth = 12
wideHoleLength = 60
wideHoleOffset = 30
// Two slots mirrored across the plate width
wideHolePlane = startSketchOn(XY)
wideHoleShape = startProfile(
wideHolePlane,
at = [
-(wideHoleLength - wideHoleWidth) / 2,
wideHoleWidth / 2
],
)
|> xLine(length = wideHoleLength - wideHoleWidth)
|> tangentialArc(endAbsolute = [
(wideHoleLength - wideHoleWidth) / 2,
-wideHoleWidth / 2
])
|> xLine(length = wideHoleWidth - wideHoleLength)
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> translate(
%,
x = offsetSlabRail,
y = wideHoleOffset - (slabPlateWidth / 2),
z = -1,
)
wideHoleVoidLeft = extrude(wideHoleShape, length = slabPlateThickness + 2)
wideHoleVoidRight = clone(wideHoleVoidLeft)
|> translate(
%,
x = 0,
y = slabPlateWidth - (wideHoleOffset * 2),
z = 0,
)
// Cut the holes into the anchor plate body
slabPlatePunchOne = subtract([slabPlate], tools = [wideHoleVoidLeft])
slabPlatePunchTwo = subtract([slabPlatePunchOne], tools = [wideHoleVoidRight])
// Add two bolt assemblies into the oblong slots
// Properly rotated and spaced to match anchor hole layout
slabPlateBolts = boltWithPlateAndNutFn(diameter = 10, length = 20, gap = slabPlateThickness + 5)
|> rotate(
%,
roll = 180,
pitch = 0,
yaw = 0,
)
|> translate(
%,
x = offsetSlabRail,
y = wideHoleOffset - (slabPlateWidth / 2),
z = 5,
)
|> patternLinear3d(
%,
instances = 2,
distance = slabPlateWidth - (wideHoleOffset * 2),
axis = [0, -1, 0],
)

View File

@ -1,79 +0,0 @@
// Engine Valve
// A mechanical valve used in internal combustion engines to control intake or exhaust flow
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define parameters
valveDiameter = 30
valveLength = 120
valveHeadLength = valveDiameter * 1.0
valveHeadThickness = 3
stemDiameter = 6
stemHeadLength = 9
stemLength = valveLength - valveHeadLength - stemHeadLength
// Create the valve head
valveRadius = valveDiameter / 2
valveHeadPlane = startSketchOn(XZ)
valveHeadShape = startProfile(valveHeadPlane, at = [-0.01, valveHeadLength])
|> xLine(length = 0.01 - (stemDiameter / 2))
|> line(endAbsolute = [0.01 - (stemDiameter / 2), valveRadius])
|> tangentialArc(endAbsolute = [-0.8 * valveRadius, valveHeadThickness], tag = $seg01)
|> tangentialArc(endAbsolute = [-valveRadius, 0])
|> xLine(length = 0.3 * valveRadius)
|> arc(
interiorAbsolute = [
-0.34 * valveRadius,
0.08 * valveRadius
],
endAbsolute = [
-0.02 * valveRadius,
0.11 * valveRadius
],
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
valveHead = revolve(valveHeadShape, angle = 360, axis = Y)
// Create the valve stem
valveStemSketch = startSketchOn(offsetPlane(XY, offset = valveHeadLength))
|> circle(center = [0, 0], radius = stemDiameter / 2)
|> extrude(length = stemLength - valveHeadLength - stemHeadLength)
// Create the valve stem end
stepLength = stemHeadLength / 10
step1 = startSketchOn(valveStemSketch, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.9)
|> extrude(%, length = stepLength * 2)
step2 = startSketchOn(step1, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.8)
|> extrude(%, length = stepLength)
step3 = startSketchOn(step2, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.9)
|> extrude(%, length = stepLength)
step4 = startSketchOn(step3, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.8)
|> extrude(%, length = stepLength)
step5 = startSketchOn(step4, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.9)
|> extrude(%, length = stepLength)
step6 = startSketchOn(step5, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.8)
|> extrude(%, length = stepLength)
step7 = startSketchOn(step6, face = END)
|> circle(
%,
center = [0, 0],
radius = stemDiameter / 2 * 0.9,
tag = $seg02,
)
|> extrude(%, length = stepLength * 3, tagEnd = $capEnd001)
|> chamfer(
length = 0.5,
tags = [
getCommonEdge(faces = [seg02, capEnd001])
],
)

View File

@ -74,16 +74,6 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "brake-rotor/main.kcl",
"multipleFiles": false,
"title": "A 320mm vented brake disc (rotor), with straight vanes, 30mm thick. The disc bell should accommodate 5 M12 wheel studs on a 114.3mm pitch circle diameter.",
"description": "",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
@ -157,16 +147,6 @@
"removable-sticker.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "curtain-wall-anchor-plate/main.kcl",
"multipleFiles": false,
"title": "Curtain Wall Anchor Plate",
"description": "A structural steel L-plate used to anchor curtain wall systems to concrete slabs, with elongated holes for adjustability and bolts with nuts and base plates for secure fastening",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "cycloidal-gear/main.kcl",
@ -197,16 +177,6 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "engine-valve/main.kcl",
"multipleFiles": false,
"title": "Engine Valve",
"description": "A mechanical valve used in internal combustion engines to control intake or exhaust flow",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "exhaust-manifold/main.kcl",
@ -452,16 +422,6 @@
"robot-rotating-base.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pdu-faceplate/main.kcl",
"multipleFiles": false,
"title": "Power Distribution Unit (PDU) faceplate with European plug sockets and switch",
"description": "Designed for standard 19-inch rack systems with 1U height and 8 sockets",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pillow-block-bearing/main.kcl",
@ -552,16 +512,6 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "sash-window/main.kcl",
"multipleFiles": false,
"title": "Sash Window",
"description": "A traditional wooden sash window with two vertically sliding panels and a central locking mechanism",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
@ -572,16 +522,6 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "shepherds-hook-bolt/main.kcl",
"multipleFiles": false,
"title": "Shepherds Hook Bolt",
"description": "A bent bolt with a curved hook, typically used for hanging or anchoring loads. The threaded end allows secure attachment to surfaces or materials, while the curved hook resists pull-out under tension.",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
@ -592,16 +532,6 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "spinning-highrise-tower/main.kcl",
"multipleFiles": false,
"title": "Spinning Highrise Tower",
"description": "A conceptual high-rise tower with a central core and rotating floor slabs, demonstrating dynamic form through vertical repetition and transformation",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "spur-gear/main.kcl",
@ -632,16 +562,6 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "thermal-block-insert/main.kcl",
"multipleFiles": false,
"title": "Thermal Block Insert",
"description": "Interlocking insulation insert for masonry walls, designed with a tongue-and-groove profile for modular alignment and thermal efficiency",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "tooling-nest-block/main.kcl",

View File

@ -1,240 +0,0 @@
// Power Distribution Unit (PDU) faceplate with European plug sockets and switch
// Designed for standard 19-inch rack systems with 1U height and 8 sockets
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define the dimensions
// Width fits standard 19” rack, height is 1U, depth is variable
faceplateWidth = 482.6 // this is standardized to fit 19-inch racks)
faceplateHeight = 44.45 // usually 1U (44.45 mm), but can be 2U (88.9 mm) or more
faceplateDepth = 100 // varies by manufacturer, but commonly between 100 mm and 300 mm
// Define dimensions of side supports (width and thickness)
supportWidth = 50
supportThickness = 3
// Main body of the PDU faceplate with integrated rack mounting flanges
faceplateShape = startSketchOn(offsetPlane(XY, offset = -faceplateHeight / 2))
|> startProfile(%, at = [-faceplateWidth / 2 - supportWidth, 0])
|> yLine(length = supportThickness)
|> xLine(length = supportWidth)
|> yLine(length = faceplateDepth - supportThickness)
|> xLine(length = faceplateWidth)
|> yLine(length = supportThickness - faceplateDepth)
|> xLine(length = supportWidth)
|> yLine(length = -supportThickness)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
faceplateBody = extrude(faceplateShape, length = faceplateHeight)
faceplateFrontFace = startSketchOn(faceplateBody, face = seg01)
// Creates recessed volume within the faceplate for inserting modules
nestWall = 2
nestWidth = faceplateWidth - (nestWall * 2)
nestHeight = faceplateHeight - (nestWall * 2)
nestDepth = faceplateDepth - nestWall
nestShape = startProfile(faceplateFrontFace, at = [-nestWidth / 2, nestHeight / 2])
|> xLine(length = nestWidth)
|> yLine(length = -nestHeight)
|> xLine(length = -nestWidth)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
nestVoid = extrude(nestShape, length = -nestDepth)
// Spacer block on the left side, used to position components correctly
moduleHeight = nestHeight
moduleWidth = nestHeight
moduleDepth = nestHeight
leftSpacerWidth = moduleWidth * 1.5
leftSpacerPosition = leftSpacerWidth / 2 - (nestWidth / 2)
fn boxModuleFn(width) {
shape = startSketchOn(XZ)
|> startProfile(%, at = [-width / 2, moduleHeight / 2])
|> xLine(length = width)
|> yLine(length = -moduleHeight)
|> xLine(length = -width)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
body = extrude(shape, length = -moduleDepth)
return body
}
leftSpacerShape = boxModuleFn(width = leftSpacerWidth)
|> translate(
%,
x = leftSpacerPosition,
y = 0,
z = 0,
)
// Module for power switch including front plate and red rocker button
switchPosition = leftSpacerPosition + leftSpacerWidth / 2 + moduleWidth / 2
switchWidth = moduleWidth
// Switch Body
switchBody = boxModuleFn(width = moduleWidth)
// Switch Plate
switchPlateWidth = 20
switchPlateHeight = 30
switchPlateThickness = 3
switchPlateShape = startSketchOn(switchBody, face = END)
|> startProfile(
%,
at = [
-switchPlateWidth / 2,
-switchPlateHeight / 2
],
)
|> yLine(length = switchPlateHeight)
|> xLine(length = switchPlateWidth)
|> yLine(length = -switchPlateHeight)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
switchPlateBody = extrude(switchPlateShape, length = switchPlateThickness)
|> translate(
%,
x = switchPosition,
y = 0,
z = 0,
)
// Switch Button
switchButtonHeight = 26
switchButtonWidth = 15
switchButtonShape = startSketchOn(offsetPlane(-YZ, offset = -switchButtonWidth / 2))
|> startProfile(
%,
at = [
switchPlateThickness,
switchButtonHeight / 2
],
)
|> line(end = [3, -1])
|> arc(interiorAbsolute = [6, 0], endAbsolute = [12, -9])
|> line(endAbsolute = [
switchPlateThickness,
-switchButtonHeight / 2
])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
switchButtonBody = extrude(switchButtonShape, length = switchButtonWidth)
|> translate(
%,
x = switchPosition,
y = 0,
z = 0,
)
|> appearance(%, color = "#ff0000")
// Spacer between switch and plug modules for layout alignment
secondSpacerWidth = moduleWidth / 2
secondSpacerPosition = switchPosition + switchWidth / 2 + secondSpacerWidth / 2
secondSpacerBody = boxModuleFn(width = secondSpacerWidth)
|> translate(
%,
x = secondSpacerPosition,
y = 0,
z = 0,
)
// European power plug modules with circular sockets and two-pin holes
// 8 identical sockets, each with grounding notch and dual-pin recesses
powerPlugWidth = moduleWidth
powerPlugCount = 8
powerPlugOveralWidth = powerPlugWidth * powerPlugCount
firstPowerPlugPosition = secondSpacerPosition + secondSpacerWidth / 2 + powerPlugWidth / 2
lastPowerPlugPosition = firstPowerPlugPosition + powerPlugWidth * (powerPlugCount - 1)
powerPlugBody = boxModuleFn(width = powerPlugWidth)
|> translate(
%,
x = firstPowerPlugPosition,
y = 0,
z = 0,
)
plugShape = startSketchOn(powerPlugBody, face = END)
|> circle(%, center = [0, 0], radius = 17)
|> translate(
%,
x = firstPowerPlugPosition,
y = 0,
z = 0,
)
plugBody = extrude(plugShape, length = -20)
plugHoleDistance = 20
plugHoleShape = startSketchOn(plugBody, face = START)
|> circle(%, center = [-plugHoleDistance / 2, 0], radius = 3)
|> translate(
%,
x = firstPowerPlugPosition,
y = 0,
z = 0,
)
|> patternLinear2d(
%,
instances = 2,
distance = plugHoleDistance,
axis = [1, 0],
)
plugHoleBody = extrude(plugHoleShape, length = -5)
|> patternLinear3d(
%,
instances = powerPlugCount,
distance = powerPlugWidth,
axis = [1, 0, 0],
)
// Rightmost spacer to fill in remaining horizontal space
rightSpacerWidth = nestWidth / 2 - lastPowerPlugPosition - (powerPlugWidth / 2)
rightSpacerPosition = lastPowerPlugPosition + powerPlugWidth / 2 + rightSpacerWidth / 2
rightSpacerBody = boxModuleFn(width = rightSpacerWidth)
|> translate(
%,
x = rightSpacerPosition,
y = 0,
z = 0,
)
// Rack mounting holes on flanges, elongated for alignment flexibility
holeWidth = 25
holeDiameter = 5
holeStraightSegment = holeWidth - holeDiameter
holeVerticalDistance = faceplateHeight * 0.3
holeShapes = startProfile(
faceplateFrontFace,
at = [
-holeStraightSegment / 2,
holeDiameter / 2
],
)
|> xLine(length = holeStraightSegment)
|> tangentialArc(endAbsolute = [
holeStraightSegment / 2,
-holeDiameter / 2
])
|> xLine(length = -holeStraightSegment)
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> translate(
%,
x = -faceplateWidth / 2 - (supportWidth / 2),
y = 0,
z = -holeVerticalDistance,
)
|> patternLinear2d(
%,
instances = 3,
distance = holeVerticalDistance,
axis = [0, 1],
)
|> patternLinear2d(
%,
instances = 2,
distance = faceplateWidth + supportWidth,
axis = [1, 0],
)
holeVoid = extrude(holeShapes, length = -supportThickness)

View File

@ -1,214 +0,0 @@
// Sash Window
// A traditional wooden sash window with two vertically sliding panels and a central locking mechanism
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Window state: 0 for closed, 1 for open
windowState = 0
// Basic window dimensions
windowWidth = 500
windowHeight = 1000
// Frame thickness and depth
frameWidth = 30
frameDepth = 50
// Number of divisions per sash (horizontal and vertical)
sashOpeningCountHorizontal = 2
sashOpeningCountVertical = 1
// Derived dimensions
sashWidth = windowWidth - (frameWidth * 2)
sashHeight = (windowHeight - (frameWidth * 2)) / 2 + frameWidth / 2
sashDepth = frameDepth / 2 - 2
sashTravelDistance = sashHeight * windowState * 0.8
// Function to create panel with frame and openings
fn panelFn(plane, offset, width, height, depth, perimeter, divisionThickness, openingCountHorizontal, openingCountVertical) {
// Create panel base shape
panelPlane = startSketchOn(offsetPlane(XZ, offset = offset))
panelShape = startProfile(panelPlane, at = [-width / 2, -height / 2])
|> yLine(length = height)
|> xLine(length = width)
|> yLine(length = -height)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
panelBody = extrude(panelShape, length = depth)
// Create opening grid within the panel
voidAreaWidth = width - (perimeter * 2)
voidAreaHeight = height - (perimeter * 2)
divisionTotalThicknessHorizontal = divisionThickness * openingCountHorizontal - divisionThickness
divisionTotalThicknessVertical = divisionThickness * openingCountVertical - divisionThickness
voidWidth = (voidAreaWidth - divisionTotalThicknessHorizontal) / openingCountHorizontal
voidHeight = (voidAreaHeight - divisionTotalThicknessVertical) / openingCountVertical
voidStepHorizontal = voidWidth + divisionThickness
voidStepVertical = voidHeight + divisionThickness
voidPlane = startSketchOn(panelBody, face = END)
voidShape = startProfile(
voidPlane,
at = [
-voidAreaWidth / 2,
-voidAreaHeight / 2
],
)
|> yLine(length = voidHeight)
|> xLine(length = voidWidth)
|> yLine(length = -voidHeight)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> patternLinear2d(
%,
instances = openingCountHorizontal,
distance = voidStepHorizontal,
axis = [1, 0],
)
|> patternLinear2d(
%,
instances = openingCountVertical,
distance = voidStepVertical,
axis = [0, 1],
)
voidBody = extrude(voidShape, length = -depth)
|> appearance(color = "#a55e2c")
return panelBody
}
// Create main window frame
frame = panelFn(
plane = XZ,
offset = -frameDepth / 2,
width = windowWidth,
height = windowHeight,
depth = frameDepth,
perimeter = frameWidth,
divisionThickness = 10,
openingCountHorizontal = 1,
openingCountVertical = 1,
)
// Create bottom sliding sash
bottomSash = panelFn(
plane = XZ,
offset = (frameDepth / 2 - sashDepth) / 2,
width = sashWidth,
height = sashHeight,
depth = sashDepth,
perimeter = frameWidth,
divisionThickness = 10,
openingCountHorizontal = sashOpeningCountHorizontal,
openingCountVertical = sashOpeningCountVertical,
)
|> translate(
%,
x = 0,
y = 0,
z = frameWidth / 2 - (sashHeight / 2),
)
|> translate(
%,
x = 0,
y = 0,
z = sashTravelDistance,
) // open / close
// Latch mechanism on bottom sash
// Create latch plate
latchPlateWidth = 13
latchPlateLength = 30
latchPlateThickness = 1
latchPlatePlane = startSketchOn(offsetPlane(XY, offset = frameWidth / 2))
latchPlateShape = startProfile(
latchPlatePlane,
at = [
-latchPlateLength / 2,
-latchPlateWidth / 2
],
)
|> yLine(length = latchPlateWidth)
|> xLine(length = latchPlateLength)
|> yLine(length = -latchPlateWidth)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
latchPlateBody = extrude(latchPlateShape, length = latchPlateThickness)
|> translate(
%,
x = 0,
y = -frameDepth / 4,
z = 0,
)
|> translate(
%,
x = 0,
y = 0,
z = sashTravelDistance,
) // open / close
// Create latch cylinder
latchCylinderHeight = 5
latchCylinderPlane = startSketchOn(offsetPlane(latchPlatePlane, offset = latchPlateThickness))
latchCylinderShape = startProfile(latchCylinderPlane, at = [40, -1])
|> xLine(length = -35)
|> arc(interiorAbsolute = [-5, 0], endAbsolute = [5, 1])
|> xLine(length = 35)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
latchCylinderBody = extrude(latchCylinderShape, length = latchCylinderHeight)
|> translate(
%,
x = 0,
y = -frameDepth / 4,
z = 0,
)
|> translate(
%,
x = 0,
y = 0,
z = sashTravelDistance,
) // open / close
|> rotate(
%,
roll = 0,
pitch = 0,
yaw = -90 * windowState,
)
// Create top fixed sash
topSash = panelFn(
plane = XZ,
offset = -(frameDepth / 2 - sashDepth) / 2 - sashDepth,
width = sashWidth,
height = sashHeight,
depth = sashDepth,
perimeter = frameWidth,
divisionThickness = 10,
openingCountHorizontal = sashOpeningCountHorizontal,
openingCountVertical = sashOpeningCountVertical,
)
|> translate(
%,
x = 0,
y = 0,
z = sashHeight / 2 - (frameWidth / 2),
)
// Create latch nut on the top sash
latchNutPlane = startSketchOn(XZ)
latchNutShape = startProfile(
latchNutPlane,
at = [
-latchPlateLength / 2,
-latchPlateWidth / 2
],
)
|> yLine(length = latchPlateWidth)
|> xLine(length = latchPlateLength)
|> yLine(length = -latchPlateWidth)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
latchNutPlateBody = extrude(latchNutShape, length = latchPlateThickness)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@ -1,89 +0,0 @@
// Shepherds Hook Bolt
// A bent bolt with a curved hook, typically used for hanging or anchoring loads. The threaded end allows secure attachment to surfaces or materials, while the curved hook resists pull-out under tension.
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define bolt geometry parameters
boltDiameter = 5
hookRadius = 12
shankLength = 5
threadedEndLength = 30
nutDistance = 20
hookStartAngle = 290
hookEndAngle = 150
approximatePitch = boltDiameter * 0.15
threadDepth = 0.6134 * approximatePitch
innerRadius = boltDiameter / 2 - threadDepth
boltNumberOfRevolutions = threadedEndLength / approximatePitch
// Helper values for computing geometry transitions between straight shaft and hook arc
hypotenuse = hookRadius / cos(hookStartAngle - 270)
side = sqrt(pow(hypotenuse, exp = 2) - pow(hookRadius, exp = 2))
shankOffset = hypotenuse + side
// Converts polar coordinates to cartesian points for drawing arcs
fn polarToCartesian(radius, angle) {
x = radius * cos(angle)
y = radius * sin(angle)
return [x, y]
}
// Create the hook and shank profile path
// Includes straight segment and two connected arcs forming the hook
hookProfilePlane = startSketchOn(XZ)
hookProfileShape = startProfile(hookProfilePlane, at = [0, -shankOffset - shankLength])
|> line(endAbsolute = [0, -shankOffset])
|> tangentialArc(endAbsolute = polarToCartesian(radius = hookRadius, angle = hookStartAngle))
|> tangentialArc(endAbsolute = polarToCartesian(radius = hookRadius, angle = hookEndAngle), tag = $hook)
// Create the circular cross-section used for sweeping along the hook path
hookSectionPlane = offsetPlane(XY, offset = -shankOffset - shankLength)
hookSectionShape = circle(hookSectionPlane, center = [0, 0], radius = boltDiameter / 2)
// Sweep the section along the hook profile to form the main body of the hook bolt
hookBody = sweep(hookSectionShape, path = hookProfileShape, sectional = true)
// Add a cylindrical tip at the hook end
tipPlane = startSketchOn(hookBody, face = END)
tipShape = circle(
tipPlane,
center = [hookRadius, 0],
radius = boltDiameter / 2,
tag = $seg01,
)
tipBody = extrude(
tipShape,
length = hookRadius * 0.5,
tagStart = $startTag,
tagEnd = $capEnd001,
)
|> fillet(
radius = boltDiameter / 4,
tags = [
getCommonEdge(faces = [seg01, capEnd001])
],
)
// Create the threaded end of the bolt
// Construct the triangular profile for thread cutting
boltThreadSectionPlane = startSketchOn(XZ)
boltThreadSectionShapeForRevolve = startProfile(
boltThreadSectionPlane,
at = [
innerRadius,
-shankOffset - shankLength - threadedEndLength
],
)
|> line(end = [threadDepth, approximatePitch / 2])
|> line(end = [-threadDepth, approximatePitch / 2])
|> patternLinear2d(axis = [0, 1], instances = boltNumberOfRevolutions, distance = approximatePitch)
|> xLine(length = -innerRadius * 0.9)
|> yLine(length = -threadedEndLength)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
// Create a revolved solid representing the thread geometry by repeating and revolving the profile around the shaft
boltThreadRevolve = revolve(boltThreadSectionShapeForRevolve, angle = 360, axis = Y)

View File

@ -1,93 +0,0 @@
// Spinning Highrise Tower
// A conceptual high-rise tower with a central core and rotating floor slabs, demonstrating dynamic form through vertical repetition and transformation
@settings(defaultLengthUnit = m, kclVersion = 1.0)
// Define global parameters for floor geometry and building layout
floorCount = 17
floorHeight = 5
slabWidth = 30
slabThickness = 0.5
rotationAngleStep = 5
handrailHeight = 1.2
handrailThickness = 0.3
balconyDepth = 3
// Calculate facade and core geometry from parameters
facadeWidth = slabWidth - (balconyDepth * 2)
facadeHeight = floorHeight - slabThickness
coreHeight = floorCount * floorHeight - slabThickness
frameSide = 0.1
windowTargetWidth = 6
windowTargetCount = facadeWidth / windowTargetWidth
windowCount = round(windowTargetCount)
windowWidth = facadeWidth / windowCount
// Helper function: Creates a box from a center plane with given width and height
fn boxFn(plane, width, height) {
shape = startSketchOn(plane)
|> startProfile(%, at = [-width / 2, -width / 2])
|> line(%, end = [0, width])
|> line(%, end = [width, 0])
|> line(%, end = [0, -width])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%)
body = extrude(shape, length = height)
return body
}
// Helper function: Defines transformation (translation and rotation) for each floor
fn transformFn(@i) {
return {
translate = [0, 0, i * floorHeight],
rotation = { angle = rotationAngleStep * i }
}
}
// Create building base
baseThickness = 0.2
baseSlab = boxFn(plane = XY, width = slabWidth, height = -baseThickness)
|> appearance(%, color = "#dbd7d2")
// Create ground platform beneath the base
groundSize = 50
groundBody = boxFn(plane = offsetPlane(XY, offset = -baseThickness), width = groundSize, height = -5)
|> appearance(%, color = "#3a3631")
// Create a single slab with handrail height to be reused with pattern
slabAndHandrailGeometry = boxFn(plane = offsetPlane(XY, offset = floorHeight - slabThickness), width = slabWidth, height = slabThickness + handrailHeight)
slabVoidStart = -slabWidth / 2 + handrailThickness
slabVoidWidth = slabWidth - (handrailThickness * 2)
slabVoidShape = startSketchOn(slabAndHandrailGeometry, face = END)
|> startProfile(%, at = [slabVoidStart, slabVoidStart])
|> line(%, end = [0, slabVoidWidth])
|> line(%, end = [slabVoidWidth, 0])
|> line(%, end = [0, -slabVoidWidth])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%)
// Generate and pattern slabs with voids across all floors
slabBody = extrude(slabVoidShape, length = -handrailHeight)
|> patternTransform(instances = floorCount, transform = transformFn)
|> appearance(%, color = "#dbd7d2")
// Create structural core of the tower
coreLength = 10
coreWidth = 8
core = startSketchOn(XY)
|> startProfile(%, at = [-coreLength / 2, -coreWidth / 2])
|> line(%, end = [0, coreWidth])
|> line(%, end = [coreLength, 0])
|> line(%, end = [-0.22, -coreWidth])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%)
|> extrude(%, length = coreHeight)
// Create facade panels for each floor
facadeStart = facadeWidth / 2
facadeGeometry = boxFn(plane = XY, width = facadeWidth, height = facadeHeight)
|> patternTransform(instances = floorCount, transform = transformFn)
|> appearance(%, color = "#151819")

View File

@ -1,61 +0,0 @@
// Thermal Block Insert
// Interlocking insulation insert for masonry walls, designed with a tongue-and-groove profile for modular alignment and thermal efficiency
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define overall dimensions of the insert block
insertLength = 400
insertHeight = 200
insertThickness = 50
// Define tongue-and-groove profile parameters for interlocking geometry
setbackFactor = 0.25 // spacing between tongues
tongueTargetCount = insertLength / 80
tongueCount = round(tongueTargetCount)
tongueLength = insertLength / (tongueCount * (1 + setbackFactor * 2) + 1)
tongueGap = tongueLength * setbackFactor * 2
tongueStep = tongueLength + tongueGap
tongueDepth = tongueLength * 0.5
tongueSetback = tongueLength * setbackFactor
// Function to create one side of the repeating tongue geometry along the block edge
fn tongueBlockFn() {
tongueSingleBlock = xLine(length = tongueLength)
|> line(end = [-tongueSetback, tongueDepth])
|> xLine(length = tongueLength)
|> line(end = [-tongueSetback, -tongueDepth])
|> patternLinear2d(
%,
instances = tongueCount,
distance = tongueStep,
axis = [1, 0],
)
|> xLine(length = tongueLength)
return tongueSingleBlock
}
// Create top-side profile with tongues
tongueShape = startSketchOn(XY)
|> startProfile(%, at = [-insertLength / 2, insertThickness / 2])
|> tongueBlockFn()
|> yLine(length = -insertThickness / 2)
|> xLine(length = -insertLength)
|> close(%)
// Create bottom-side profile with grooves (inverse of tongue)
grooveShape = startSketchOn(XY)
|> startProfile(
%,
at = [
-insertLength / 2,
-insertThickness / 2 - tongueDepth
],
)
|> tongueBlockFn()
|> yLine(length = insertThickness / 2 + tongueDepth)
|> xLine(length = -insertLength)
|> close(%)
// Extrude both tongue and groove profiles to form the final thermal insert block
insertShape = extrude([tongueShape, grooveShape], length = insertHeight)

20
rust/Cargo.lock generated
View File

@ -1815,7 +1815,7 @@ dependencies = [
[[package]]
name = "kcl-bumper"
version = "0.1.75"
version = "0.1.74"
dependencies = [
"anyhow",
"clap",
@ -1826,7 +1826,7 @@ dependencies = [
[[package]]
name = "kcl-derive-docs"
version = "0.1.75"
version = "0.1.74"
dependencies = [
"Inflector",
"anyhow",
@ -1845,7 +1845,7 @@ dependencies = [
[[package]]
name = "kcl-directory-test-macro"
version = "0.1.75"
version = "0.1.74"
dependencies = [
"convert_case",
"proc-macro2",
@ -1855,7 +1855,7 @@ dependencies = [
[[package]]
name = "kcl-language-server"
version = "0.2.75"
version = "0.2.74"
dependencies = [
"anyhow",
"clap",
@ -1876,7 +1876,7 @@ dependencies = [
[[package]]
name = "kcl-language-server-release"
version = "0.1.75"
version = "0.1.74"
dependencies = [
"anyhow",
"clap",
@ -1896,7 +1896,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.75"
version = "0.2.74"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1973,7 +1973,7 @@ dependencies = [
[[package]]
name = "kcl-python-bindings"
version = "0.3.75"
version = "0.3.74"
dependencies = [
"anyhow",
"kcl-lib",
@ -1988,7 +1988,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.75"
version = "0.1.74"
dependencies = [
"anyhow",
"hyper 0.14.32",
@ -2001,7 +2001,7 @@ dependencies = [
[[package]]
name = "kcl-to-core"
version = "0.1.75"
version = "0.1.74"
dependencies = [
"anyhow",
"async-trait",
@ -2015,7 +2015,7 @@ dependencies = [
[[package]]
name = "kcl-wasm-lib"
version = "0.1.75"
version = "0.1.74"
dependencies = [
"anyhow",
"bson",

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-bumper"
version = "0.1.75"
version = "0.1.74"
edition = "2021"
repository = "https://github.com/KittyCAD/modeling-api"
rust-version = "1.76"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.75"
version = "0.1.74"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files"
version = "0.1.75"
version = "0.1.74"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-language-server-release"
version = "0.1.75"
version = "0.1.74"
edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false

View File

@ -2,7 +2,7 @@
name = "kcl-language-server"
description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.75"
version = "0.2.74"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.75"
version = "0.2.74"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -2,7 +2,7 @@ mod cache;
use kcl_lib::{
test_server::{execute_and_export_step, execute_and_snapshot, execute_and_snapshot_no_auth},
BacktraceItem, ExecError, ModuleId, SourceRange,
ExecError,
};
/// The minimum permissible difference between asserted twenty-twenty images.
@ -441,15 +441,10 @@ async fn kcl_test_import_file_doesnt_exist() {
model = cube"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "File `thing.obj` does not exist.");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(0, 18, ModuleId::default()),
fn_name: None,
}]
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([0, 18, 0])], message: "File `thing.obj` does not exist." }"#
);
}
@ -524,18 +519,10 @@ import 'e2e/executor/inputs/cube.gltf'
model = cube"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert!(result.is_err());
assert_eq!(
err.message(),
"The given format does not match the file extension. Expected: `gltf`, Given: `obj`"
);
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(32, 70, ModuleId::default()),
fn_name: None,
}]
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([32, 70, 0])], message: "The given format does not match the file extension. Expected: `gltf`, Given: `obj`" }"#
);
}
@ -1679,15 +1666,10 @@ example = extrude(exampleSketch, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(70, 111, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 111, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
);
}
@ -1704,15 +1686,10 @@ example = extrude(exampleSketch, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(70, 112, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 112, 0])], message: "Cannot have an x constrained angle of 270 degrees" }"#
);
}
@ -1729,15 +1706,10 @@ example = extrude(exampleSketch, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(70, 110, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 110, 0])], message: "Cannot have a y constrained angle of 0 degrees" }"#
);
}
@ -1754,15 +1726,10 @@ example = extrude(exampleSketch, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(70, 112, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 112, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
);
}
@ -1779,15 +1746,10 @@ extrusion = extrude(sketch001, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(66, 116, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([66, 116, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
);
}
@ -1795,7 +1757,7 @@ extrusion = extrude(sketch001, length = 10)
async fn kcl_test_angled_line_of_x_length_270() {
let code = r#"sketch001 = startSketchOn(XZ)
|> startProfile(at = [0, 0])
|> angledLine(angle = 270, lengthX = 90, tag = $edge1)
|> angledLine(angle = 90, lengthX = 90, tag = $edge1)
|> angledLine(angle = -15, lengthX = -15, tag = $edge2)
|> line(end = [0, -5])
|> close(tag = $edge3)
@ -1804,15 +1766,10 @@ extrusion = extrude(sketch001, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(66, 117, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([66, 116, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
);
}
@ -1831,15 +1788,10 @@ example = extrude(exampleSketch, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(95, 130, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 130, 0])], message: "Cannot have a y constrained angle of 0 degrees" }"#
);
}
@ -1858,15 +1810,10 @@ example = extrude(exampleSketch, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(95, 132, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 132, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
);
}
@ -1885,15 +1832,10 @@ example = extrude(exampleSketch, length = 10)
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
assert!(result.is_err());
assert_eq!(
err.backtrace(),
vec![BacktraceItem {
source_range: SourceRange::new(95, 133, ModuleId::default()),
fn_name: Some("angledLine".to_owned())
}]
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 133, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
);
}
@ -1907,31 +1849,10 @@ someFunction('INVALID')
"#;
let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert!(result.is_err());
assert_eq!(
err.message(),
"This function expected the input argument to be Solid or Plane but it's actually of type string"
);
assert_eq!(
err.source_ranges(),
vec![
SourceRange::new(46, 55, ModuleId::default()),
SourceRange::new(60, 83, ModuleId::default()),
]
);
assert_eq!(
err.backtrace(),
vec![
BacktraceItem {
source_range: SourceRange::new(46, 55, ModuleId::default()),
fn_name: Some("someFunction".to_owned()),
},
BacktraceItem {
source_range: SourceRange::new(60, 83, ModuleId::default()),
fn_name: None,
},
]
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([46, 55, 0]), SourceRange([60, 83, 0])], message: "This function expected the input argument to be Solid or Plane but it's actually of type string" }"#
);
}
@ -1952,14 +1873,12 @@ async fn kcl_test_error_no_auth_websocket() {
"#;
let result = execute_and_snapshot_no_auth(code, None).await;
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert!(
err.message()
.contains("Please send the following object over this websocket"),
"actual: {}",
err.message()
);
assert!(result.is_err());
assert!(result
.err()
.unwrap()
.to_string()
.contains("Please send the following object over this websocket"));
}
#[tokio::test(flavor = "multi_thread")]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -995,7 +995,7 @@ mod tests {
let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"patternCircular3d(${0:%}, instances = ${1:10}, axis = [${2:3.14}, ${3:3.14}, ${4:3.14}], center = [${5:3.14}, ${6:3.14}, ${7:3.14}])"#
r#"patternCircular3d(${0:%}, instances = ${1:10}, axis = [${2:3.14}, ${3:3.14}, ${4:3.14}], center = [${5:3.14}, ${6:3.14}, ${7:3.14}], arcDegrees = ${8:3.14}, rotateDuplicates = ${9:false})"#
);
}

View File

@ -439,7 +439,12 @@ impl EngineManager for EngineConnection {
request_sent: tx,
})
.await
.map_err(|e| KclError::Engine(KclErrorDetails::new(format!("Failed to send debug: {}", e), vec![])))?;
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send debug: {}", e),
source_ranges: vec![],
})
})?;
let _ = rx.await;
Ok(())
@ -474,25 +479,25 @@ impl EngineManager for EngineConnection {
})
.await
.map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to send modeling command: {}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to send modeling command: {}", e),
source_ranges: vec![source_range],
})
})?;
// Wait for the request to be sent.
rx.await
.map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("could not send request to the engine actor: {e}"),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("could not send request to the engine actor: {e}"),
source_ranges: vec![source_range],
})
})?
.map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("could not send request to the engine: {e}"),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("could not send request to the engine: {e}"),
source_ranges: vec![source_range],
})
})?;
Ok(())
@ -516,15 +521,15 @@ impl EngineManager for EngineConnection {
// Check if we have any pending errors.
let pe = self.pending_errors.read().await;
if !pe.is_empty() {
return Err(KclError::Engine(KclErrorDetails::new(
pe.join(", ").to_string(),
vec![source_range],
)));
return Err(KclError::Engine(KclErrorDetails {
message: pe.join(", ").to_string(),
source_ranges: vec![source_range],
}));
} else {
return Err(KclError::Engine(KclErrorDetails::new(
"Modeling command failed: websocket closed early".to_string(),
vec![source_range],
)));
return Err(KclError::Engine(KclErrorDetails {
message: "Modeling command failed: websocket closed early".to_string(),
source_ranges: vec![source_range],
}));
}
}
@ -543,10 +548,10 @@ impl EngineManager for EngineConnection {
}
}
Err(KclError::Engine(KclErrorDetails::new(
format!("Modeling command timed out `{}`", id),
vec![source_range],
)))
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command timed out `{}`", id),
source_ranges: vec![source_range],
}))
}
async fn get_session_data(&self) -> Option<ModelingSessionData> {

View File

@ -147,27 +147,32 @@ impl EngineConnection {
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<(), KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize source range: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize modeling command: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize id to source range: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize id to source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
self.manager
.fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
Ok(())
}
@ -180,28 +185,33 @@ impl EngineConnection {
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<WebSocketResponse, KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize source range: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize modeling command: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize id to source range: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize id to source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let promise = self
.manager
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
// Try to parse the error as an engine error.
@ -209,52 +219,53 @@ impl EngineConnection {
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
serde_json::from_str(&err_str)
{
KclError::Engine(KclErrorDetails::new(
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
source_ranges: vec![source_range],
})
} else if let Ok(data) =
serde_json::from_str::<Vec<kittycad_modeling_cmds::websocket::FailureWebSocketResponse>>(&err_str)
{
if let Some(data) = data.first() {
// It could also be an array of responses.
KclError::Engine(KclErrorDetails::new(
data.errors
KclError::Engine(KclErrorDetails {
message: data
.errors
.iter()
.map(|e| e.message.clone())
.collect::<Vec<_>>()
.join("\n"),
vec![source_range],
))
source_ranges: vec![source_range],
})
} else {
KclError::Engine(KclErrorDetails::new(
"Received empty response from engine".into(),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: "Received empty response from engine".into(),
source_ranges: vec![source_range],
})
}
} else {
KclError::Engine(KclErrorDetails::new(
format!("Failed to wait for promise from send modeling command: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from send modeling command: {:?}", e),
source_ranges: vec![source_range],
})
}
})?;
if value.is_null() || value.is_undefined() {
return Err(KclError::Engine(KclErrorDetails::new(
"Received null or undefined response from engine".into(),
vec![source_range],
)));
return Err(KclError::Engine(KclErrorDetails {
message: "Received null or undefined response from engine".into(),
source_ranges: vec![source_range],
}));
}
// Convert JsValue to a Uint8Array
let data = js_sys::Uint8Array::from(value);
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to deserialize bson response from engine: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to deserialize bson response from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
Ok(ws_result)
@ -305,16 +316,18 @@ impl crate::engine::EngineManager for EngineConnection {
*self.default_planes.write().await = Some(new_planes);
// Start a new session.
let promise = self
.manager
.start_new_session()
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
let promise = self.manager.start_new_session().map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
crate::wasm::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to wait for promise from start new session: {:?}", e),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from start new session: {:?}", e),
source_ranges: vec![source_range],
})
})?;
Ok(())

View File

@ -276,10 +276,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
{
let duration = instant::Duration::from_millis(1);
wasm_timer::Delay::new(duration).await.map_err(|err| {
KclError::Internal(KclErrorDetails::new(
format!("Failed to sleep: {:?}", err),
vec![source_range],
))
KclError::Internal(KclErrorDetails {
message: format!("Failed to sleep: {:?}", err),
source_ranges: vec![source_range],
})
})?;
}
#[cfg(not(target_arch = "wasm32"))]
@ -293,10 +293,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
return Ok(response);
}
Err(KclError::Engine(KclErrorDetails::new(
"async command timed out".to_string(),
vec![source_range],
)))
Err(KclError::Engine(KclErrorDetails {
message: "async command timed out".to_string(),
source_ranges: vec![source_range],
}))
}
/// Ensure ALL async commands have been completed.
@ -547,10 +547,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
id_to_source_range.insert(Uuid::from(*cmd_id), *range);
}
_ => {
return Err(KclError::Engine(KclErrorDetails::new(
format!("The request is not a modeling command: {:?}", req),
vec![*range],
)));
return Err(KclError::Engine(KclErrorDetails {
message: format!("The request is not a modeling command: {:?}", req),
source_ranges: vec![*range],
}));
}
}
}
@ -595,10 +595,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
} else {
// We should never get here.
Err(KclError::Engine(KclErrorDetails::new(
format!("Failed to get batch response: {:?}", response),
vec![source_range],
)))
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to get batch response: {:?}", response),
source_ranges: vec![source_range],
}))
}
}
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
@ -610,20 +610,20 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// request so we need the original request source range in case the engine returns
// an error.
let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to get source range for command ID: {:?}", cmd_id),
vec![],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
source_ranges: vec![],
})
})?;
let ws_resp = self
.inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
.await?;
self.parse_websocket_response(ws_resp, source_range)
}
_ => Err(KclError::Engine(KclErrorDetails::new(
format!("The final request is not a modeling command: {:?}", final_req),
vec![source_range],
))),
_ => Err(KclError::Engine(KclErrorDetails {
message: format!("The final request is not a modeling command: {:?}", final_req),
source_ranges: vec![source_range],
})),
}
}
@ -729,10 +729,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
for (name, plane_id, color) in plane_settings {
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
// We should never get here.
KclError::Engine(KclErrorDetails::new(
format!("Failed to get default plane info for: {:?}", name),
vec![source_range],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to get default plane info for: {:?}", name),
source_ranges: vec![source_range],
})
})?;
planes.insert(
name,
@ -763,14 +763,15 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
WebSocketResponse::Success(success) => Ok(success.resp),
WebSocketResponse::Failure(fail) => {
let _request_id = fail.request_id;
Err(KclError::Engine(KclErrorDetails::new(
fail.errors
Err(KclError::Engine(KclErrorDetails {
message: fail
.errors
.iter()
.map(|e| e.message.clone())
.collect::<Vec<_>>()
.join("\n"),
vec![source_range],
)))
source_ranges: vec![source_range],
}))
}
}
}
@ -805,25 +806,25 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
BatchResponse::Failure { errors } => {
// Get the source range for the command.
let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails::new(
format!("Failed to get source range for command ID: {:?}", cmd_id),
vec![],
))
KclError::Engine(KclErrorDetails {
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
source_ranges: vec![],
})
})?;
return Err(KclError::Engine(KclErrorDetails::new(
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
vec![source_range],
)));
return Err(KclError::Engine(KclErrorDetails {
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
source_ranges: vec![source_range],
}));
}
}
}
// Return an error that we did not get an error or the response we wanted.
// This should never happen but who knows.
Err(KclError::Engine(KclErrorDetails::new(
format!("Failed to find response for command ID: {:?}", id),
vec![],
)))
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to find response for command ID: {:?}", id),
source_ranges: vec![],
}))
}
async fn modify_grid(

View File

@ -380,39 +380,20 @@ impl miette::Diagnostic for Report {
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
#[serde(rename_all = "camelCase")]
#[error("{message}")]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]
#[label(collection, "Errors")]
pub source_ranges: Vec<SourceRange>,
pub backtrace: Vec<BacktraceItem>,
#[serde(rename = "msg")]
pub message: String,
}
impl KclErrorDetails {
pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
let backtrace = source_ranges
.iter()
.map(|s| BacktraceItem {
source_range: *s,
fn_name: None,
})
.collect();
KclErrorDetails {
source_ranges,
backtrace,
message,
}
}
}
impl KclError {
pub fn internal(message: String) -> KclError {
KclError::Internal(KclErrorDetails {
source_ranges: Default::default(),
backtrace: Default::default(),
message,
})
}
@ -474,122 +455,45 @@ impl KclError {
}
}
pub fn backtrace(&self) -> Vec<BacktraceItem> {
match self {
KclError::Lexical(e)
| KclError::Syntax(e)
| KclError::Semantic(e)
| KclError::ImportCycle(e)
| KclError::Type(e)
| KclError::Io(e)
| KclError::Unexpected(e)
| KclError::ValueAlreadyDefined(e)
| KclError::UndefinedValue(e)
| KclError::InvalidExpression(e)
| KclError::Engine(e)
| KclError::Internal(e) => e.backtrace.clone(),
}
}
pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e)
| KclError::Syntax(e)
| KclError::Semantic(e)
| KclError::ImportCycle(e)
| KclError::Type(e)
| KclError::Io(e)
| KclError::Unexpected(e)
| KclError::ValueAlreadyDefined(e)
| KclError::UndefinedValue(e)
| KclError::InvalidExpression(e)
| KclError::Engine(e)
| KclError::Internal(e) => {
e.backtrace = source_ranges
.iter()
.map(|s| BacktraceItem {
source_range: *s,
fn_name: None,
})
.collect();
e.source_ranges = source_ranges;
}
KclError::Lexical(e) => e.source_ranges = source_ranges,
KclError::Syntax(e) => e.source_ranges = source_ranges,
KclError::Semantic(e) => e.source_ranges = source_ranges,
KclError::ImportCycle(e) => e.source_ranges = source_ranges,
KclError::Type(e) => e.source_ranges = source_ranges,
KclError::Io(e) => e.source_ranges = source_ranges,
KclError::Unexpected(e) => e.source_ranges = source_ranges,
KclError::ValueAlreadyDefined(e) => e.source_ranges = source_ranges,
KclError::UndefinedValue(e) => e.source_ranges = source_ranges,
KclError::InvalidExpression(e) => e.source_ranges = source_ranges,
KclError::Engine(e) => e.source_ranges = source_ranges,
KclError::Internal(e) => e.source_ranges = source_ranges,
}
new
}
pub(crate) fn set_last_backtrace_fn_name(&self, last_fn_name: Option<String>) -> Self {
pub(crate) fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e)
| KclError::Syntax(e)
| KclError::Semantic(e)
| KclError::ImportCycle(e)
| KclError::Type(e)
| KclError::Io(e)
| KclError::Unexpected(e)
| KclError::ValueAlreadyDefined(e)
| KclError::UndefinedValue(e)
| KclError::InvalidExpression(e)
| KclError::Engine(e)
| KclError::Internal(e) => {
if let Some(item) = e.backtrace.last_mut() {
item.fn_name = last_fn_name;
}
}
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
KclError::Type(e) => e.source_ranges.extend(source_ranges),
KclError::Io(e) => e.source_ranges.extend(source_ranges),
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
KclError::ValueAlreadyDefined(e) => e.source_ranges.extend(source_ranges),
KclError::UndefinedValue(e) => e.source_ranges.extend(source_ranges),
KclError::InvalidExpression(e) => e.source_ranges.extend(source_ranges),
KclError::Engine(e) => e.source_ranges.extend(source_ranges),
KclError::Internal(e) => e.source_ranges.extend(source_ranges),
}
new
}
pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e)
| KclError::Syntax(e)
| KclError::Semantic(e)
| KclError::ImportCycle(e)
| KclError::Type(e)
| KclError::Io(e)
| KclError::Unexpected(e)
| KclError::ValueAlreadyDefined(e)
| KclError::UndefinedValue(e)
| KclError::InvalidExpression(e)
| KclError::Engine(e)
| KclError::Internal(e) => {
if let Some(item) = e.backtrace.last_mut() {
item.fn_name = last_fn_name;
}
e.backtrace.push(BacktraceItem {
source_range,
fn_name: None,
});
e.source_ranges.push(source_range);
}
}
new
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct BacktraceItem {
pub source_range: SourceRange,
pub fn_name: Option<String>,
}
impl std::fmt::Display for BacktraceItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(fn_name) = &self.fn_name {
write!(f, "{fn_name}: {:?}", self.source_range)
} else {
write!(f, "(fn): {:?}", self.source_range)
}
}
}
impl IntoDiagnostic for KclError {
@ -647,7 +551,6 @@ impl From<pyo3::PyErr> for KclError {
fn from(error: pyo3::PyErr) -> Self {
KclError::Internal(KclErrorDetails {
source_ranges: vec![],
backtrace: Default::default(),
message: error.to_string(),
})
}
@ -726,13 +629,8 @@ impl CompilationError {
impl From<CompilationError> for KclErrorDetails {
fn from(err: CompilationError) -> Self {
let backtrace = vec![BacktraceItem {
source_range: err.source_range,
fn_name: None,
}];
KclErrorDetails {
source_ranges: vec![err.source_range],
backtrace,
message: err.message,
}
}

View File

@ -70,10 +70,10 @@ pub(super) fn expect_properties<'a>(
) -> Result<&'a [Node<ObjectProperty>], KclError> {
assert_eq!(annotation.name().unwrap(), for_key);
Ok(&**annotation.properties.as_ref().ok_or_else(|| {
KclError::Semantic(KclErrorDetails::new(
format!("Empty `{for_key}` annotation"),
vec![annotation.as_source_range()],
))
KclError::Semantic(KclErrorDetails {
message: format!("Empty `{for_key}` annotation"),
source_ranges: vec![annotation.as_source_range()],
})
})?)
}
@ -84,10 +84,10 @@ pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
}
}
Err(KclError::Semantic(KclErrorDetails::new(
"Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
vec![expr.into()],
)))
Err(KclError::Semantic(KclErrorDetails {
message: "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
source_ranges: vec![expr.into()],
}))
}
// Returns the unparsed number literal.
@ -98,10 +98,10 @@ pub(super) fn expect_number(expr: &Expr) -> Result<String, KclError> {
}
}
Err(KclError::Semantic(KclErrorDetails::new(
"Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
vec![expr.into()],
)))
Err(KclError::Semantic(KclErrorDetails {
message: "Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
source_ranges: vec![expr.into()],
}))
}
pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRange) -> Result<Option<Impl>, KclError> {
@ -113,14 +113,14 @@ pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRan
if &*p.key.name == IMPL {
if let Some(s) = p.value.ident_name() {
return Impl::from_str(s).map(Some).map_err(|_| {
KclError::Semantic(KclErrorDetails::new(
format!(
KclError::Semantic(KclErrorDetails {
message: format!(
"Invalid value for {} attribute, expected one of: {}",
IMPL,
IMPL_VALUES.join(", ")
),
vec![source_range],
))
source_ranges: vec![source_range],
})
});
}
}
@ -139,12 +139,12 @@ impl UnitLen {
"inch" | "in" => Ok(UnitLen::Inches),
"ft" => Ok(UnitLen::Feet),
"yd" => Ok(UnitLen::Yards),
value => Err(KclError::Semantic(KclErrorDetails::new(
format!(
value => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `in`, `ft`, `yd`"
),
vec![source_range],
))),
source_ranges: vec![source_range],
})),
}
}
}
@ -154,10 +154,10 @@ impl UnitAngle {
match s {
"deg" => Ok(UnitAngle::Degrees),
"rad" => Ok(UnitAngle::Radians),
value => Err(KclError::Semantic(KclErrorDetails::new(
format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
vec![source_range],
))),
value => Err(KclError::Semantic(KclErrorDetails {
message: format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
source_ranges: vec![source_range],
})),
}
}
}

View File

@ -2,17 +2,18 @@ use fnv::FnvHashMap;
use indexmap::IndexMap;
use kittycad_modeling_cmds::{
self as kcmc,
id::ModelingCmdId,
ok_response::OkModelingCmdResponse,
shared::ExtrusionFaceCapType,
websocket::{BatchResponse, OkWebSocketResponseData, WebSocketResponse},
EnableSketchMode, ModelingCmd,
};
use schemars::JsonSchema;
use serde::{ser::SerializeSeq, Serialize};
use uuid::Uuid;
use crate::{
errors::KclErrorDetails,
execution::ArtifactId,
parsing::ast::types::{Node, Program},
KclError, NodePath, SourceRange,
};
@ -57,6 +58,52 @@ impl PartialOrd for ArtifactCommand {
}
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Artifact.ts")]
pub struct ArtifactId(Uuid);
impl ArtifactId {
pub fn new(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl From<Uuid> for ArtifactId {
fn from(uuid: Uuid) -> Self {
Self::new(uuid)
}
}
impl From<&Uuid> for ArtifactId {
fn from(uuid: &Uuid) -> Self {
Self::new(*uuid)
}
}
impl From<ArtifactId> for Uuid {
fn from(id: ArtifactId) -> Self {
id.0
}
}
impl From<&ArtifactId> for Uuid {
fn from(id: &ArtifactId) -> Self {
id.0
}
}
impl From<ModelingCmdId> for ArtifactId {
fn from(id: ModelingCmdId) -> Self {
Self::new(*id.as_ref())
}
}
impl From<&ModelingCmdId> for ArtifactId {
fn from(id: &ModelingCmdId) -> Self {
Self::new(*id.as_ref())
}
}
pub type DummyPathToNode = Vec<()>;
fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>
@ -941,10 +988,12 @@ fn artifacts_to_update(
ModelingCmd::StartPath(_) => {
let mut return_arr = Vec::new();
let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
KclError::Internal(KclErrorDetails::new(
format!("Expected a current plane ID when processing StartPath command, but we have none: {id:?}"),
vec![range],
))
KclError::Internal(KclErrorDetails {
message: format!(
"Expected a current plane ID when processing StartPath command, but we have none: {id:?}"
),
source_ranges: vec![range],
})
})?;
return_arr.push(Artifact::Path(Path {
id,
@ -1063,10 +1112,10 @@ fn artifacts_to_update(
// TODO: Using the first one. Make sure to revisit this
// choice, don't think it matters for now.
path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
KclError::Internal(KclErrorDetails::new(
format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
vec![range],
))
KclError::Internal(KclErrorDetails {
message: format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
source_ranges: vec![range],
})
})?),
surface_ids: Vec::new(),
edge_ids: Vec::new(),
@ -1106,12 +1155,12 @@ fn artifacts_to_update(
};
last_path = Some(path);
let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::Internal(KclErrorDetails::new(
format!(
KclError::Internal(KclErrorDetails {
message:format!(
"Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
),
vec![range],
))
source_ranges: vec![range],
})
})?;
let extra_artifact = exec_artifacts.values().find(|a| {
if let Artifact::StartSketchOnFace(s) = a {
@ -1160,12 +1209,12 @@ fn artifacts_to_update(
continue;
};
let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::Internal(KclErrorDetails::new(
format!(
KclError::Internal(KclErrorDetails {
message:format!(
"Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
),
vec![range],
))
source_ranges: vec![range],
})
})?;
let extra_artifact = exec_artifacts.values().find(|a| {
if let Artifact::StartSketchOnFace(s) = a {

View File

@ -308,11 +308,6 @@ impl ArtifactGraph {
// a child of the line above it.
let label = label.unwrap_or("");
if code_ref.node_path.is_empty() {
if !code_ref.range.module_id().is_top_level() {
// This is pointing to another module. We don't care about
// these. It's okay that it's missing, for now.
return Ok(());
}
return writeln!(output, "{prefix} %% {label}Missing NodePath");
}
writeln!(output, "{prefix} %% {label}{:?}", code_ref.node_path.steps)

View File

@ -3,7 +3,7 @@ use schemars::JsonSchema;
use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{ModuleId, SourceRange};
use crate::{docs::StdLibFn, ModuleId, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
@ -13,6 +13,21 @@ use crate::{ModuleId, SourceRange};
pub enum Operation {
#[serde(rename_all = "camelCase")]
StdLibCall {
/// The standard library function being called.
#[serde(flatten)]
std_lib_fn: StdLibFnRef,
/// The unlabeled argument to the function.
unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>,
/// The source range of the operation in the source code.
source_range: SourceRange,
/// True if the operation resulted in an error.
#[serde(default, skip_serializing_if = "is_false")]
is_error: bool,
},
#[serde(rename_all = "camelCase")]
KclStdLibCall {
name: String,
/// The unlabeled argument to the function.
unlabeled_arg: Option<OpArg>,
@ -42,12 +57,19 @@ impl PartialOrd for Operation {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(match (self, other) {
(Self::StdLibCall { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { source_range: a, .. }, Self::KclStdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::KclStdLibCall { source_range: a, .. }, Self::KclStdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::KclStdLibCall { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::KclStdLibCall { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::KclStdLibCall { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::GroupBegin { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { source_range: a, .. }, Self::KclStdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::GroupEnd, Self::StdLibCall { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::KclStdLibCall { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::GroupBegin { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::GroupEnd) => std::cmp::Ordering::Equal,
})
@ -59,6 +81,7 @@ impl Operation {
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::KclStdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::GroupBegin { .. } | Self::GroupEnd => {}
}
}
@ -84,7 +107,6 @@ pub enum Group {
labeled_args: IndexMap<String, OpArg>,
},
/// A whole-module import use.
#[allow(dead_code)]
#[serde(rename_all = "camelCase")]
ModuleInstance {
/// The name of the module being used.
@ -113,6 +135,54 @@ impl OpArg {
}
}
/// A reference to a standard library function. This exists to implement
/// `PartialEq` and `Eq` for `Operation`.
#[derive(Debug, Clone, Serialize, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct StdLibFnRef {
// The following doc comment gets inlined into Operation, overriding what's
// there, in the generated TS. We serialize to its name. Renaming the
// field to "name" allows it to match the other variant.
/// The standard library function being called.
#[serde(
rename = "name",
serialize_with = "std_lib_fn_name",
deserialize_with = "std_lib_fn_from_name"
)]
#[ts(type = "string", rename = "name")]
pub std_lib_fn: Box<dyn StdLibFn>,
}
impl StdLibFnRef {
pub(crate) fn new(std_lib_fn: Box<dyn StdLibFn>) -> Self {
Self { std_lib_fn }
}
}
impl From<&Box<dyn StdLibFn>> for StdLibFnRef {
fn from(std_lib_fn: &Box<dyn StdLibFn>) -> Self {
Self::new(std_lib_fn.clone())
}
}
impl PartialEq for StdLibFnRef {
fn eq(&self, other: &Self) -> bool {
self.std_lib_fn.name() == other.std_lib_fn.name()
}
}
impl Eq for StdLibFnRef {}
#[expect(clippy::borrowed_box, reason = "Explicit Box is needed for serde")]
fn std_lib_fn_name<S>(std_lib_fn: &Box<dyn StdLibFn>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let name = std_lib_fn.name();
serializer.serialize_str(&name)
}
fn is_false(b: &bool) -> bool {
!*b
}

File diff suppressed because it is too large Load Diff

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