Merge remote-tracking branch 'origin/main' into paultag/import

This commit is contained in:
Paul Tagliamonte
2025-04-03 10:15:28 -04:00
270 changed files with 12907 additions and 39155 deletions

View File

@ -136,6 +136,36 @@ jobs:
- run: yarn lint - run: yarn lint
yarn-circular-dependencies:
runs-on: ubuntu-latest
needs: yarn-build-wasm
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: yarn circular-deps:diff
python-codespell: python-codespell:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:

View File

@ -4,18 +4,38 @@ all: install build check
############################################################################### ###############################################################################
# INSTALL # INSTALL
WASM_PACK ?= ~/.cargo/bin/wasm-pack ifeq ($(OS),Windows_NT)
CARGO ?= ~/.cargo/bin/cargo.exe
WASM_PACK ?= ~/.cargo/bin/wasm-pack.exe
else
CARGO ?= ~/.cargo/bin/cargo
WASM_PACK ?= ~/.cargo/bin/wasm-pack
endif
.PHONY: install .PHONY: install
install: node_modules/.yarn-integrity $(WASM_PACK) ## Install dependencies install: node_modules/.yarn-integrity $(CARGO) $(WASM_PACK) ## Install dependencies
node_modules/.yarn-integrity: package.json yarn.lock node_modules/.yarn-integrity: package.json yarn.lock
yarn install yarn install
ifeq ($(OS),Windows_NT)
@ type nul > $@
else
@ touch $@ @ touch $@
endif
$(CARGO):
ifeq ($(OS),Windows_NT)
yarn install:rust:windows
else
yarn install:rust
endif
$(WASM_PACK): $(WASM_PACK):
yarn install:rust ifeq ($(OS),Windows_NT)
yarn install:wasm-pack:cargo
else
yarn install:wasm-pack:sh yarn install:wasm-pack:sh
endif
############################################################################### ###############################################################################
# BUILD # BUILD
@ -31,13 +51,17 @@ VITE_SOURCES := $(wildcard vite.*) $(wildcard vite/**/*.tsx)
build: build-web build-desktop build: build-web build-desktop
.PHONY: build-web .PHONY: build-web
build-web: public/kcl_wasm_lib_bg.wasm build/index.html build-web: install public/kcl_wasm_lib_bg.wasm build/index.html
.PHONY: build-desktop .PHONY: build-desktop
build-desktop: public/kcl_wasm_lib_bg.wasm .vite/build/main.js build-desktop: install public/kcl_wasm_lib_bg.wasm .vite/build/main.js
public/kcl_wasm_lib_bg.wasm: $(CARGO_SOURCES)$(RUST_SOURCES) public/kcl_wasm_lib_bg.wasm: $(CARGO_SOURCES) $(RUST_SOURCES)
ifeq ($(OS),Windows_NT)
yarn build:wasm:dev:windows
else
yarn build:wasm:dev yarn build:wasm:dev
endif
build/index.html: $(REACT_SOURCES) $(TYPESCRIPT_SOURCES) $(VITE_SOURCES) build/index.html: $(REACT_SOURCES) $(TYPESCRIPT_SOURCES) $(VITE_SOURCES)
yarn build:local yarn build:local
@ -63,8 +87,10 @@ lint: install ## Lint the code
############################################################################### ###############################################################################
# RUN # RUN
TARGET ?= web
.PHONY: run .PHONY: run
run: run-web run: run-$(TARGET)
.PHONY: run-web .PHONY: run-web
run-web: install build-web ## Start the web app run-web: install build-web ## Start the web app
@ -90,7 +116,7 @@ test-unit: install ## Run the unit tests
yarn test:unit yarn test:unit
.PHONY: test-e2e .PHONY: test-e2e
test-e2e: test-e2e-desktop test-e2e: test-e2e-$(TARGET)
.PHONY: test-e2e-web .PHONY: test-e2e-web
test-e2e-web: install build-web ## Run the web e2e tests test-e2e-web: install build-web ## Run the web e2e tests
@ -106,15 +132,23 @@ test-e2e-desktop: install build-desktop ## Run the desktop e2e tests
.PHONY: clean .PHONY: clean
clean: ## Delete all artifacts clean: ## Delete all artifacts
ifeq ($(OS),Windows_NT)
git clean --force -d -X
else
rm -rf .vite/ build/ rm -rf .vite/ build/
rm -rf trace.zip playwright-report/ test-results/ rm -rf trace.zip playwright-report/ test-results/
rm -rf public/kcl_wasm_lib_bg.wasm rm -rf public/kcl_wasm_lib_bg.wasm
rm -rf rust/*/bindings/ rust/*/pkg/ rust/target/ rm -rf rust/*/bindings/ rust/*/pkg/ rust/target/
rm -rf node_modules/ rust/*/node_modules/ rm -rf node_modules/ rust/*/node_modules/
endif
.PHONY: help .PHONY: help
help: install help: install
ifeq ($(OS),Windows_NT)
@ powershell -Command "Get-Content $(MAKEFILE_LIST) | Select-String -Pattern '^[^\s]+:.*##\s.*$$' | ForEach-Object { $$line = $$_.Line -split ':.*?##\s+'; Write-Host -NoNewline $$line[0].PadRight(30) -ForegroundColor Cyan; Write-Host $$line[1] }"
else
@ grep -E '^[^[:space:]]+:.*## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @ grep -E '^[^[:space:]]+:.*## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
endif
.DEFAULT_GOAL := help .DEFAULT_GOAL := help

View File

@ -54,7 +54,7 @@ example = extrude(exampleSketch, length = 5)
// Add color to a revolved solid. // Add color to a revolved solid.
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [15, 0], radius = 5) |> circle(center = [15, 0], radius = 5)
|> revolve(angle = 360, axis = 'y') |> revolve(angle = 360, axis = Y)
|> appearance(color = '#ff0000', metalness = 90, roughness = 90) |> appearance(color = '#ff0000', metalness = 90, roughness = 90)
``` ```

View File

@ -9,9 +9,12 @@ layout: manual
### `std` ### `std`
- [`X`](/docs/kcl/consts/std-X)
- [`XY`](/docs/kcl/consts/std-XY) - [`XY`](/docs/kcl/consts/std-XY)
- [`XZ`](/docs/kcl/consts/std-XZ) - [`XZ`](/docs/kcl/consts/std-XZ)
- [`Y`](/docs/kcl/consts/std-Y)
- [`YZ`](/docs/kcl/consts/std-YZ) - [`YZ`](/docs/kcl/consts/std-YZ)
- [`Z`](/docs/kcl/consts/std-Z)
### `std::math` ### `std::math`

15
docs/kcl/consts/std-X.md Normal file
View File

@ -0,0 +1,15 @@
---
title: "std::X"
excerpt: ""
layout: manual
---
```js
std::X
```

15
docs/kcl/consts/std-Y.md Normal file
View File

@ -0,0 +1,15 @@
---
title: "std::Y"
excerpt: ""
layout: manual
---
```js
std::Y
```

15
docs/kcl/consts/std-Z.md Normal file
View File

@ -0,0 +1,15 @@
---
title: "std::Z"
excerpt: ""
layout: manual
---
```js
std::Z
```

File diff suppressed because one or more lines are too long

View File

@ -22,6 +22,9 @@ layout: manual
* [`string`](kcl/types/string) * [`string`](kcl/types/string)
* [`tag`](kcl/types/tag) * [`tag`](kcl/types/tag)
* **std** * **std**
* [`Axis2d`](kcl/types/Axis2d)
* [`Axis3d`](kcl/types/Axis3d)
* [`Edge`](kcl/types/Edge)
* [`Face`](kcl/types/Face) * [`Face`](kcl/types/Face)
* [`Helix`](kcl/types/Helix) * [`Helix`](kcl/types/Helix)
* [`Plane`](kcl/types/Plane) * [`Plane`](kcl/types/Plane)
@ -29,9 +32,12 @@ layout: manual
* [`Point3d`](kcl/types/Point3d) * [`Point3d`](kcl/types/Point3d)
* [`Sketch`](kcl/types/Sketch) * [`Sketch`](kcl/types/Sketch)
* [`Solid`](kcl/types/Solid) * [`Solid`](kcl/types/Solid)
* [`X`](kcl/consts/std-X)
* [`XY`](kcl/consts/std-XY) * [`XY`](kcl/consts/std-XY)
* [`XZ`](kcl/consts/std-XZ) * [`XZ`](kcl/consts/std-XZ)
* [`Y`](kcl/consts/std-Y)
* [`YZ`](kcl/consts/std-YZ) * [`YZ`](kcl/consts/std-YZ)
* [`Z`](kcl/consts/std-Z)
* [`abs`](kcl/abs) * [`abs`](kcl/abs)
* [`acos`](kcl/acos) * [`acos`](kcl/acos)
* [`angleToMatchLengthX`](kcl/angleToMatchLengthX) * [`angleToMatchLengthX`](kcl/angleToMatchLengthX)
@ -68,7 +74,7 @@ layout: manual
* [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge) * [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge)
* [`getOppositeEdge`](kcl/getOppositeEdge) * [`getOppositeEdge`](kcl/getOppositeEdge)
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
* [`helix`](kcl/helix) * [`helix`](kcl/std-helix)
* [`hole`](kcl/hole) * [`hole`](kcl/hole)
* [`hollow`](kcl/hollow) * [`hollow`](kcl/hollow)
* [`inch`](kcl/inch) * [`inch`](kcl/inch)
@ -87,7 +93,6 @@ layout: manual
* [`map`](kcl/map) * [`map`](kcl/map)
* [`max`](kcl/max) * [`max`](kcl/max)
* [`min`](kcl/min) * [`min`](kcl/min)
* [`mirror2d`](kcl/mirror2d)
* [`mm`](kcl/mm) * [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane) * [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d) * [`patternCircular2d`](kcl/patternCircular2d)
@ -106,7 +111,7 @@ layout: manual
* [`push`](kcl/push) * [`push`](kcl/push)
* [`reduce`](kcl/reduce) * [`reduce`](kcl/reduce)
* [`rem`](kcl/rem) * [`rem`](kcl/rem)
* [`revolve`](kcl/revolve) * [`revolve`](kcl/std-revolve)
* [`rotate`](kcl/rotate) * [`rotate`](kcl/rotate)
* [`round`](kcl/round) * [`round`](kcl/round)
* [`scale`](kcl/scale) * [`scale`](kcl/scale)
@ -142,6 +147,7 @@ layout: manual
* [`tan`](kcl/std-math-tan) * [`tan`](kcl/std-math-tan)
* **std::sketch** * **std::sketch**
* [`circle`](kcl/std-sketch-circle) * [`circle`](kcl/std-sketch-circle)
* [`mirror2d`](kcl/std-sketch-mirror2d)
* **std::turns** * **std::turns**
* [`turns::HALF_TURN`](kcl/consts/std-turns-HALF_TURN) * [`turns::HALF_TURN`](kcl/consts/std-turns-HALF_TURN)
* [`turns::QUARTER_TURN`](kcl/consts/std-turns-QUARTER_TURN) * [`turns::QUARTER_TURN`](kcl/consts/std-turns-QUARTER_TURN)

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

File diff suppressed because one or more lines are too long

View File

@ -146,7 +146,7 @@ exampleSketch = startSketchOn(XY)
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
example = revolve(exampleSketch, axis = 'y', angle = 180) example = revolve(exampleSketch, axis = Y, angle = 180)
exampleSketch002 = startSketchOn(example, 'end') exampleSketch002 = startSketchOn(example, 'end')
|> startProfileAt([4.5, -5], %) |> startProfileAt([4.5, -5], %)
@ -177,7 +177,7 @@ exampleSketch = startSketchOn(XY)
example = revolve( example = revolve(
exampleSketch, exampleSketch,
axis = 'y', axis = Y,
angle = 180, angle = 180,
tagEnd = $end01, tagEnd = $end01,
) )

116
docs/kcl/std-helix.md Normal file

File diff suppressed because one or more lines are too long

246
docs/kcl/std-revolve.md Normal file

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 it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -74,7 +74,7 @@ helixPath = helix(
revolutions = 4, revolutions = 4,
length = 10, length = 10,
radius = 5, radius = 5,
axis = 'Z', axis = Z,
) )
// Create a spring by sweeping around the helix path. // Create a spring by sweeping around the helix path.

File diff suppressed because one or more lines are too long

12
docs/kcl/types/Axis2d.md Normal file
View File

@ -0,0 +1,12 @@
---
title: "std::Axis2d"
excerpt: "An infinite line in 2d space."
layout: manual
---
An infinite line in 2d space.

12
docs/kcl/types/Axis3d.md Normal file
View File

@ -0,0 +1,12 @@
---
title: "std::Axis3d"
excerpt: "An infinite line in 3d space."
layout: manual
---
An infinite line in 3d space.

12
docs/kcl/types/Edge.md Normal file
View File

@ -0,0 +1,12 @@
---
title: "std::Edge"
excerpt: "The edge of a solid."
layout: manual
---
The edge of a solid.

File diff suppressed because one or more lines are too long

View File

@ -985,12 +985,13 @@ sketch001 = startSketchOn(XZ)
test( test(
'Can undo a sketch modification with ctrl+z', 'Can undo a sketch modification with ctrl+z',
{ tag: ['@skipWin'] }, { tag: ['@skipWin'] },
async ({ page, homePage }) => { async ({ page, homePage, editor }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn(XZ) `@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -10.01], %) |> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
@ -1080,41 +1081,45 @@ sketch001 = startSketchOn(XZ)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')) await editor.expectEditor.toContain(
.toHaveText(`sketch001 = startSketchOn(XZ) `sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([2.71, -2.71], %)
|> line(end = [15.4, -2.78]) |> line(end = [15.4, -2.78])
|> tangentialArcTo([27.6, -3.05], %) |> tangentialArcTo([27.6, -3.05], %)
|> close() |> close()
|> extrude(length = 5) |> extrude(length = 5)`,
`) { shouldNormalise: true }
)
// Hit undo // Hit undo
await page.keyboard.down('Control') await page.keyboard.down('Control')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up('Control') await page.keyboard.up('Control')
await expect(page.locator('.cm-content')) await editor.expectEditor.toContain(
.toHaveText(`sketch001 = startSketchOn(XZ) `sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([2.71, -2.71], %)
|> line(end = [15.4, -2.78]) |> line(end = [15.4, -2.78])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close() |> close()
|> extrude(length = 5)`) |> extrude(length = 5)`,
{ shouldNormalise: true }
)
// Hit undo again. // Hit undo again.
await page.keyboard.down('Control') await page.keyboard.down('Control')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up('Control') await page.keyboard.up('Control')
await expect(page.locator('.cm-content')) await editor.expectEditor.toContain(
.toHaveText(`sketch001 = startSketchOn(XZ) `sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([2.71, -2.71], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close() |> close()
|> extrude(length = 5) |> extrude(length = 5)`,
`) { shouldNormalise: true }
)
// Hit undo again. // Hit undo again.
await page.keyboard.down('Control') await page.keyboard.down('Control')
@ -1122,13 +1127,15 @@ sketch001 = startSketchOn(XZ)
await page.keyboard.up('Control') await page.keyboard.up('Control')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.locator('.cm-content')) await editor.expectEditor.toContain(
.toHaveText(`sketch001 = startSketchOn(XZ) `sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -10.01], %) |> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close() |> close()
|> extrude(length = 5)`) |> extrude(length = 5)`,
{ shouldNormalise: true }
)
} }
) )

View File

@ -22,7 +22,7 @@ sketch001 = startSketchOn(XZ)
|> angledLine([-45, length001], %) |> angledLine([-45, length001], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
revolve001 = revolve(sketch001, axis = "X") revolve001 = revolve(sketch001, axis = X)
triangle() triangle()
|> extrude(length = 30) |> extrude(length = 30)
plane001 = offsetPlane(XY, offset = 10) plane001 = offsetPlane(XY, offset = 10)
@ -127,7 +127,7 @@ test.describe('Feature Tree pane', () => {
await testViewSource({ await testViewSource({
operationName: 'Revolve', operationName: 'Revolve',
operationIndex: 0, operationIndex: 0,
expectedActiveLine: 'revolve001 = revolve(sketch001, axis = "X")', expectedActiveLine: 'revolve001 = revolve(sketch001, axis = X)',
}) })
await testViewSource({ await testViewSource({
operationName: 'Triangle', operationName: 'Triangle',

View File

@ -10,6 +10,9 @@ import {
openPane, openPane,
} from '@e2e/playwright/test-utils' } from '@e2e/playwright/test-utils'
import { expect } from '@e2e/playwright/zoo-test' import { expect } from '@e2e/playwright/zoo-test'
import { type baseUnitLabels } from '@src/lib/settings/settingsTypes'
type LengthUnitLabel = (typeof baseUnitLabels)[keyof typeof baseUnitLabels]
export class ToolbarFixture { export class ToolbarFixture {
public page: Page public page: Page
@ -236,6 +239,12 @@ export class ToolbarFixture {
async checkIfFeatureTreePaneIsOpen() { async checkIfFeatureTreePaneIsOpen() {
return this.checkIfPaneIsOpen(this.featureTreeId) return this.checkIfPaneIsOpen(this.featureTreeId)
} }
async selectUnit(unit: LengthUnitLabel) {
await this.page.getByTestId('units-menu').click()
const optionLocator = this.page.getByRole('button', { name: unit })
await expect(optionLocator).toBeVisible()
await optionLocator.click()
}
/** /**
* Get a specific operation button from the Feature Tree pane. * Get a specific operation button from the Feature Tree pane.

View File

@ -1084,8 +1084,8 @@ openSketch = startSketchOn(XY)
}) => { }) => {
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 620, y: 257 } const testPoint = { x: 620, y: 257 }
const expectedOutput = `helix001 = helix( axis = 'X', radius = 5, length = 5, revolutions = 1, angleStart = 360, ccw = false,)` const expectedOutput = `helix001 = helix( axis = X, radius = 5, length = 5, revolutions = 1, angleStart = 360, ccw = false,)`
const expectedLine = `axis='X',` const expectedLine = `axis=X,`
await homePage.goToModelingScene() await homePage.goToModelingScene()
@ -1217,7 +1217,7 @@ openSketch = startSketchOn(XY)
cmdBar, cmdBar,
}) => { }) => {
page.on('console', console.log) page.on('console', console.log)
const initialCode = `sketch001 = startSketchOn('XZ') const initialCode = `sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([0, 0], sketch001) profile001 = startProfileAt([0, 0], sketch001)
|> yLine(length = 100) |> yLine(length = 100)
|> line(endAbsolute = [100, 0]) |> line(endAbsolute = [100, 0])
@ -3474,7 +3474,7 @@ segAng(rectangleSegmentA002),
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = 'X')` const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy() expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
// Edit flow // Edit flow

View File

@ -798,6 +798,74 @@ plane002 = offsetPlane(XZ, offset = -2 * x)`
await page.getByTestId('custom-cmd-send-button').click() await page.getByTestId('custom-cmd-send-button').click()
} }
) )
test('scale other than default works with sketch mode', async ({
page,
homePage,
toolbar,
editor,
scene,
}) => {
await test.step('Load the washer code', async () => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
innerDiameter = 0.203
outerDiameter = 0.438
thicknessMax = 0.038
thicknessMin = 0.024
washerSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = outerDiameter / 2)
washer = extrude(washerSketch, length = thicknessMax)`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
})
const [circleCenterClick] = scene.makeMouseHelpers(650, 300)
const [circleRadiusClick] = scene.makeMouseHelpers(800, 320)
const [washerFaceClick] = scene.makeMouseHelpers(657, 286)
await page.waitForTimeout(100)
await test.step('Start sketching on the washer face', async () => {
await toolbar.startSketchPlaneSelection()
await washerFaceClick()
await page.waitForTimeout(600) // engine animation
await toolbar.expectToolbarMode.toBe('sketching')
})
await test.step('Draw a circle and verify code', async () => {
// select circle tool
await expect
.poll(async () => {
await toolbar.circleBtn.click()
return toolbar.circleBtn.getAttribute('aria-pressed')
})
.toBe('true')
await page.waitForTimeout(100)
await circleCenterClick()
// this number will be different if the scale is not set correctly for inches
await editor.expectEditor.toContain(
'circle(sketch001, center = [0.06, -0.06]'
)
await circleRadiusClick()
await editor.expectEditor.toContain(
'circle(sketch001, center = [0.06, -0.06], radius = 0.18'
)
})
await test.step('Exit sketch mode', async () => {
await toolbar.exitSketch()
await toolbar.expectToolbarMode.toBe('modeling')
await toolbar.selectUnit('Yards')
await editor.expectEditor.toContain('@settings(defaultLengthUnit = yd)')
})
})
}) })
async function clickExportButton(page: Page) { async function clickExportButton(page: Page) {

View File

@ -479,7 +479,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn(XZ) `@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> circle(center = [4.61, -5.01], radius = 8)` |> circle(center = [4.61, -5.01], radius = 8)`
) )
}) })
@ -564,12 +565,14 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
test('Can edit a sketch that has been extruded in the same pipe', async ({ test('Can edit a sketch that has been extruded in the same pipe', async ({
page, page,
homePage, homePage,
editor,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn(XZ) `@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -10.01], %) |> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
@ -654,31 +657,34 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')) await editor.expectEditor.toContain(
.toHaveText(`sketch001 = startSketchOn(XZ) `sketch001 = startSketchOn(XZ)
|> startProfileAt([7.12, -12.68], %) |> startProfileAt([7.12, -12.68], %)
|> line(end = [12.68, -1.09]) |> line(end = [12.68, -1.09])
|> tangentialArcTo([24.89, 0.68], %) |> tangentialArcTo([24.89, 0.68], %)
|> close() |> close()
|> extrude(length = 5) |> extrude(length = 5)`,
`) { shouldNormalise: true }
)
}) })
test('Can edit a sketch that has been revolved in the same pipe', async ({ test('Can edit a sketch that has been revolved in the same pipe', async ({
page, page,
homePage, homePage,
scene, scene,
editor,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn(XZ) `@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -14.01], %) |> startProfileAt([4.61, -14.01], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -5.38], %) |> tangentialArcTo([24.95, -5.38], %)
|> close() |> close()
|> revolve(axis = "X")` |> revolve(axis = X)`
) )
}) })
@ -758,14 +764,16 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')) await editor.expectEditor.toContain(
.toHaveText(`sketch001 = startSketchOn(XZ) `sketch001 = startSketchOn(XZ)
|> startProfileAt([6.44, -12.07], %) |> startProfileAt([6.44, -12.07], %)
|> line(end = [14.72, 1.97]) |> line(end = [14.72, 1.97])
|> tangentialArcTo([24.95, -5.38], %) |> tangentialArcTo([24.95, -5.38], %)
|> line(end = [1.97, 2.06]) |> line(end = [1.97, 2.06])
|> close() |> close()
|> revolve(axis = "X")`) |> revolve(axis = X)`,
{ shouldNormalise: true }
)
}) })
test('Can add multiple sketches', async ({ page, homePage }) => { test('Can add multiple sketches', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -1215,7 +1223,7 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
|> xLine(endAbsolute = 0 + .001) |> xLine(endAbsolute = 0 + .001)
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()
|> revolve(axis = "Y") |> revolve(axis = Y)
return lugSketch return lugSketch
} }

View File

@ -455,7 +455,7 @@ test(
await page.waitForTimeout(700) // TODO detect animation ending, or disable animation await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` code += `profile001 = startProfileAt([182.59, -246.32], sketch001)`
await expect(page.locator('.cm-content')).toHaveText(code) await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -473,7 +473,7 @@ test(
await page.waitForTimeout(500) await page.waitForTimeout(500)
code += ` code += `
|> xLine(length = 7.25)` |> xLine(length = 184.3)`
await expect(page.locator('.cm-content')).toHaveText(code) await expect(page.locator('.cm-content')).toHaveText(code)
await page await page
@ -631,7 +631,7 @@ test(
mask: [page.getByTestId('model-state-indicator')], mask: [page.getByTestId('model-state-indicator')],
}) })
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)profile001 = circle(sketch001, center = [14.44, -2.44], radius = 1)` `sketch001 = startSketchOn(XZ)profile001 = circle(sketch001, center = [366.89, -62.01], radius = 1)`
) )
} }
) )
@ -668,7 +668,7 @@ test.describe(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` code += `profile001 = startProfileAt([182.59, -246.32], sketch001)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -676,7 +676,7 @@ test.describe(
await page.waitForTimeout(100) await page.waitForTimeout(100)
code += ` code += `
|> xLine(length = 7.25)` |> xLine(length = 184.3)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page await page
@ -691,7 +691,7 @@ test.describe(
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += ` code += `
|> tangentialArcTo([21.7, -2.44], %)` |> tangentialArcTo([551.2, -62.01], %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
// click tangential arc tool again to unequip it // click tangential arc tool again to unequip it

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 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: 48 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 124 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: 72 KiB

After

Width:  |  Height:  |  Size: 71 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: 71 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -29,5 +29,5 @@
} }
} }
], ],
"kcl_version": "0.2.54" "kcl_version": "0.2.57"
} }

View File

@ -324,7 +324,7 @@ part009 = startSketchOn(XY)
|> line(end = [0, pipeLength]) |> line(end = [0, pipeLength])
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %) |> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|> close() |> close()
rev = revolve(part009, axis = 'y') rev = revolve(part009, axis = Y)
sketch006 = startSketchOn(XY) sketch006 = startSketchOn(XY)
profile001 = circle( profile001 = circle(
sketch006, sketch006,
@ -381,7 +381,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText( await expect(u.codeLocator).not.toContainText(
`rev = revolve(part009, axis: 'y')` `rev = revolve(part009, axis: Y)`
) )
// FIXME (commented section below), this test would select a wall that had a sketch on it, and delete the underlying extrude // FIXME (commented section below), this test would select a wall that had a sketch on it, and delete the underlying extrude

13
known-circular.txt Normal file
View File

@ -0,0 +1,13 @@
$ dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx
• Circular Dependencies
01) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/modifyAst/addEdgeTreatment.ts
02) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
03) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
04) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
05) src/lib/singletons.ts -> src/lang/KclSingleton.ts
06) src/lib/singletons.ts -> src/lang/codeManager.ts
07) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
08) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/machines/commandBarMachine.ts -> src/lib/commandBarConfigs/authCommandConfig.ts -> src/machines/appMachine.ts -> src/machines/settingsMachine.ts
09) src/machines/commandBarMachine.ts -> src/lib/commandBarConfigs/authCommandConfig.ts -> src/machines/appMachine.ts -> src/machines/settingsMachine.ts
10) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts
11) src/routes/Onboarding/index.tsx -> src/routes/Onboarding/Camera.tsx -> src/routes/Onboarding/utils.tsx

View File

@ -92,21 +92,23 @@
"fmt:generated": "prettier --write .eslintrc.json *.ts *.json *.js ./rust/kcl-lib/bindings ./rust/kcl-wasm-lib/pkg", "fmt:generated": "prettier --write .eslintrc.json *.ts *.json *.js ./rust/kcl-lib/bindings ./rust/kcl-wasm-lib/pkg",
"fmt-check": "prettier --check .eslintrc.json ./src *.ts *.json *.js ./e2e ./packages ./rust/kcl-language-server", "fmt-check": "prettier --check .eslintrc.json ./src *.ts *.json *.js ./e2e ./packages ./rust/kcl-language-server",
"fetch:wasm": "./scripts/get-latest-wasm-bundle.sh", "fetch:wasm": "./scripts/get-latest-wasm-bundle.sh",
"fetch:wasm:windows": "./scripts/get-latest-wasm-bundle.ps1", "fetch:wasm:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/get-latest-wasm-bundle.ps1",
"fetch:samples": "rm -rf public/kcl-samples* && curl -L -o public/kcl-samples.zip https://github.com/KittyCAD/kcl-samples/archive/refs/heads/achalmers/kw-args-xylineto.zip && unzip -o public/kcl-samples.zip -d public && mv public/kcl-samples-* public/kcl-samples", "fetch:samples": "rm -rf public/kcl-samples* && curl -L -o public/kcl-samples.zip https://github.com/KittyCAD/kcl-samples/archive/refs/heads/achalmers/kw-args-xylineto.zip && unzip -o public/kcl-samples.zip -d public && mv public/kcl-samples-* public/kcl-samples",
"build:wasm": "./scripts/build-wasm.sh", "build:wasm": "./scripts/build-wasm.sh",
"build:wasm:windows": "./scripts/build-wasm.ps1", "build:wasm:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/build-wasm.ps1",
"build:wasm-dev": "yarn build:wasm:dev", "build:wasm-dev": "yarn build:wasm:dev",
"build:wasm:dev": "./scripts/build-wasm-dev.sh", "build:wasm:dev": "./scripts/build-wasm-dev.sh",
"build:wasm:dev:windows": "./scripts/build-wasm-dev.ps1", "build:wasm:dev:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/build-wasm-dev.ps1",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\" || echo \"sed for both mac and linux\"",
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src", "lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src",
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src", "lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src",
"circular-deps": "dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx", "circular-deps": "dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx",
"circular-deps:overwrite": "yarn circular-deps | sed '$d' | grep -v '^yarn run' > known-circular.txt",
"circular-deps:diff": "./scripts/diff-circular-deps.sh",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh", "files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh", "files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
"files:flip-to-nightly:windows": "./scripts/flip-files-to-nightly.ps1", "files:flip-to-nightly:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/flip-files-to-nightly.ps1",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh", "files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly", "files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"postinstall": "yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install && ./node_modules/.bin/electron-rebuild", "postinstall": "yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install && ./node_modules/.bin/electron-rebuild",
@ -231,7 +233,7 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.26.1",
"vite": "^5.4.12", "vite": "^5.4.16",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-top-level-await": "^1.5.0", "vite-plugin-top-level-await": "^1.5.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

@ -683,9 +683,9 @@ vite-tsconfig-paths@^4.3.2:
tsconfck "^3.0.3" tsconfck "^3.0.3"
vite@^5.0.0: vite@^5.0.0:
version "5.4.14" version "5.4.16"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.16.tgz#471983257a890ef33f2700cbbbc2134f2d08abf1"
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA== integrity sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==
dependencies: dependencies:
esbuild "^0.21.3" esbuild "^0.21.3"
postcss "^8.4.43" postcss "^8.4.43"

View File

@ -35,7 +35,7 @@ ballsSketch = startSketchOn(XY)
|> close() |> close()
// Revolve the ball to make a sphere and pattern around the inside wall // Revolve the ball to make a sphere and pattern around the inside wall
balls = revolve(ballsSketch, axis = "X") balls = revolve(ballsSketch, axis = X)
|> patternCircular3d( |> patternCircular3d(
arcDegrees = 360, arcDegrees = 360,
axis = [0, 0, 1], axis = [0, 0, 1],
@ -60,7 +60,7 @@ chainSketch = startSketchOn(XY)
|> close() |> close()
// Revolve the chain sketch // Revolve the chain sketch
chainHead = revolve(chainSketch, axis = "X") chainHead = revolve(chainSketch, axis = X)
|> patternCircular3d( |> patternCircular3d(
arcDegrees = 360, arcDegrees = 360,
axis = [0, 0, 1], axis = [0, 0, 1],
@ -80,7 +80,7 @@ linkSketch = startSketchOn(XZ)
) )
// Revolve the link sketch // Revolve the link sketch
linkRevolve = revolve(linkSketch, axis = 'Y', angle = 360 / nBalls) linkRevolve = revolve(linkSketch, axis = Y, angle = 360 / nBalls)
|> patternCircular3d( |> patternCircular3d(
arcDegrees = 360, arcDegrees = 360,
axis = [0, 0, 1], axis = [0, 0, 1],

View File

@ -80,5 +80,5 @@ brakeCaliperSketch = startSketchOn(XY)
|> close() |> close()
// Revolve the brake caliper sketch // Revolve the brake caliper sketch
revolve(brakeCaliperSketch, axis = "Y", angle = -70) revolve(brakeCaliperSketch, axis = Y, angle = -70)
|> appearance(color = "#c82d2d", metalness = 90, roughness = 90) |> appearance(color = "#c82d2d", metalness = 90, roughness = 90)

View File

@ -41,5 +41,5 @@ tireSketch = startSketchOn(XY)
|> close() |> close()
// Revolve the sketch to create the tire // Revolve the sketch to create the tire
revolve(tireSketch, axis = "Y") revolve(tireSketch, axis = Y)
|> appearance(color = "#0f0f0f", roughness = 80) |> appearance(color = "#0f0f0f", roughness = 80)

View File

@ -54,7 +54,7 @@ wheelCenterInner = startSketchOn(XY)
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
|> revolve(axis = 'y') |> revolve(axis = Y)
|> appearance(color = "#ffffff", metalness = 0, roughness = 0) |> appearance(color = "#ffffff", metalness = 0, roughness = 0)
wheelCenterOuter = startSketchOn(XY) wheelCenterOuter = startSketchOn(XY)
@ -68,7 +68,7 @@ wheelCenterOuter = startSketchOn(XY)
|> yLine(endAbsolute = -wheelWidth / 20) |> yLine(endAbsolute = -wheelWidth / 20)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
|> revolve(axis = 'y') |> revolve(axis = Y)
|> appearance(color = "#ffffff", metalness = 0, roughness = 0) |> appearance(color = "#ffffff", metalness = 0, roughness = 0)
// Write a function that defines the spoke geometry, patterns and extrudes it // Write a function that defines the spoke geometry, patterns and extrudes it
@ -173,5 +173,5 @@ startSketchOn(XY)
|> xLine(length = wheelWidth * 0.03) |> xLine(length = wheelWidth * 0.03)
|> yLine(length = wheelWidth * 0.05) |> yLine(length = wheelWidth * 0.05)
|> close() |> close()
|> revolve(axis = 'y') |> revolve(axis = Y)
|> appearance(color = "#ffffff", metalness = 0, roughness = 0) |> appearance(color = "#ffffff", metalness = 0, roughness = 0)

View File

@ -32,7 +32,7 @@ fn lug(plane, length, diameter) {
|> xLine(endAbsolute = lugThreadDiameter) |> xLine(endAbsolute = lugThreadDiameter)
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()
|> revolve(axis = "Y") |> revolve(axis = Y)
|> appearance(color = "#dbcd70", roughness = 90, metalness = 90) |> appearance(color = "#dbcd70", roughness = 90, metalness = 90)
return lugSketch return lugSketch
} }

View File

@ -23,7 +23,7 @@ sketch001 = startSketchOn(XZ)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
|> revolve(angle = 360, axis = 'Y') |> revolve(angle = 360, axis = Y)
// Create an angled plane to sketch the supports // Create an angled plane to sketch the supports
plane001 = { plane001 = {
@ -132,7 +132,7 @@ sketch005 = startSketchOn(XZ)
|> xLine(endAbsolute = 0.15) |> xLine(endAbsolute = 0.15)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
|> revolve(axis = 'y') |> revolve(axis = Y)
// Plunger and stem // Plunger and stem
sketch006 = startSketchOn(XZ) sketch006 = startSketchOn(XZ)
@ -145,7 +145,7 @@ sketch006 = startSketchOn(XZ)
|> tangentialArc({ radius = 0.6, offset = -90 }, %) |> tangentialArc({ radius = 0.6, offset = -90 }, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
|> revolve(axis = 'y') |> revolve(axis = Y)
// Spiral plate // Spiral plate
sketch007 = startSketchOn(offsetPlane(XY, offset = 1.12)) sketch007 = startSketchOn(offsetPlane(XY, offset = 1.12))
@ -201,7 +201,7 @@ sketch011 = startSketchOn(XZ)
}, %) }, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
|> revolve(axis = 'y') |> revolve(axis = Y)
// Draw and extrude handle // Draw and extrude handle
sketch012 = startSketchOn(offsetPlane(XZ, offset = handleThickness / 2)) sketch012 = startSketchOn(offsetPlane(XZ, offset = handleThickness / 2))

View File

@ -48,10 +48,8 @@ sides = patternCircular3d(
// define an axis axis000 // define an axis axis000
axis000 = { axis000 = {
custom = { direction = [0.0, 1.0],
axis = [0.0, 1.0],
origin = [cornerRadius, cornerRadius] origin = [cornerRadius, cornerRadius]
}
} }
// create a single corner of the bin // create a single corner of the bin

View File

@ -45,10 +45,8 @@ sides = patternCircular3d(
// define an axis axis000 // define an axis axis000
axis000 = { axis000 = {
custom = { direction = [0.0, 1.0],
axis = [0.0, 1.0],
origin = [cornerRadius, cornerRadius] origin = [cornerRadius, cornerRadius]
}
} }
// create a single corner of the bin // create a single corner of the bin

View File

@ -65,13 +65,11 @@ sides = patternCircular3d(
// define an axis axis000 // define an axis axis000
axis000 = { axis000 = {
custom = { direction = [0.0, 1.0],
axis = [0.0, 1.0],
origin = [ origin = [
cornerRadius + binTol, cornerRadius + binTol,
cornerRadius + binTol cornerRadius + binTol
] ]
}
} }
// create a single corner of the bin // create a single corner of the bin
@ -272,10 +270,8 @@ lipWidths = patternCircular3d(
// define an axis axis000 // define an axis axis000
axis001 = { axis001 = {
custom = { direction = [0.0, 1.0],
axis = [0.0, 1.0],
origin = [cornerRadius, cornerRadius] origin = [cornerRadius, cornerRadius]
}
} }
// create a single corner of the bin // create a single corner of the bin

View File

@ -58,13 +58,11 @@ sides = patternCircular3d(
// define an axis axis000 // define an axis axis000
axis000 = { axis000 = {
custom = { direction = [0.0, 1.0],
axis = [0.0, 1.0],
origin = [ origin = [
cornerRadius + binTol, cornerRadius + binTol,
cornerRadius + binTol cornerRadius + binTol
] ]
}
} }
// create a single corner of the bin // create a single corner of the bin

View File

@ -20,6 +20,6 @@ sketch001 = startSketchOn(-XZ)
|> xLine(endAbsolute = webThickness / 2 + rootRadius) |> xLine(endAbsolute = webThickness / 2 + rootRadius)
|> tangentialArc({ radius = rootRadius, offset = 90 }, %) |> tangentialArc({ radius = rootRadius, offset = 90 }, %)
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> mirror2d({ axis = 'X' }, %) |> mirror2d(axis = X)
|> mirror2d({ axis = 'Y' }, %) |> mirror2d(axis = Y)
|> extrude(length = beamLength) |> extrude(length = beamLength)

View File

@ -24,4 +24,4 @@ pipeProfile = outerProfile
|> hole(innerProfile, %) |> hole(innerProfile, %)
// revolve the pipe profile at the desired angle // revolve the pipe profile at the desired angle
pipe = revolve(pipeProfile, axis = 'Y', angle = bendAngle) pipe = revolve(pipeProfile, axis = Y, angle = bendAngle)

View File

@ -33,4 +33,4 @@ pipeSketch = startSketchOn(XY)
|> close() |> close()
// Revolve the sketch to create the pipe // Revolve the sketch to create the pipe
pipe = revolve(pipeSketch, axis = 'y') pipe = revolve(pipeSketch, axis = Y)

View File

@ -34,10 +34,8 @@ part001 = revolve(
sketch001, sketch001,
angle = 90, angle = 90,
axis = { axis = {
custom = { direction = [1.0, 0.0],
axis = [1.0, 0.0],
origin = [0.0, height + .0001] origin = [0.0, height + .0001]
}
}, },
) )

View File

@ -21,7 +21,7 @@ export fn knob() {
}, %) }, %)
|> xLine(endAbsolute = 0.0001) |> xLine(endAbsolute = 0.0001)
|> close() |> close()
|> revolve(axis = "Y") |> revolve(axis = Y)
|> appearance(color = '#D0FF01', metalness = 90, roughness = 50) |> appearance(color = '#D0FF01', metalness = 90, roughness = 50)
return knob return knob

20
rust/Cargo.lock generated
View File

@ -1780,7 +1780,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.57" version = "0.1.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1791,7 +1791,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-derive-docs" name = "kcl-derive-docs"
version = "0.1.57" version = "0.1.58"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -1810,7 +1810,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
version = "0.1.57" version = "0.1.58"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1819,7 +1819,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server" name = "kcl-language-server"
version = "0.2.57" version = "0.2.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1840,7 +1840,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.57" version = "0.1.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1860,7 +1860,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.57" version = "0.2.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1929,7 +1929,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-python-bindings" name = "kcl-python-bindings"
version = "0.3.57" version = "0.3.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"kcl-lib", "kcl-lib",
@ -1944,7 +1944,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.57" version = "0.1.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper 0.14.32", "hyper 0.14.32",
@ -1957,7 +1957,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-to-core" name = "kcl-to-core"
version = "0.1.57" version = "0.1.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1971,7 +1971,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-wasm-lib" name = "kcl-wasm-lib"
version = "0.1.57" version = "0.1.58"
dependencies = [ dependencies = [
"bson", "bson",
"console_error_panic_hook", "console_error_panic_hook",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -881,7 +881,7 @@ async fn kcl_test_simple_revolve() {
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = 'y') |> revolve(axis = Y)
"#; "#;
@ -901,7 +901,7 @@ async fn kcl_test_simple_revolve_uppercase() {
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = 'Y') |> revolve(axis = Y)
"#; "#;
@ -921,7 +921,7 @@ async fn kcl_test_simple_revolve_negative() {
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = '-Y', angle = 180) |> revolve(axis = -Y, angle = 180)
"#; "#;
@ -941,7 +941,7 @@ async fn kcl_test_revolve_bad_angle_low() {
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = 'y', angle = -455) |> revolve(axis = Y, angle = -455)
"#; "#;
@ -967,7 +967,7 @@ async fn kcl_test_revolve_bad_angle_high() {
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = 'y', angle = 455) |> revolve(axis = Y, angle = 455)
"#; "#;
@ -993,7 +993,7 @@ async fn kcl_test_simple_revolve_custom_angle() {
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = 'y', angle = 180) |> revolve(axis = Y, angle = 180)
"#; "#;
@ -1013,7 +1013,7 @@ async fn kcl_test_simple_revolve_custom_axis() {
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = {custom: {axis: [0, -1], origin: [0,0]}}, angle = 180) |> revolve(axis = { direction = [0, -1], origin: [0,0] }, angle = 180)
"#; "#;
@ -1111,7 +1111,7 @@ sketch001 = startSketchOn(box, "END")
|> circle(center = [10,10], radius= 4 ) |> circle(center = [10,10], radius= 4 )
|> revolve( |> revolve(
angle = -90, angle = -90,
axis = 'y' axis = Y
) )
"#; "#;
@ -1136,7 +1136,7 @@ sketch001 = startSketchOn(box, "end")
|> line(end = [0, 10]) |> line(end = [0, 10])
|> close() |> close()
|> revolve( |> revolve(
axis = 'y', axis = Y,
angle = -90, angle = -90,
) )
"#; "#;
@ -1151,7 +1151,7 @@ async fn kcl_test_basic_revolve_circle() {
|> circle(center = [15, 0], radius= 5) |> circle(center = [15, 0], radius= 5)
|> revolve( |> revolve(
angle = 360, angle = 360,
axis = 'y' axis = Y
) )
"#; "#;
@ -1171,7 +1171,7 @@ async fn kcl_test_simple_revolve_sketch_on_edge() {
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = 'y', angle = 180) |> revolve(axis = Y, angle = 180)
part002 = startSketchOn(part001, 'end') part002 = startSketchOn(part001, 'end')
|> startProfileAt([4.5, -5], %) |> startProfileAt([4.5, -5], %)

View File

@ -24,8 +24,9 @@ use crate::{
const TYPES_DIR: &str = "../../docs/kcl/types"; const TYPES_DIR: &str = "../../docs/kcl/types";
const LANG_TOPICS: [&str; 5] = ["Types", "Modules", "Settings", "Known Issues", "Constants"]; const LANG_TOPICS: [&str; 5] = ["Types", "Modules", "Settings", "Known Issues", "Constants"];
// These types are declared in std. // These types are declared in std.
const DECLARED_TYPES: [&str; 11] = [ const DECLARED_TYPES: [&str; 14] = [
"number", "string", "tag", "bool", "Sketch", "Solid", "Plane", "Helix", "Face", "Point2d", "Point3d", "number", "string", "tag", "bool", "Sketch", "Solid", "Plane", "Helix", "Face", "Edge", "Point2d", "Point3d",
"Axis2d", "Axis3d",
]; ];
fn init_handlebars() -> Result<handlebars::Handlebars<'static>> { fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {

View File

@ -508,6 +508,7 @@ pub struct ArgData {
pub ty: Option<String>, pub ty: Option<String>,
/// If the argument is required. /// If the argument is required.
pub kind: ArgKind, pub kind: ArgKind,
pub override_in_snippet: Option<bool>,
/// Additional information that could be used instead of the type's description. /// Additional information that could be used instead of the type's description.
/// This is helpful if the type is really basic, like "number" -- that won't tell the user much about /// This is helpful if the type is really basic, like "number" -- that won't tell the user much about
/// how this argument is meant to be used. /// how this argument is meant to be used.
@ -528,6 +529,7 @@ impl ArgData {
name: arg.identifier.name.clone(), name: arg.identifier.name.clone(),
ty: arg.type_.as_ref().map(|t| t.to_string()), ty: arg.type_.as_ref().map(|t| t.to_string()),
docs: None, docs: None,
override_in_snippet: None,
kind: if arg.labeled { kind: if arg.labeled {
ArgKind::Labelled(arg.optional()) ArgKind::Labelled(arg.optional())
} else { } else {
@ -535,26 +537,51 @@ impl ArgData {
}, },
}; };
for attr in &arg.identifier.outer_attrs {
if let Annotation {
name: None,
properties: Some(props),
..
} = &attr.inner
{
for p in props {
if p.key.name == "include_in_snippet" {
if let Some(b) = p.value.literal_bool() {
result.override_in_snippet = Some(b);
} else {
panic!(
"Invalid value for `include_in_snippet`, expected bool literal, found {:?}",
p.value
);
}
}
}
}
}
result.with_comments(&arg.identifier.pre_comments); result.with_comments(&arg.identifier.pre_comments);
result result
} }
pub fn get_autocomplete_snippet(&self, index: usize) -> Option<(usize, String)> { pub fn get_autocomplete_snippet(&self, index: usize) -> Option<(usize, String)> {
match self.override_in_snippet {
Some(false) => return None,
None if !self.kind.required() => return None,
_ => {}
}
let label = if self.kind == ArgKind::Special { let label = if self.kind == ArgKind::Special {
String::new() String::new()
} else { } else {
format!("{} = ", self.name) format!("{} = ", self.name)
}; };
match self.ty.as_deref() { match self.ty.as_deref() {
Some(s) if ["Sketch", "Solid", "Plane | Face", "Sketch | Plane | Face"].contains(&s) => { Some(s) if s.starts_with("number") => Some((index, format!(r#"{label}${{{}:3.14}}"#, index))),
Some((index, format!("{label}${{{}:{}}}", index, "%"))) Some("Point2d") => Some((
}
Some("number") if self.kind.required() => Some((index, format!(r#"{label}${{{}:3.14}}"#, index))),
Some("Point2d") if self.kind.required() => Some((
index + 1, index + 1,
format!(r#"{label}[${{{}:3.14}}, ${{{}:3.14}}]"#, index, index + 1), format!(r#"{label}[${{{}:3.14}}, ${{{}:3.14}}]"#, index, index + 1),
)), )),
Some("Point3d") if self.kind.required() => Some(( Some("Point3d") => Some((
index + 2, index + 2,
format!( format!(
r#"{label}[${{{}:3.14}}, ${{{}:3.14}}, ${{{}:3.14}}]"#, r#"{label}[${{{}:3.14}}, ${{{}:3.14}}, ${{{}:3.14}}]"#,
@ -563,8 +590,10 @@ impl ArgData {
index + 2 index + 2
), ),
)), )),
Some("string") if self.kind.required() => Some((index, format!(r#"{label}${{{}:"string"}}"#, index))), Some("Axis2d | Edge") | Some("Axis3d | Edge") => Some((index, format!(r#"{label}${{{}:X}}"#, index))),
Some("bool") if self.kind.required() => Some((index, format!(r#"{label}${{{}:false}}"#, index))),
Some("string") => Some((index, format!(r#"{label}${{{}:"string"}}"#, index))),
Some("bool") => Some((index, format!(r#"{label}${{{}:false}}"#, index))),
_ => None, _ => None,
} }
} }

View File

@ -937,9 +937,12 @@ mod tests {
#[test] #[test]
fn get_autocomplete_snippet_revolve() { fn get_autocomplete_snippet_revolve() {
let revolve_fn: Box<dyn StdLibFn> = Box::new(crate::std::revolve::Revolve); let data = kcl_doc::walk_prelude();
let snippet = revolve_fn.to_autocomplete_snippet().unwrap(); let DocData::Fn(revolve_fn) = data.into_iter().find(|d| d.name() == "revolve").unwrap() else {
assert_eq!(snippet, r#"revolve(${0:%}, axis = ${1:"X"})${}"#); panic!();
};
let snippet = revolve_fn.to_autocomplete_snippet();
assert_eq!(snippet, r#"revolve(axis = ${0:X})${}"#);
} }
#[test] #[test]
@ -952,7 +955,7 @@ mod tests {
let snippet = circle_fn.to_autocomplete_snippet(); let snippet = circle_fn.to_autocomplete_snippet();
assert_eq!( assert_eq!(
snippet, snippet,
r#"circle(${0:%}, center = [${1:3.14}, ${2:3.14}], radius = ${3:3.14})${}"# r#"circle(center = [${0:3.14}, ${1:3.14}], radius = ${2:3.14})${}"#
); );
} }
@ -1026,11 +1029,14 @@ mod tests {
#[test] #[test]
#[allow(clippy::literal_string_with_formatting_args)] #[allow(clippy::literal_string_with_formatting_args)]
fn get_autocomplete_snippet_helix() { fn get_autocomplete_snippet_helix() {
let helix_fn: Box<dyn StdLibFn> = Box::new(crate::std::helix::Helix); let data = kcl_doc::walk_prelude();
let snippet = helix_fn.to_autocomplete_snippet().unwrap(); let DocData::Fn(helix_fn) = data.into_iter().find(|d| d.name() == "helix").unwrap() else {
panic!();
};
let snippet = helix_fn.to_autocomplete_snippet();
assert_eq!( assert_eq!(
snippet, snippet,
r#"helix(revolutions = ${0:3.14}, angleStart = ${1:3.14}, radius = ${2:3.14}, axis = ${3:"X"}, length = ${4:3.14})${}"# r#"helix(revolutions = ${0:3.14}, angleStart = ${1:3.14}, radius = ${2:3.14}, axis = ${3:X}, length = ${4:3.14})${}"#
); );
} }

View File

@ -27,6 +27,19 @@ pub enum Operation {
is_error: bool, is_error: bool,
}, },
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
KclStdLibCall {
name: String,
/// 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")]
UserDefinedFunctionCall { UserDefinedFunctionCall {
/// The name of the user-defined function being called. Anonymous /// The name of the user-defined function being called. Anonymous
/// functions have no name. /// functions have no name.
@ -49,6 +62,7 @@ impl Operation {
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) { pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self { match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err, Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::KclStdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::UserDefinedFunctionCall { .. } | Self::UserDefinedFunctionReturn => {} Self::UserDefinedFunctionCall { .. } | Self::UserDefinedFunctionReturn => {}
} }
} }

View File

@ -3,7 +3,7 @@ use std::collections::HashMap;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use indexmap::IndexMap; use indexmap::IndexMap;
use super::kcl_value::TypeDef; use super::{kcl_value::TypeDef, types::PrimitiveType};
use crate::{ use crate::{
engine::ExecutionKind, engine::ExecutionKind,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
@ -954,6 +954,33 @@ impl Node<BinaryExpression> {
} }
} }
// Then check if we have solids.
if self.operator == BinaryOperator::Add || self.operator == BinaryOperator::Or {
if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
let args = crate::std::Args::new(Default::default(), self.into(), ctx.clone(), None);
let result =
crate::std::csg::inner_union(vec![*left.clone(), *right.clone()], exec_state, args).await?;
return Ok(result.into());
}
} else if self.operator == BinaryOperator::Sub {
// Check if we have solids.
if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
let args = crate::std::Args::new(Default::default(), self.into(), ctx.clone(), None);
let result =
crate::std::csg::inner_subtract(vec![*left.clone()], vec![*right.clone()], exec_state, args)
.await?;
return Ok(result.into());
}
} else if self.operator == BinaryOperator::And {
// Check if we have solids.
if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
let args = crate::std::Args::new(Default::default(), self.into(), ctx.clone(), None);
let result =
crate::std::csg::inner_intersect(vec![*left.clone(), *right.clone()], exec_state, args).await?;
return Ok(result.into());
}
}
// Check if we are doing logical operations on booleans. // Check if we are doing logical operations on booleans.
if self.operator == BinaryOperator::Or || self.operator == BinaryOperator::And { if self.operator == BinaryOperator::Or || self.operator == BinaryOperator::And {
let KclValue::Bool { let KclValue::Bool {
@ -1084,6 +1111,15 @@ impl Node<UnaryExpression> {
} }
let value = &self.argument.get_result(exec_state, ctx).await?; let value = &self.argument.get_result(exec_state, ctx).await?;
let err = || {
KclError::Semantic(KclErrorDetails {
message: format!(
"You can only negate numbers, planes, or lines, but this is a {}",
value.human_friendly_type()
),
source_ranges: vec![self.into()],
})
};
match value { match value {
KclValue::Number { value, ty, .. } => { KclValue::Number { value, ty, .. } => {
let meta = vec![Metadata { let meta = vec![Metadata {
@ -1105,13 +1141,63 @@ impl Node<UnaryExpression> {
plane.id = exec_state.next_uuid(); plane.id = exec_state.next_uuid();
Ok(KclValue::Plane { value: plane }) Ok(KclValue::Plane { value: plane })
} }
_ => Err(KclError::Semantic(KclErrorDetails { KclValue::Object { value: values, meta } => {
message: format!( // Special-case for negating line-like objects.
"You can only negate numbers or planes, but this is a {}", let Some(direction) = values.get("direction") else {
value.human_friendly_type() return Err(err());
), };
source_ranges: vec![self.into()],
})), let direction = match direction {
KclValue::MixedArray { value: values, meta } => {
let values = values
.iter()
.map(|v| match v {
KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
value: *value * -1.0,
ty: ty.clone(),
meta: meta.clone(),
}),
_ => Err(err()),
})
.collect::<Result<Vec<_>, _>>()?;
KclValue::MixedArray {
value: values,
meta: meta.clone(),
}
}
KclValue::HomArray {
value: values,
ty: ty @ RuntimeType::Primitive(PrimitiveType::Number(_)),
} => {
let values = values
.iter()
.map(|v| match v {
KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
value: *value * -1.0,
ty: ty.clone(),
meta: meta.clone(),
}),
_ => Err(err()),
})
.collect::<Result<Vec<_>, _>>()?;
KclValue::HomArray {
value: values,
ty: ty.clone(),
}
}
_ => return Err(err()),
};
let mut value = values.clone();
value.insert("direction".to_owned(), direction);
Ok(KclValue::Object {
value,
meta: meta.clone(),
})
}
_ => Err(err()),
} }
} }
} }
@ -1297,27 +1383,6 @@ impl Node<CallExpressionKw> {
// exec_state. // exec_state.
let func = fn_name.get_result(exec_state, ctx).await?.clone(); let func = fn_name.get_result(exec_state, ctx).await?.clone();
if !ctx.is_isolated_execution().await {
// Track call operation.
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
.collect();
exec_state.global.operations.push(Operation::UserDefinedFunctionCall {
name: Some(fn_name.to_string()),
function_source_range: func.function_def_source_range().unwrap_or_default(),
unlabeled_arg: args
.kw_args
.unlabeled
.as_ref()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
});
}
let Some(fn_src) = func.as_fn() else { let Some(fn_src) = func.as_fn() else {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
message: "cannot call this because it isn't a function".to_string(), message: "cannot call this because it isn't a function".to_string(),
@ -1325,12 +1390,20 @@ impl Node<CallExpressionKw> {
})); }));
}; };
let return_value = fn_src.call_kw(exec_state, ctx, args, callsite).await.map_err(|e| { let return_value = fn_src
.call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
.await
.map_err(|e| {
// Add the call expression to the source ranges. // Add the call expression to the source ranges.
// TODO currently ignored by the frontend // TODO currently ignored by the frontend
e.add_source_ranges(vec![callsite]) e.add_source_ranges(vec![callsite])
})?; })?;
if matches!(fn_src, FunctionSource::User { .. }) && !ctx.is_isolated_execution().await {
// Track return operation.
exec_state.global.operations.push(Operation::UserDefinedFunctionReturn);
}
let result = return_value.ok_or_else(move || { let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![callsite]; let mut source_ranges: Vec<SourceRange> = vec![callsite];
// We want to send the source range of the original function. // We want to send the source range of the original function.
@ -1343,11 +1416,6 @@ impl Node<CallExpressionKw> {
}) })
})?; })?;
if !ctx.is_isolated_execution().await {
// Track return operation.
exec_state.global.operations.push(Operation::UserDefinedFunctionReturn);
}
Ok(result) Ok(result)
} }
} }
@ -1459,7 +1527,10 @@ impl Node<CallExpression> {
source_ranges: vec![source_range], source_ranges: vec![source_range],
})); }));
}; };
let return_value = fn_src.call(exec_state, ctx, fn_args, source_range).await.map_err(|e| { let return_value = fn_src
.call(Some(fn_name.to_string()), exec_state, ctx, fn_args, source_range)
.await
.map_err(|e| {
// Add the call expression to the source ranges. // Add the call expression to the source ranges.
// TODO currently ignored by the frontend // TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range]) e.add_source_ranges(vec![source_range])
@ -2089,6 +2160,7 @@ async fn call_user_defined_function_kw(
impl FunctionSource { impl FunctionSource {
pub async fn call( pub async fn call(
&self, &self,
fn_name: Option<String>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
ctx: &ExecutorContext, ctx: &ExecutorContext,
mut args: Vec<Arg>, mut args: Vec<Arg>,
@ -2106,7 +2178,7 @@ impl FunctionSource {
ctx.clone(), ctx.clone(),
exec_state.mod_local.pipe_value.clone().map(|v| Arg::new(v, callsite)), exec_state.mod_local.pipe_value.clone().map(|v| Arg::new(v, callsite)),
); );
self.call_kw(exec_state, ctx, args, callsite).await self.call_kw(fn_name, exec_state, ctx, args, callsite).await
} else { } else {
Err(KclError::Semantic(KclErrorDetails { Err(KclError::Semantic(KclErrorDetails {
message: format!("{} requires its arguments to be labelled", props.name), message: format!("{} requires its arguments to be labelled", props.name),
@ -2123,6 +2195,7 @@ impl FunctionSource {
pub async fn call_kw( pub async fn call_kw(
&self, &self,
fn_name: Option<String>,
exec_state: &mut ExecState, exec_state: &mut ExecState,
ctx: &ExecutorContext, ctx: &ExecutorContext,
mut args: crate::std::Args, mut args: crate::std::Args,
@ -2206,6 +2279,26 @@ impl FunctionSource {
} }
} }
let op = if props.include_in_feature_tree && !ctx.is_isolated_execution().await {
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
.collect();
Some(Operation::KclStdLibCall {
name: fn_name.unwrap_or_default(),
unlabeled_arg: args
.unlabeled_kw_arg_unconverted()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
is_error: false,
})
} else {
None
};
// Attempt to call the function. // Attempt to call the function.
exec_state.mut_stack().push_new_env_for_rust_call(); exec_state.mut_stack().push_new_env_for_rust_call();
let mut result = { let mut result = {
@ -2213,7 +2306,15 @@ impl FunctionSource {
let result = func(exec_state, args).await; let result = func(exec_state, args).await;
exec_state.mut_stack().pop_env(); exec_state.mut_stack().pop_env();
// TODO support recording op into the feature tree if let Some(mut op) = op {
op.set_std_lib_call_is_error(result.is_err());
// Track call operation. We do this after the call
// since things like patternTransform may call user code
// before running, and we will likely want to use the
// return value. The call takes ownership of the args,
// so we need to build the op before the call.
exec_state.global.operations.push(op);
}
result result
}?; }?;
@ -2222,6 +2323,27 @@ impl FunctionSource {
Ok(Some(result)) Ok(Some(result))
} }
FunctionSource::User { ast, memory, .. } => { FunctionSource::User { ast, memory, .. } => {
if !ctx.is_isolated_execution().await {
// Track call operation.
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
.collect();
exec_state.global.operations.push(Operation::UserDefinedFunctionCall {
name: fn_name,
function_source_range: ast.as_source_range(),
unlabeled_arg: args
.kw_args
.unlabeled
.as_ref()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
});
}
call_user_defined_function_kw(args.kw_args, *memory, ast, exec_state, ctx).await call_user_defined_function_kw(args.kw_args, *memory, ast, exec_state, ctx).await
} }
FunctionSource::None => unreachable!(), FunctionSource::None => unreachable!(),

View File

@ -56,6 +56,20 @@ impl RuntimeType {
RuntimeType::Primitive(PrimitiveType::ImportedGeometry) RuntimeType::Primitive(PrimitiveType::ImportedGeometry)
} }
/// `[number; 2]`
pub fn point2d() -> Self {
RuntimeType::Array(Box::new(RuntimeType::number_any()), ArrayLen::Known(2))
}
/// `[number; 3]`
pub fn point3d() -> Self {
RuntimeType::Array(Box::new(RuntimeType::number_any()), ArrayLen::Known(3))
}
pub fn number_any() -> Self {
RuntimeType::Primitive(PrimitiveType::Number(NumericType::Any))
}
pub fn from_parsed( pub fn from_parsed(
value: Type, value: Type,
exec_state: &mut ExecState, exec_state: &mut ExecState,
@ -93,21 +107,27 @@ impl RuntimeType {
AstPrimitiveType::Number(suffix) => RuntimeType::Primitive(PrimitiveType::Number( AstPrimitiveType::Number(suffix) => RuntimeType::Primitive(PrimitiveType::Number(
NumericType::from_parsed(suffix, &exec_state.mod_local.settings), NumericType::from_parsed(suffix, &exec_state.mod_local.settings),
)), )),
AstPrimitiveType::Named(name) => { AstPrimitiveType::Named(name) => Self::from_alias(&name.name, exec_state, source_range)?,
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
})
}
pub fn from_alias(
alias: &str,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<Self, CompilationError> {
let ty_val = exec_state let ty_val = exec_state
.stack() .stack()
.get(&format!("{}{}", memory::TYPE_PREFIX, name.name), source_range) .get(&format!("{}{}", memory::TYPE_PREFIX, alias), source_range)
.map_err(|_| CompilationError::err(source_range, format!("Unknown type: {}", name.name)))?; .map_err(|_| CompilationError::err(source_range, format!("Unknown type: {}", alias)))?;
match ty_val { Ok(match ty_val {
KclValue::Type { value, .. } => match value { KclValue::Type { value, .. } => match value {
TypeDef::RustRepr(ty, _) => RuntimeType::Primitive(ty.clone()), TypeDef::RustRepr(ty, _) => RuntimeType::Primitive(ty.clone()),
TypeDef::Alias(ty) => ty.clone(), TypeDef::Alias(ty) => ty.clone(),
}, },
_ => unreachable!(), _ => unreachable!(),
}
}
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
}) })
} }
@ -143,6 +163,35 @@ impl RuntimeType {
(Object(t1), Object(t2)) => t2 (Object(t1), Object(t2)) => t2
.iter() .iter()
.all(|(f, t)| t1.iter().any(|(ff, tt)| f == ff && tt.subtype(t))), .all(|(f, t)| t1.iter().any(|(ff, tt)| f == ff && tt.subtype(t))),
// Equality between Axis types and their object representation.
(Object(t1), Primitive(PrimitiveType::Axis2d)) => {
t1.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point2d()))
&& t1
.iter()
.any(|(n, t)| n == "direction" && t.subtype(&RuntimeType::point2d()))
}
(Object(t1), Primitive(PrimitiveType::Axis3d)) => {
t1.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point3d()))
&& t1
.iter()
.any(|(n, t)| n == "direction" && t.subtype(&RuntimeType::point3d()))
}
(Primitive(PrimitiveType::Axis2d), Object(t2)) => {
t2.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point2d()))
&& t2
.iter()
.any(|(n, t)| n == "direction" && t.subtype(&RuntimeType::point2d()))
}
(Primitive(PrimitiveType::Axis3d), Object(t2)) => {
t2.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point3d()))
&& t2
.iter()
.any(|(n, t)| n == "direction" && t.subtype(&RuntimeType::point3d()))
}
_ => false, _ => false,
} }
} }
@ -213,11 +262,11 @@ impl ArrayLen {
} }
/// True if the length constraint is satisfied by the supplied length. /// True if the length constraint is satisfied by the supplied length.
fn satisfied(self, len: usize) -> bool { fn satisfied(self, len: usize, allow_shrink: bool) -> Option<usize> {
match self { match self {
ArrayLen::None => true, ArrayLen::None => Some(len),
ArrayLen::NonEmpty => len > 0, ArrayLen::NonEmpty => (len > 0).then_some(len),
ArrayLen::Known(s) => len == s, ArrayLen::Known(s) => (if allow_shrink { len >= s } else { len == s }).then_some(s),
} }
} }
} }
@ -233,6 +282,9 @@ pub enum PrimitiveType {
Plane, Plane,
Helix, Helix,
Face, Face,
Edge,
Axis2d,
Axis3d,
ImportedGeometry, ImportedGeometry,
} }
@ -248,6 +300,9 @@ impl PrimitiveType {
PrimitiveType::Plane => "Planes".to_owned(), PrimitiveType::Plane => "Planes".to_owned(),
PrimitiveType::Helix => "Helices".to_owned(), PrimitiveType::Helix => "Helices".to_owned(),
PrimitiveType::Face => "Faces".to_owned(), PrimitiveType::Face => "Faces".to_owned(),
PrimitiveType::Edge => "Edges".to_owned(),
PrimitiveType::Axis2d => "2d axes".to_owned(),
PrimitiveType::Axis3d => "3d axes".to_owned(),
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(), PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
PrimitiveType::Tag => "tags".to_owned(), PrimitiveType::Tag => "tags".to_owned(),
} }
@ -273,6 +328,9 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Solid => write!(f, "Solid"), PrimitiveType::Solid => write!(f, "Solid"),
PrimitiveType::Plane => write!(f, "Plane"), PrimitiveType::Plane => write!(f, "Plane"),
PrimitiveType::Face => write!(f, "Face"), PrimitiveType::Face => write!(f, "Face"),
PrimitiveType::Edge => write!(f, "Edge"),
PrimitiveType::Axis2d => write!(f, "Axis2d"),
PrimitiveType::Axis3d => write!(f, "Axis3d"),
PrimitiveType::Helix => write!(f, "Helix"), PrimitiveType::Helix => write!(f, "Helix"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"), PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
} }
@ -298,6 +356,10 @@ impl NumericType {
NumericType::Known(UnitType::Count) NumericType::Known(UnitType::Count)
} }
pub fn mm() -> Self {
NumericType::Known(UnitType::Length(UnitLen::Mm))
}
/// Combine two types when we expect them to be equal. /// Combine two types when we expect them to be equal.
pub fn combine_eq(self, other: &NumericType) -> NumericType { pub fn combine_eq(self, other: &NumericType) -> NumericType {
if &self == other { if &self == other {
@ -541,7 +603,7 @@ impl KclValue {
pub fn coerce(&self, ty: &RuntimeType, exec_state: &mut ExecState) -> Option<KclValue> { pub fn coerce(&self, ty: &RuntimeType, exec_state: &mut ExecState) -> Option<KclValue> {
match ty { match ty {
RuntimeType::Primitive(ty) => self.coerce_to_primitive_type(ty, exec_state), RuntimeType::Primitive(ty) => self.coerce_to_primitive_type(ty, exec_state),
RuntimeType::Array(ty, len) => self.coerce_to_array_type(ty, *len, exec_state), RuntimeType::Array(ty, len) => self.coerce_to_array_type(ty, *len, exec_state, false),
RuntimeType::Tuple(tys) => self.coerce_to_tuple_type(tys, exec_state), RuntimeType::Tuple(tys) => self.coerce_to_tuple_type(tys, exec_state),
RuntimeType::Union(tys) => self.coerce_to_union_type(tys, exec_state), RuntimeType::Union(tys) => self.coerce_to_union_type(tys, exec_state),
RuntimeType::Object(tys) => self.coerce_to_object_type(tys, exec_state), RuntimeType::Object(tys) => self.coerce_to_object_type(tys, exec_state),
@ -609,6 +671,55 @@ impl KclValue {
KclValue::Helix { .. } => Some(value.clone()), KclValue::Helix { .. } => Some(value.clone()),
_ => None, _ => None,
}, },
PrimitiveType::Edge => match value {
KclValue::Uuid { .. } => Some(value.clone()),
KclValue::TagIdentifier { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Axis2d => match value {
KclValue::Object { value: values, meta } => {
if values.get("origin")?.has_type(&RuntimeType::point2d())
&& values.get("direction")?.has_type(&RuntimeType::point2d())
{
return Some(value.clone());
}
let origin = values.get("origin").and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(2), exec_state, true)
})?;
let direction = values.get("direction").and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(2), exec_state, true)
})?;
Some(KclValue::Object {
value: [("origin".to_owned(), origin), ("direction".to_owned(), direction)].into(),
meta: meta.clone(),
})
}
_ => None,
},
PrimitiveType::Axis3d => match value {
KclValue::Object { value: values, meta } => {
if values.get("origin")?.has_type(&RuntimeType::point3d())
&& values.get("direction")?.has_type(&RuntimeType::point3d())
{
return Some(value.clone());
}
let origin = values.get("origin").and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(3), exec_state, true)
})?;
let direction = values.get("direction").and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(3), exec_state, true)
})?;
Some(KclValue::Object {
value: [("origin".to_owned(), origin), ("direction".to_owned(), direction)].into(),
meta: meta.clone(),
})
}
_ => None,
},
PrimitiveType::ImportedGeometry => match value { PrimitiveType::ImportedGeometry => match value {
KclValue::ImportedGeometry { .. } => Some(value.clone()), KclValue::ImportedGeometry { .. } => Some(value.clone()),
_ => None, _ => None,
@ -621,60 +732,35 @@ impl KclValue {
} }
} }
fn coerce_to_array_type(&self, ty: &RuntimeType, len: ArrayLen, exec_state: &mut ExecState) -> Option<KclValue> { fn coerce_to_array_type(
&self,
ty: &RuntimeType,
len: ArrayLen,
exec_state: &mut ExecState,
allow_shrink: bool,
) -> Option<KclValue> {
match self { match self {
KclValue::HomArray { value, ty: aty } if aty == ty => { KclValue::HomArray { value, ty: aty } if aty.subtype(ty) => {
let value = match len { len.satisfied(value.len(), allow_shrink).map(|len| KclValue::HomArray {
ArrayLen::None => value.clone(), value: value[..len].to_vec(),
ArrayLen::NonEmpty => { ty: aty.clone(),
if value.is_empty() { })
return None;
} }
value if len.satisfied(1, false).is_some() && value.has_type(ty) => Some(KclValue::HomArray {
value.clone()
}
ArrayLen::Known(n) => {
if n != value.len() {
return None;
}
value[..n].to_vec()
}
};
Some(KclValue::HomArray { value, ty: ty.clone() })
}
value if len.satisfied(1) && value.has_type(ty) => Some(KclValue::HomArray {
value: vec![value.clone()], value: vec![value.clone()],
ty: ty.clone(), ty: ty.clone(),
}), }),
KclValue::MixedArray { value, .. } => { KclValue::MixedArray { value, .. } => {
let value = match len { let len = len.satisfied(value.len(), allow_shrink)?;
ArrayLen::None => value.clone(),
ArrayLen::NonEmpty => {
if value.is_empty() {
return None;
}
value.clone() let value = value[..len]
}
ArrayLen::Known(n) => {
if n != value.len() {
return None;
}
value[..n].to_vec()
}
};
let value = value
.iter() .iter()
.map(|v| v.coerce(ty, exec_state)) .map(|v| v.coerce(ty, exec_state))
.collect::<Option<Vec<_>>>()?; .collect::<Option<Vec<_>>>()?;
Some(KclValue::HomArray { value, ty: ty.clone() }) Some(KclValue::HomArray { value, ty: ty.clone() })
} }
KclValue::KclNone { .. } if len.satisfied(0) => Some(KclValue::HomArray { KclValue::KclNone { .. } if len.satisfied(0, false).is_some() => Some(KclValue::HomArray {
value: Vec::new(), value: Vec::new(),
ty: ty.clone(), ty: ty.clone(),
}), }),
@ -1251,4 +1337,119 @@ mod test {
assert!(count.coerce(&tyb, &mut exec_state).is_none()); assert!(count.coerce(&tyb, &mut exec_state).is_none());
assert!(count.coerce(&tyb2, &mut exec_state).is_none()); assert!(count.coerce(&tyb2, &mut exec_state).is_none());
} }
#[tokio::test(flavor = "multi_thread")]
async fn coerce_axes() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
// Subtyping
assert!(RuntimeType::Primitive(PrimitiveType::Axis2d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis2d)));
assert!(RuntimeType::Primitive(PrimitiveType::Axis3d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis3d)));
assert!(!RuntimeType::Primitive(PrimitiveType::Axis3d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis2d)));
assert!(!RuntimeType::Primitive(PrimitiveType::Axis2d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis3d)));
// Coercion
let a2d = KclValue::Object {
value: [
(
"origin".to_owned(),
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
},
),
(
"direction".to_owned(),
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 1.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
},
),
]
.into(),
meta: Vec::new(),
};
let a3d = KclValue::Object {
value: [
(
"origin".to_owned(),
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
},
),
(
"direction".to_owned(),
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 1.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 1.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
},
),
]
.into(),
meta: Vec::new(),
};
let ty2d = RuntimeType::Primitive(PrimitiveType::Axis2d);
let ty3d = RuntimeType::Primitive(PrimitiveType::Axis3d);
assert_coerce_results(&a2d, &ty2d, &a2d, &mut exec_state);
assert_coerce_results(&a3d, &ty3d, &a3d, &mut exec_state);
assert_coerce_results(&a3d, &ty2d, &a2d, &mut exec_state);
assert!(a2d.coerce(&ty3d, &mut exec_state).is_none());
}
} }

View File

@ -120,7 +120,7 @@ const Part001 = startSketchOn('XY')
|> line([0, pipeLength], %) |> line([0, pipeLength], %)
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %) |> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|> close() |> close()
|> revolve({ axis: 'y' }, %) |> revolve({ axis = Y }, %)
" "
); );
@ -156,7 +156,7 @@ const part001 = startSketchOn('XY')
|> line([0, pipeLength], %) |> line([0, pipeLength], %)
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %) |> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|> close() |> close()
|> revolve({ axis: 'y' }, %) |> revolve({ axis = Y }, %)
" "
); );

View File

@ -1634,7 +1634,7 @@ insideRevolve = startSketchOn(XZ)
|> line(end = [0, -thickness]) |> line(end = [0, -thickness])
|> line(end = [-overHangLength, 0]) |> line(end = [-overHangLength, 0])
|> close() |> close()
|> revolve({ axis: 'y' }, %) |> revolve({ axis = Y }, %)
// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis) // Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
sphere = startSketchOn(XZ) sphere = startSketchOn(XZ)
@ -1649,7 +1649,7 @@ sphere = startSketchOn(XZ)
radius: sphereDia / 2 - 0.05 radius: sphereDia / 2 - 0.05
}, %) }, %)
|> close() |> close()
|> revolve({ axis: 'x' }, %) |> revolve({ axis = X }, %)
|> patternCircular3d( |> patternCircular3d(
axis = [0, 0, 1], axis = [0, 0, 1],
center = [0, 0, 0], center = [0, 0, 0],
@ -1673,7 +1673,7 @@ outsideRevolve = startSketchOn(XZ)
|> line(end = [0, thickness]) |> line(end = [0, thickness])
|> line(end = [overHangLength - thickness, 0]) |> line(end = [overHangLength - thickness, 0])
|> close() |> close()
|> revolve({ axis: 'y' }, %)"# |> revolve({ axis = Y }, %)"#
.to_string(), .to_string(),
}, },
}) })
@ -1707,7 +1707,7 @@ outsideRevolve = startSketchOn(XZ)
start: tower_lsp::lsp_types::Position { line: 0, character: 0 }, start: tower_lsp::lsp_types::Position { line: 0, character: 0 },
end: tower_lsp::lsp_types::Position { end: tower_lsp::lsp_types::Position {
line: 60, line: 60,
character: 30 character: 29
} }
} }
); );
@ -1734,7 +1734,7 @@ insideRevolve = startSketchOn(XZ)
|> line(end = [0, -thickness]) |> line(end = [0, -thickness])
|> line(end = [-overHangLength, 0]) |> line(end = [-overHangLength, 0])
|> close() |> close()
|> revolve({ axis = 'y' }, %) |> revolve({ axis = Y }, %)
// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis) // Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
sphere = startSketchOn(XZ) sphere = startSketchOn(XZ)
@ -1749,7 +1749,7 @@ sphere = startSketchOn(XZ)
radius = sphereDia / 2 - 0.05 radius = sphereDia / 2 - 0.05
}, %) }, %)
|> close() |> close()
|> revolve({ axis = 'x' }, %) |> revolve({ axis = X }, %)
|> patternCircular3d( |> patternCircular3d(
axis = [0, 0, 1], axis = [0, 0, 1],
center = [0, 0, 0], center = [0, 0, 0],
@ -1773,7 +1773,7 @@ outsideRevolve = startSketchOn(XZ)
|> line(end = [0, thickness]) |> line(end = [0, thickness])
|> line(end = [overHangLength - thickness, 0]) |> line(end = [overHangLength - thickness, 0])
|> close() |> close()
|> revolve({ axis = 'y' }, %)"# |> revolve({ axis = Y }, %)"#
); );
} }

View File

@ -286,6 +286,11 @@ fn non_code_node(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
alt((non_code_node_leading_whitespace, non_code_node_no_leading_whitespace)).parse_next(i) alt((non_code_node_leading_whitespace, non_code_node_no_leading_whitespace)).parse_next(i)
} }
fn outer_annotation(i: &mut TokenSlice) -> PResult<Node<Annotation>> {
peek((at_sign, open_paren)).parse_next(i)?;
annotation(i)
}
fn annotation(i: &mut TokenSlice) -> PResult<Node<Annotation>> { fn annotation(i: &mut TokenSlice) -> PResult<Node<Annotation>> {
let at = at_sign.parse_next(i)?; let at = at_sign.parse_next(i)?;
let name = opt(binding_name).parse_next(i)?; let name = opt(binding_name).parse_next(i)?;
@ -2903,13 +2908,17 @@ struct ParamDescription {
arg_name: Token, arg_name: Token,
type_: std::option::Option<Node<Type>>, type_: std::option::Option<Node<Type>>,
default_value: Option<DefaultParamVal>, default_value: Option<DefaultParamVal>,
attr: Option<Node<Annotation>>,
comments: Option<Node<Vec<String>>>, comments: Option<Node<Vec<String>>>,
} }
fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> { fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
let (_, comments, found_at_sign, arg_name, question_mark, _, type_, _ws, default_literal) = ( let (_, comments, _, attr, _, found_at_sign, arg_name, question_mark, _, type_, _ws, default_literal) = (
opt(whitespace), opt(whitespace),
opt(comments), opt(comments),
opt(whitespace),
opt(outer_annotation),
opt(whitespace),
opt(at_sign), opt(at_sign),
any.verify(|token: &Token| !matches!(token.token_type, TokenType::Brace) || token.value != ")"), any.verify(|token: &Token| !matches!(token.token_type, TokenType::Brace) || token.value != ")"),
opt(question_mark), opt(question_mark),
@ -2934,6 +2943,7 @@ fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
return Err(ErrMode::Backtrack(ContextError::from(e))); return Err(ErrMode::Backtrack(ContextError::from(e)));
} }
}, },
attr,
comments, comments,
}) })
} }
@ -2955,6 +2965,7 @@ fn parameters(i: &mut TokenSlice) -> PResult<Vec<Parameter>> {
arg_name, arg_name,
type_, type_,
default_value, default_value,
attr,
comments, comments,
}| { }| {
let mut identifier = Node::<Identifier>::try_from(arg_name)?; let mut identifier = Node::<Identifier>::try_from(arg_name)?;
@ -2962,6 +2973,9 @@ fn parameters(i: &mut TokenSlice) -> PResult<Vec<Parameter>> {
identifier.comment_start = comments.start; identifier.comment_start = comments.start;
identifier.pre_comments = comments.inner; identifier.pre_comments = comments.inner;
} }
if let Some(attr) = attr {
identifier.outer_attrs.push(attr);
}
Ok(Parameter { Ok(Parameter {
identifier, identifier,

View File

@ -75,7 +75,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// This will work on any solid, including extruded solids, revolved solids, and shelled solids. /// This will work on any solid, including extruded solids, revolved solids, and shelled solids.
/// ```no_run /// ```no_run
/// // Add color to an extruded solid. /// // Add color to an extruded solid.
/// exampleSketch = startSketchOn("XZ") /// exampleSketch = startSketchOn(XZ)
/// |> startProfileAt([0, 0], %) /// |> startProfileAt([0, 0], %)
/// |> line(endAbsolute = [10, 0]) /// |> line(endAbsolute = [10, 0])
/// |> line(endAbsolute = [0, 10]) /// |> line(endAbsolute = [0, 10])
@ -89,9 +89,9 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// ///
/// ```no_run /// ```no_run
/// // Add color to a revolved solid. /// // Add color to a revolved solid.
/// sketch001 = startSketchOn('XY') /// sketch001 = startSketchOn(XY)
/// |> circle( center = [15, 0], radius = 5 ) /// |> circle( center = [15, 0], radius = 5 )
/// |> revolve( angle = 360, axis = 'y') /// |> revolve( angle = 360, axis = Y)
/// |> appearance( /// |> appearance(
/// color = '#ff0000', /// color = '#ff0000',
/// metalness = 90, /// metalness = 90,
@ -102,7 +102,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// ```no_run /// ```no_run
/// // Add color to different solids. /// // Add color to different solids.
/// fn cube(center) { /// fn cube(center) {
/// return startSketchOn('XY') /// return startSketchOn(XY)
/// |> startProfileAt([center[0] - 10, center[1] - 10], %) /// |> startProfileAt([center[0] - 10, center[1] - 10], %)
/// |> line(endAbsolute = [center[0] + 10, center[1] - 10]) /// |> line(endAbsolute = [center[0] + 10, center[1] - 10])
/// |> line(endAbsolute = [center[0] + 10, center[1] + 10]) /// |> line(endAbsolute = [center[0] + 10, center[1] + 10])
@ -122,7 +122,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// ```no_run /// ```no_run
/// // You can set the appearance before or after you shell it will yield the same result. /// // You can set the appearance before or after you shell it will yield the same result.
/// // This example shows setting the appearance _after_ the shell. /// // This example shows setting the appearance _after_ the shell.
/// firstSketch = startSketchOn('XY') /// firstSketch = startSketchOn(XY)
/// |> startProfileAt([-12, 12], %) /// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0]) /// |> line(end = [24, 0])
/// |> line(end = [0, -24]) /// |> line(end = [0, -24])
@ -145,7 +145,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// ```no_run /// ```no_run
/// // You can set the appearance before or after you shell it will yield the same result. /// // You can set the appearance before or after you shell it will yield the same result.
/// // This example shows setting the appearance _before_ the shell. /// // This example shows setting the appearance _before_ the shell.
/// firstSketch = startSketchOn('XY') /// firstSketch = startSketchOn(XY)
/// |> startProfileAt([-12, 12], %) /// |> startProfileAt([-12, 12], %)
/// |> line(end = [24, 0]) /// |> line(end = [24, 0])
/// |> line(end = [0, -24]) /// |> line(end = [0, -24])
@ -168,7 +168,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// ```no_run /// ```no_run
/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern. /// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
/// // This example shows _before_ the pattern. /// // This example shows _before_ the pattern.
/// exampleSketch = startSketchOn('XZ') /// exampleSketch = startSketchOn(XZ)
/// |> startProfileAt([0, 0], %) /// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 2]) /// |> line(end = [0, 2])
/// |> line(end = [3, 1]) /// |> line(end = [3, 1])
@ -191,7 +191,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// ```no_run /// ```no_run
/// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern. /// // Setting the appearance of a 3D pattern can be done _before_ or _after_ the pattern.
/// // This example shows _after_ the pattern. /// // This example shows _after_ the pattern.
/// exampleSketch = startSketchOn('XZ') /// exampleSketch = startSketchOn(XZ)
/// |> startProfileAt([0, 0], %) /// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 2]) /// |> line(end = [0, 2])
/// |> line(end = [3, 1]) /// |> line(end = [3, 1])
@ -213,7 +213,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// ///
/// ```no_run /// ```no_run
/// // Color the result of a 2D pattern that was extruded. /// // Color the result of a 2D pattern that was extruded.
/// exampleSketch = startSketchOn('XZ') /// exampleSketch = startSketchOn(XZ)
/// |> startProfileAt([.5, 25], %) /// |> startProfileAt([.5, 25], %)
/// |> line(end = [0, 5]) /// |> line(end = [0, 5])
/// |> line(end = [-1, 0]) /// |> line(end = [-1, 0])
@ -238,7 +238,7 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// // Color the result of a sweep. /// // Color the result of a sweep.
/// ///
/// // Create a path for the sweep. /// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ') /// sweepPath = startSketchOn(XZ)
/// |> startProfileAt([0.05, 0.05], %) /// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7]) /// |> line(end = [0, 7])
/// |> tangentialArc({ /// |> tangentialArc({
@ -252,13 +252,13 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// }, %) /// }, %)
/// |> line(end = [0, 7]) /// |> line(end = [0, 7])
/// ///
/// pipeHole = startSketchOn('XY') /// pipeHole = startSketchOn(XY)
/// |> circle( /// |> circle(
/// center = [0, 0], /// center = [0, 0],
/// radius = 1.5, /// radius = 1.5,
/// ) /// )
/// ///
/// sweepSketch = startSketchOn('XY') /// sweepSketch = startSketchOn(XY)
/// |> circle( /// |> circle(
/// center = [0, 0], /// center = [0, 0],
/// radius = 2, /// radius = 2,

View File

@ -164,6 +164,22 @@ impl Args {
}) })
} }
pub(crate) fn get_kw_arg_opt_typed<T>(
&self,
label: &str,
ty: &RuntimeType,
exec_state: &mut ExecState,
) -> Result<Option<T>, KclError>
where
T: for<'a> FromKclValue<'a>,
{
if self.kw_args.labeled.get(label).is_none() {
return Ok(None);
};
self.get_kw_arg_typed(label, ty, exec_state).map(Some)
}
/// Get a keyword argument. If not set, returns Err. /// Get a keyword argument. If not set, returns Err.
pub(crate) fn get_kw_arg<'a, T>(&'a self, label: &str) -> Result<T, KclError> pub(crate) fn get_kw_arg<'a, T>(&'a self, label: &str) -> Result<T, KclError>
where where
@ -685,37 +701,6 @@ impl Args {
FromArgs::from_args(self, 0) FromArgs::from_args(self, 0)
} }
pub(crate) fn get_data_and_sketches<'a, T>(
&'a self,
exec_state: &mut ExecState,
) -> Result<(T, Vec<Sketch>), KclError>
where
T: serde::de::DeserializeOwned + FromArgs<'a>,
{
let data: T = FromArgs::from_args(self, 0)?;
let Some(arg1) = self.args.get(1) else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected one or more sketches for second argument".to_owned(),
source_ranges: vec![self.source_range],
}));
};
let sarg = arg1
.value
.coerce(&RuntimeType::sketches(), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!(
"Expected one or more sketches for second argument, found {}",
arg1.value.human_friendly_type()
),
source_ranges: vec![self.source_range],
}))?;
let sketches = match sarg {
KclValue::HomArray { value, .. } => value.iter().map(|v| v.as_sketch().unwrap().clone()).collect(),
_ => unreachable!(),
};
Ok((data, sketches))
}
pub(crate) fn get_data_and_sketch_and_tag<'a, T>( pub(crate) fn get_data_and_sketch_and_tag<'a, T>(
&'a self, &'a self,
exec_state: &mut ExecState, exec_state: &mut ExecState,
@ -1593,50 +1578,6 @@ impl<'a> FromKclValue<'a> for super::sketch::SketchData {
} }
} }
impl<'a> FromKclValue<'a> for super::axis_or_reference::AxisAndOrigin2d {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
// Case 1: predefined planes.
if let Some(s) = arg.as_str() {
return match s {
"X" | "x" => Some(Self::X),
"Y" | "y" => Some(Self::Y),
"-X" | "-x" => Some(Self::NegX),
"-Y" | "-y" => Some(Self::NegY),
_ => None,
};
}
// Case 2: custom planes.
let obj = arg.as_object()?;
let_field_of!(obj, custom, &KclObjectFields);
let_field_of!(custom, origin);
let_field_of!(custom, axis);
Some(Self::Custom { axis, origin })
}
}
impl<'a> FromKclValue<'a> for super::axis_or_reference::AxisAndOrigin3d {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
// Case 1: predefined planes.
if let Some(s) = arg.as_str() {
return match s {
"X" | "x" => Some(Self::X),
"Y" | "y" => Some(Self::Y),
"Z" | "z" => Some(Self::Z),
"-X" | "-x" => Some(Self::NegX),
"-Y" | "-y" => Some(Self::NegY),
"-Z" | "-z" => Some(Self::NegZ),
_ => None,
};
}
// Case 2: custom planes.
let obj = arg.as_object()?;
let_field_of!(obj, custom, &KclObjectFields);
let_field_of!(custom, origin);
let_field_of!(custom, axis);
Some(Self::Custom { axis, origin })
}
}
impl<'a> FromKclValue<'a> for super::fillet::EdgeReference { impl<'a> FromKclValue<'a> for super::fillet::EdgeReference {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let id = arg.as_uuid().map(Self::Uuid); let id = arg.as_uuid().map(Self::Uuid);
@ -1647,25 +1588,27 @@ impl<'a> FromKclValue<'a> for super::fillet::EdgeReference {
impl<'a> FromKclValue<'a> for super::axis_or_reference::Axis2dOrEdgeReference { impl<'a> FromKclValue<'a> for super::axis_or_reference::Axis2dOrEdgeReference {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let case1 = super::axis_or_reference::AxisAndOrigin2d::from_kcl_val; let case1 = |arg: &KclValue| {
let obj = arg.as_object()?;
let_field_of!(obj, direction);
let_field_of!(obj, origin);
Some(Self::Axis { direction, origin })
};
let case2 = super::fillet::EdgeReference::from_kcl_val; let case2 = super::fillet::EdgeReference::from_kcl_val;
case1(arg).map(Self::Axis).or_else(|| case2(arg).map(Self::Edge)) case1(arg).or_else(|| case2(arg).map(Self::Edge))
} }
} }
impl<'a> FromKclValue<'a> for super::axis_or_reference::Axis3dOrEdgeReference { impl<'a> FromKclValue<'a> for super::axis_or_reference::Axis3dOrEdgeReference {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let case1 = super::axis_or_reference::AxisAndOrigin3d::from_kcl_val; let case1 = |arg: &KclValue| {
let case2 = super::fillet::EdgeReference::from_kcl_val;
case1(arg).map(Self::Axis).or_else(|| case2(arg).map(Self::Edge))
}
}
impl<'a> FromKclValue<'a> for super::mirror::Mirror2dData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let obj = arg.as_object()?; let obj = arg.as_object()?;
let_field_of!(obj, axis); let_field_of!(obj, direction);
Some(Self { axis }) let_field_of!(obj, origin);
Some(Self::Axis { direction, origin })
};
let case2 = super::fillet::EdgeReference::from_kcl_val;
case1(arg).or_else(|| case2(arg).map(Self::Edge))
} }
} }

View File

@ -77,7 +77,7 @@ async fn call_map_closure(
ctxt: &ExecutorContext, ctxt: &ExecutorContext,
) -> Result<KclValue, KclError> { ) -> Result<KclValue, KclError> {
let output = map_fn let output = map_fn
.call(exec_state, ctxt, vec![Arg::synthetic(input)], source_range) .call(None, exec_state, ctxt, vec![Arg::synthetic(input)], source_range)
.await?; .await?;
let source_ranges = vec![source_range]; let source_ranges = vec![source_range];
let output = output.ok_or_else(|| { let output = output.ok_or_else(|| {
@ -202,7 +202,9 @@ async fn call_reduce_closure(
) -> Result<KclValue, KclError> { ) -> Result<KclValue, KclError> {
// Call the reduce fn for this repetition. // Call the reduce fn for this repetition.
let reduce_fn_args = vec![Arg::synthetic(elem), Arg::synthetic(start)]; let reduce_fn_args = vec![Arg::synthetic(elem), Arg::synthetic(start)];
let transform_fn_return = reduce_fn.call(exec_state, ctxt, reduce_fn_args, source_range).await?; let transform_fn_return = reduce_fn
.call(None, exec_state, ctxt, reduce_fn_args, source_range)
.await?;
// Unpack the returned transform object. // Unpack the returned transform object.
let source_ranges = vec![source_range]; let source_ranges = vec![source_range];

View File

@ -1,233 +1,21 @@
//! Types for referencing an axis or edge. //! Types for referencing an axis or edge.
use anyhow::Result; use crate::std::fillet::EdgeReference;
use kcmc::length_unit::LengthUnit;
use kittycad_modeling_cmds::{self as kcmc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{errors::KclError, std::fillet::EdgeReference};
/// A 2D axis or tagged edge. /// A 2D axis or tagged edge.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, PartialEq)]
#[ts(export)]
#[serde(untagged)]
pub enum Axis2dOrEdgeReference { pub enum Axis2dOrEdgeReference {
/// 2D axis and origin. /// 2D axis and origin.
Axis(AxisAndOrigin2d), Axis { direction: [f64; 2], origin: [f64; 2] },
/// Tagged edge. /// Tagged edge.
Edge(EdgeReference), Edge(EdgeReference),
} }
/// A 2D axis and origin.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum AxisAndOrigin2d {
/// X-axis.
#[serde(rename = "X", alias = "x")]
X,
/// Y-axis.
#[serde(rename = "Y", alias = "y")]
Y,
/// Flip the X-axis.
#[serde(rename = "-X", alias = "-x")]
NegX,
/// Flip the Y-axis.
#[serde(rename = "-Y", alias = "-y")]
NegY,
Custom {
/// The axis.
axis: [f64; 2],
/// The origin.
origin: [f64; 2],
},
}
impl AxisAndOrigin2d {
/// Get the axis and origin.
pub fn axis_and_origin(&self) -> Result<(kcmc::shared::Point3d<f64>, kcmc::shared::Point3d<LengthUnit>), KclError> {
let (axis, origin) = match self {
AxisAndOrigin2d::X => ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin2d::Y => ([0.0, 1.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin2d::NegX => ([-1.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin2d::NegY => ([0.0, -1.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin2d::Custom { axis, origin } => ([axis[0], axis[1], 0.0], [origin[0], origin[1], 0.0]),
};
Ok((
kcmc::shared::Point3d {
x: axis[0],
y: axis[1],
z: axis[2],
},
kcmc::shared::Point3d {
x: LengthUnit(origin[0]),
y: LengthUnit(origin[1]),
z: LengthUnit(origin[2]),
},
))
}
}
/// A 3D axis or tagged edge. /// A 3D axis or tagged edge.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, PartialEq)]
#[ts(export)]
#[serde(untagged)]
pub enum Axis3dOrEdgeReference { pub enum Axis3dOrEdgeReference {
/// 3D axis and origin. /// 3D axis and origin.
Axis(AxisAndOrigin3d), Axis { direction: [f64; 3], origin: [f64; 3] },
/// Tagged edge. /// Tagged edge.
Edge(EdgeReference), Edge(EdgeReference),
} }
/// A 3D axis and origin.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum AxisAndOrigin3d {
/// X-axis.
#[serde(rename = "X", alias = "x")]
X,
/// Y-axis.
#[serde(rename = "Y", alias = "y")]
Y,
/// Z-axis.
#[serde(rename = "Z", alias = "z")]
Z,
/// Flip the X-axis.
#[serde(rename = "-X", alias = "-x")]
NegX,
/// Flip the Y-axis.
#[serde(rename = "-Y", alias = "-y")]
NegY,
/// Flip the Z-axis.
#[serde(rename = "-Z", alias = "-z")]
NegZ,
Custom {
/// The axis.
axis: [f64; 3],
/// The origin.
origin: [f64; 3],
},
}
impl AxisAndOrigin3d {
/// Get the axis and origin.
pub fn axis_and_origin(&self) -> Result<(kcmc::shared::Point3d<f64>, kcmc::shared::Point3d<LengthUnit>), KclError> {
let (axis, origin) = match self {
AxisAndOrigin3d::X => ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::Y => ([0.0, 1.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::Z => ([0.0, 0.0, 1.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::NegX => ([-1.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::NegY => ([0.0, -1.0, 0.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::NegZ => ([0.0, 0.0, -1.0], [0.0, 0.0, 0.0]),
AxisAndOrigin3d::Custom { axis, origin } => {
([axis[0], axis[1], axis[2]], [origin[0], origin[1], origin[2]])
}
};
Ok((
kcmc::shared::Point3d {
x: axis[0],
y: axis[1],
z: axis[2],
},
kcmc::shared::Point3d {
x: LengthUnit(origin[0]),
y: LengthUnit(origin[1]),
z: LengthUnit(origin[2]),
},
))
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::std::axis_or_reference::{
Axis2dOrEdgeReference, Axis3dOrEdgeReference, AxisAndOrigin2d, AxisAndOrigin3d,
};
#[test]
fn test_deserialize_revolve_axis_2d() {
let data = Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::X);
let mut str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, "\"X\"");
str_json = "\"Y\"".to_string();
let data: Axis2dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::Y));
str_json = "\"-Y\"".to_string();
let data: Axis2dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::NegY));
str_json = "\"-x\"".to_string();
let data: Axis2dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::NegX));
let data = Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::Custom {
axis: [0.0, -1.0],
origin: [1.0, 0.0],
});
str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, r#"{"custom":{"axis":[0.0,-1.0],"origin":[1.0,0.0]}}"#);
str_json = r#"{"custom": {"axis": [0,-1], "origin": [1,2.0]}}"#.to_string();
let data: Axis2dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
Axis2dOrEdgeReference::Axis(AxisAndOrigin2d::Custom {
axis: [0.0, -1.0],
origin: [1.0, 2.0]
})
);
}
#[test]
fn test_deserialize_revolve_axis_3d() {
let data = Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::X);
let mut str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, "\"X\"");
str_json = "\"Y\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::Y));
str_json = "\"Z\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::Z));
str_json = "\"-Y\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::NegY));
str_json = "\"-x\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::NegX));
str_json = "\"-z\"".to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::NegZ));
let data = Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::Custom {
axis: [0.0, -1.0, 0.0],
origin: [1.0, 0.0, 0.0],
});
str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, r#"{"custom":{"axis":[0.0,-1.0,0.0],"origin":[1.0,0.0,0.0]}}"#);
str_json = r#"{"custom": {"axis": [0,-1,0], "origin": [1,2.0,0]}}"#.to_string();
let data: Axis3dOrEdgeReference = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
Axis3dOrEdgeReference::Axis(AxisAndOrigin3d::Custom {
axis: [0.0, -1.0, 0.0],
origin: [1.0, 2.0, 0.0]
})
);
}
}

View File

@ -28,6 +28,8 @@ pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// Union two or more solids into a single solid. /// Union two or more solids into a single solid.
/// ///
/// ```no_run /// ```no_run
/// // Union two cubes using the stdlib functions.
///
/// fn cube(center) { /// fn cube(center) {
/// return startSketchOn('XY') /// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %) /// |> startProfileAt([center[0] - 10, center[1] - 10], %)
@ -43,6 +45,52 @@ pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// ///
/// unionedPart = union([part001, part002]) /// unionedPart = union([part001, part002])
/// ``` /// ```
///
/// ```no_run
/// // Union two cubes using operators.
/// // NOTE: This will not work when using codemods through the UI.
/// // Codemods will generate the stdlib function call instead.
///
/// fn cube(center) {
/// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
/// |> line(endAbsolute = [center[0] + 10, center[1] - 10])
/// |> line(endAbsolute = [center[0] + 10, center[1] + 10])
/// |> line(endAbsolute = [center[0] - 10, center[1] + 10])
/// |> close()
/// |> extrude(length = 10)
/// }
///
/// part001 = cube([0, 0])
/// part002 = cube([20, 10])
///
/// // This is the equivalent of: union([part001, part002])
/// unionedPart = part001 + part002
/// ```
///
/// ```no_run
/// // Union two cubes using the more programmer-friendly operator.
/// // NOTE: This will not work when using codemods through the UI.
/// // Codemods will generate the stdlib function call instead.
///
/// fn cube(center) {
/// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
/// |> line(endAbsolute = [center[0] + 10, center[1] - 10])
/// |> line(endAbsolute = [center[0] + 10, center[1] + 10])
/// |> line(endAbsolute = [center[0] - 10, center[1] + 10])
/// |> close()
/// |> extrude(length = 10)
/// }
///
/// part001 = cube([0, 0])
/// part002 = cube([20, 10])
///
/// // This is the equivalent of: union([part001, part002])
/// // Programmers will understand `|` as a union operation, but mechanical engineers
/// // will understand `+`, we made both work.
/// unionedPart = part001 | part002
/// ```
#[stdlib { #[stdlib {
name = "union", name = "union",
feature_tree_operation = true, feature_tree_operation = true,
@ -53,7 +101,11 @@ pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
solids = {docs = "The solids to union."}, solids = {docs = "The solids to union."},
} }
}] }]
async fn inner_union(solids: Vec<Solid>, exec_state: &mut ExecState, args: Args) -> Result<Vec<Solid>, KclError> { pub(crate) async fn inner_union(
solids: Vec<Solid>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Solid>, KclError> {
// Flush the fillets for the solids. // Flush the fillets for the solids.
args.flush_batch_for_solids(exec_state, &solids).await?; args.flush_batch_for_solids(exec_state, &solids).await?;
@ -90,6 +142,8 @@ pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValu
/// verifying fit, and analyzing overlapping geometries in assemblies. /// verifying fit, and analyzing overlapping geometries in assemblies.
/// ///
/// ```no_run /// ```no_run
/// // Intersect two cubes using the stdlib functions.
///
/// fn cube(center) { /// fn cube(center) {
/// return startSketchOn('XY') /// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %) /// |> startProfileAt([center[0] - 10, center[1] - 10], %)
@ -105,6 +159,28 @@ pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValu
/// ///
/// intersectedPart = intersect([part001, part002]) /// intersectedPart = intersect([part001, part002])
/// ``` /// ```
///
/// ```no_run
/// // Intersect two cubes using operators.
/// // NOTE: This will not work when using codemods through the UI.
/// // Codemods will generate the stdlib function call instead.
///
/// fn cube(center) {
/// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
/// |> line(endAbsolute = [center[0] + 10, center[1] - 10])
/// |> line(endAbsolute = [center[0] + 10, center[1] + 10])
/// |> line(endAbsolute = [center[0] - 10, center[1] + 10])
/// |> close()
/// |> extrude(length = 10)
/// }
///
/// part001 = cube([0, 0])
/// part002 = cube([8, 8])
///
/// // This is the equivalent of: intersect([part001, part002])
/// intersectedPart = part001 & part002
/// ```
#[stdlib { #[stdlib {
name = "intersect", name = "intersect",
feature_tree_operation = true, feature_tree_operation = true,
@ -115,7 +191,11 @@ pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValu
solids = {docs = "The solids to intersect."}, solids = {docs = "The solids to intersect."},
} }
}] }]
async fn inner_intersect(solids: Vec<Solid>, exec_state: &mut ExecState, args: Args) -> Result<Vec<Solid>, KclError> { pub(crate) async fn inner_intersect(
solids: Vec<Solid>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Solid>, KclError> {
// Flush the fillets for the solids. // Flush the fillets for the solids.
args.flush_batch_for_solids(exec_state, &solids).await?; args.flush_batch_for_solids(exec_state, &solids).await?;
@ -145,6 +225,8 @@ pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue
/// and complex multi-body part modeling. /// and complex multi-body part modeling.
/// ///
/// ```no_run /// ```no_run
/// // Subtract a cylinder from a cube using the stdlib functions.
///
/// fn cube(center) { /// fn cube(center) {
/// return startSketchOn('XY') /// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %) /// |> startProfileAt([center[0] - 10, center[1] - 10], %)
@ -162,6 +244,30 @@ pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue
/// ///
/// subtractedPart = subtract([part001], tools=[part002]) /// subtractedPart = subtract([part001], tools=[part002])
/// ``` /// ```
///
/// ```no_run
/// // Subtract a cylinder from a cube using operators.
/// // NOTE: This will not work when using codemods through the UI.
/// // Codemods will generate the stdlib function call instead.
///
/// fn cube(center) {
/// return startSketchOn('XY')
/// |> startProfileAt([center[0] - 10, center[1] - 10], %)
/// |> line(endAbsolute = [center[0] + 10, center[1] - 10])
/// |> line(endAbsolute = [center[0] + 10, center[1] + 10])
/// |> line(endAbsolute = [center[0] - 10, center[1] + 10])
/// |> close()
/// |> extrude(length = 10)
/// }
///
/// part001 = cube([0, 0])
/// part002 = startSketchOn('XY')
/// |> circle(center = [0, 0], radius = 2)
/// |> extrude(length = 10)
///
/// // This is the equivalent of: subtract([part001], tools=[part002])
/// subtractedPart = part001 - part002
/// ```
#[stdlib { #[stdlib {
name = "subtract", name = "subtract",
feature_tree_operation = true, feature_tree_operation = true,
@ -169,11 +275,11 @@ pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue
unlabeled_first = true, unlabeled_first = true,
deprecated = true, deprecated = true,
args = { args = {
solids = {docs = "The solids to intersect."}, solids = {docs = "The solids to use as the base to subtract from."},
tools = {docs = "The solids to subtract."}, tools = {docs = "The solids to subtract."},
} }
}] }]
async fn inner_subtract( pub(crate) async fn inner_subtract(
solids: Vec<Solid>, solids: Vec<Solid>,
tools: Vec<Solid>, tools: Vec<Solid>,
exec_state: &mut ExecState, exec_state: &mut ExecState,

View File

@ -1,13 +1,15 @@
//! Standard library helices. //! Standard library helices.
use anyhow::Result; use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, ModelingCmd}; use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, ModelingCmd};
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds::{self as kcmc, shared::Point3d};
use crate::{ use crate::{
errors::KclError, errors::KclError,
execution::{ExecState, Helix as HelixValue, KclValue, Solid}, execution::{
types::{PrimitiveType, RuntimeType},
ExecState, Helix as HelixValue, KclValue, Solid,
},
std::{axis_or_reference::Axis3dOrEdgeReference, Args}, std::{axis_or_reference::Axis3dOrEdgeReference, Args},
}; };
@ -17,7 +19,14 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let revolutions = args.get_kw_arg("revolutions")?; let revolutions = args.get_kw_arg("revolutions")?;
let ccw = args.get_kw_arg_opt("ccw")?; let ccw = args.get_kw_arg_opt("ccw")?;
let radius = args.get_kw_arg_opt("radius")?; let radius = args.get_kw_arg_opt("radius")?;
let axis = args.get_kw_arg_opt("axis")?; let axis: Option<Axis3dOrEdgeReference> = args.get_kw_arg_opt_typed(
"axis",
&RuntimeType::Union(vec![
RuntimeType::Primitive(PrimitiveType::Edge),
RuntimeType::Primitive(PrimitiveType::Axis3d),
]),
exec_state,
)?;
let length = args.get_kw_arg_opt("length")?; let length = args.get_kw_arg_opt("length")?;
let cylinder = args.get_kw_arg_opt("cylinder")?; let cylinder = args.get_kw_arg_opt("cylinder")?;
@ -84,100 +93,6 @@ pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
Ok(KclValue::Helix { value }) Ok(KclValue::Helix { value })
} }
/// Create a helix.
///
/// ```no_run
/// // Create a helix around the Z axis.
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 5,
/// length = 10,
/// radius = 5,
/// axis = 'Z',
/// )
///
///
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('YZ')
/// |> circle( center = [0, 0], radius = 0.5)
/// |> sweep(path = helixPath)
/// ```
///
/// ```no_run
/// // Create a helix around an edge.
/// helper001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 10], tag = $edge001)
///
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 5,
/// length = 10,
/// radius = 5,
/// axis = edge001,
/// )
///
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('XY')
/// |> circle( center = [0, 0], radius = 0.5 )
/// |> sweep(path = helixPath)
/// ```
///
/// ```no_run
/// // Create a helix around a custom axis.
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 5,
/// length = 10,
/// radius = 5,
/// axis = {
/// custom = {
/// axis = [0, 0, 1.0],
/// origin = [0, 0.25, 0]
/// }
/// }
/// )
///
/// // Create a spring by sweeping around the helix path.
/// springSketch = startSketchOn('XY')
/// |> circle( center = [0, 0], radius = 1 )
/// |> sweep(path = helixPath)
/// ```
///
///
///
/// ```no_run
/// // Create a helix on a cylinder.
///
/// part001 = startSketchOn('XY')
/// |> circle( center= [5, 5], radius= 10 )
/// |> extrude(length = 10)
///
/// helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 16,
/// cylinder = part001,
/// )
/// ```
#[stdlib {
name = "helix",
keywords = true,
unlabeled_first = false,
args = {
revolutions = { docs = "Number of revolutions."},
angle_start = { docs = "Start angle (in degrees)."},
ccw = { docs = "Is the helix rotation counter clockwise? The default is `false`.", include_in_snippet = false},
radius = { docs = "Radius of the helix.", include_in_snippet = true},
axis = { docs = "Axis to use for the helix.", include_in_snippet = true},
length = { docs = "Length of the helix. This is not necessary if the helix is created around an edge. If not given the length of the edge is used.", include_in_snippet = true},
cylinder = { docs = "Cylinder to create the helix on.", include_in_snippet = false},
},
feature_tree_operation = true,
}]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn inner_helix( async fn inner_helix(
revolutions: f64, revolutions: f64,
@ -221,9 +136,7 @@ async fn inner_helix(
.await?; .await?;
} else if let (Some(axis), Some(radius)) = (axis, radius) { } else if let (Some(axis), Some(radius)) = (axis, radius) {
match axis { match axis {
Axis3dOrEdgeReference::Axis(axis) => { Axis3dOrEdgeReference::Axis { direction, origin } => {
let (axis, origin) = axis.axis_and_origin()?;
// Make sure they gave us a length. // Make sure they gave us a length.
let Some(length) = length else { let Some(length) = length else {
return Err(KclError::Semantic(crate::errors::KclErrorDetails { return Err(KclError::Semantic(crate::errors::KclErrorDetails {
@ -240,8 +153,16 @@ async fn inner_helix(
length: LengthUnit(length), length: LengthUnit(length),
revolutions, revolutions,
start_angle: Angle::from_degrees(angle_start), start_angle: Angle::from_degrees(angle_start),
axis, axis: Point3d {
center: origin, x: direction[0],
y: direction[1],
z: direction[2],
},
center: Point3d {
x: LengthUnit(origin[0]),
y: LengthUnit(origin[1]),
z: LengthUnit(origin[2]),
},
}), }),
) )
.await?; .await?;

View File

@ -1,109 +1,39 @@
//! Standard library mirror. //! Standard library mirror.
use anyhow::Result; use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, ModelingCmd}; use kcmc::{each_cmd as mcmd, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc}; use kittycad_modeling_cmds::{self as kcmc, length_unit::LengthUnit, shared::Point3d};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
errors::KclError, errors::KclError,
execution::{ExecState, KclValue, Sketch}, execution::{
types::{PrimitiveType, RuntimeType},
ExecState, KclValue, Sketch,
},
std::{axis_or_reference::Axis2dOrEdgeReference, Args}, std::{axis_or_reference::Axis2dOrEdgeReference, Args},
}; };
/// Data for a mirror.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Mirror2dData {
/// Axis to use as mirror.
pub axis: Axis2dOrEdgeReference,
}
/// Mirror a sketch. /// Mirror a sketch.
/// ///
/// Only works on unclosed sketches for now. /// Only works on unclosed sketches for now.
pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch_set): (Mirror2dData, Vec<Sketch>) = args.get_data_and_sketches(exec_state)?; let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
let axis = args.get_kw_arg_typed(
"axis",
&RuntimeType::Union(vec![
RuntimeType::Primitive(PrimitiveType::Edge),
RuntimeType::Primitive(PrimitiveType::Axis2d),
]),
exec_state,
)?;
let sketches = inner_mirror_2d(data, sketch_set, exec_state, args).await?; let sketches = inner_mirror_2d(sketches, axis, exec_state, args).await?;
Ok(sketches.into()) Ok(sketches.into())
} }
/// Mirror a sketch.
///
/// Only works on unclosed sketches for now.
///
/// Mirror occurs around a local sketch axis rather than a global axis.
///
/// ```no_run
/// // Mirror an un-closed sketch across the Y axis.
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 10], %)
/// |> line(end = [15, 0])
/// |> line(end = [-7, -3])
/// |> line(end = [9, -1])
/// |> line(end = [-8, -5])
/// |> line(end = [9, -3])
/// |> line(end = [-8, -3])
/// |> line(end = [9, -1])
/// |> line(end = [-19, -0])
/// |> mirror2d({axis = 'Y'}, %)
///
/// example = extrude(sketch001, length = 10)
/// ```
///
/// ```no_run
/// // Mirror a un-closed sketch across the Y axis.
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 8.5], %)
/// |> line(end = [20, -8.5])
/// |> line(end = [-20, -8.5])
/// |> mirror2d({axis = 'Y'}, %)
///
/// example = extrude(sketch001, length = 10)
/// ```
///
/// ```no_run
/// // Mirror a un-closed sketch across an edge.
/// helper001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [0, 10], tag = $edge001)
///
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 8.5], %)
/// |> line(end = [20, -8.5])
/// |> line(end = [-20, -8.5])
/// |> mirror2d({axis = edge001}, %)
///
/// // example = extrude(sketch001, length = 10)
/// ```
///
/// ```no_run
/// // Mirror an un-closed sketch across a custom axis.
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 8.5], %)
/// |> line(end = [20, -8.5])
/// |> line(end = [-20, -8.5])
/// |> mirror2d({
/// axis = {
/// custom = {
/// axis = [0.0, 1.0],
/// origin = [0.0, 0.0]
/// }
/// }
/// }, %)
///
/// example = extrude(sketch001, length = 10)
/// ```
#[stdlib {
name = "mirror2d",
}]
async fn inner_mirror_2d( async fn inner_mirror_2d(
data: Mirror2dData,
sketches: Vec<Sketch>, sketches: Vec<Sketch>,
axis: Axis2dOrEdgeReference,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Vec<Sketch>, KclError> { ) -> Result<Vec<Sketch>, KclError> {
@ -113,16 +43,22 @@ async fn inner_mirror_2d(
return Ok(starting_sketches); return Ok(starting_sketches);
} }
match data.axis { match axis {
Axis2dOrEdgeReference::Axis(axis) => { Axis2dOrEdgeReference::Axis { direction, origin } => {
let (axis, origin) = axis.axis_and_origin()?;
args.batch_modeling_cmd( args.batch_modeling_cmd(
exec_state.next_uuid(), exec_state.next_uuid(),
ModelingCmd::from(mcmd::EntityMirror { ModelingCmd::from(mcmd::EntityMirror {
ids: starting_sketches.iter().map(|sketch| sketch.id).collect(), ids: starting_sketches.iter().map(|sketch| sketch.id).collect(),
axis, axis: Point3d {
point: origin, x: direction[0],
y: direction[1],
z: 0.0,
},
point: Point3d {
x: LengthUnit(origin[0]),
y: LengthUnit(origin[1]),
z: LengthUnit(0.0),
},
}), }),
) )
.await?; .await?;

View File

@ -26,7 +26,6 @@ pub mod shell;
pub mod sketch; pub mod sketch;
pub mod sweep; pub mod sweep;
pub mod transform; pub mod transform;
pub mod types;
pub mod units; pub mod units;
pub mod utils; pub mod utils;
@ -96,7 +95,6 @@ lazy_static! {
Box::new(crate::std::sketch::TangentialArcToRelative), Box::new(crate::std::sketch::TangentialArcToRelative),
Box::new(crate::std::sketch::BezierCurve), Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Hole), Box::new(crate::std::sketch::Hole),
Box::new(crate::std::mirror::Mirror2D),
Box::new(crate::std::patterns::PatternLinear2D), Box::new(crate::std::patterns::PatternLinear2D),
Box::new(crate::std::patterns::PatternLinear3D), Box::new(crate::std::patterns::PatternLinear3D),
Box::new(crate::std::patterns::PatternCircular2D), Box::new(crate::std::patterns::PatternCircular2D),
@ -113,10 +111,8 @@ lazy_static! {
Box::new(crate::std::edge::GetNextAdjacentEdge), Box::new(crate::std::edge::GetNextAdjacentEdge),
Box::new(crate::std::edge::GetPreviousAdjacentEdge), Box::new(crate::std::edge::GetPreviousAdjacentEdge),
Box::new(crate::std::edge::GetCommonEdge), Box::new(crate::std::edge::GetCommonEdge),
Box::new(crate::std::helix::Helix),
Box::new(crate::std::shell::Shell), Box::new(crate::std::shell::Shell),
Box::new(crate::std::shell::Hollow), Box::new(crate::std::shell::Hollow),
Box::new(crate::std::revolve::Revolve),
Box::new(crate::std::sweep::Sweep), Box::new(crate::std::sweep::Sweep),
Box::new(crate::std::loft::Loft), Box::new(crate::std::loft::Loft),
Box::new(crate::std::planes::OffsetPlane), Box::new(crate::std::planes::OffsetPlane),
@ -177,6 +173,7 @@ pub fn get_stdlib_fn(name: &str) -> Option<Box<dyn StdLibFn>> {
pub struct StdFnProps { pub struct StdFnProps {
pub name: String, pub name: String,
pub deprecated: bool, pub deprecated: bool,
pub include_in_feature_tree: bool,
} }
impl StdFnProps { impl StdFnProps {
@ -184,8 +181,14 @@ impl StdFnProps {
Self { Self {
name: name.to_owned(), name: name.to_owned(),
deprecated: false, deprecated: false,
include_in_feature_tree: false,
} }
} }
fn include_in_feature_tree(mut self) -> Self {
self.include_in_feature_tree = true;
self
}
} }
pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProps) { pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProps) {
@ -206,6 +209,18 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
|e, a| Box::pin(crate::std::shapes::circle(e, a)), |e, a| Box::pin(crate::std::shapes::circle(e, a)),
StdFnProps::default("std::sketch::circle"), StdFnProps::default("std::sketch::circle"),
), ),
("prelude", "helix") => (
|e, a| Box::pin(crate::std::helix::helix(e, a)),
StdFnProps::default("std::helix").include_in_feature_tree(),
),
("sketch", "mirror2d") => (
|e, a| Box::pin(crate::std::mirror::mirror_2d(e, a)),
StdFnProps::default("std::sketch::mirror2d"),
),
("prelude", "revolve") => (
|e, a| Box::pin(crate::std::revolve::revolve(e, a)),
StdFnProps::default("std::revolve").include_in_feature_tree(),
),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -217,6 +232,9 @@ pub(crate) fn std_ty(path: &str, fn_name: &str) -> (PrimitiveType, StdFnProps) {
("prelude", "Plane") => (PrimitiveType::Plane, StdFnProps::default("std::Plane")), ("prelude", "Plane") => (PrimitiveType::Plane, StdFnProps::default("std::Plane")),
("prelude", "Face") => (PrimitiveType::Face, StdFnProps::default("std::Face")), ("prelude", "Face") => (PrimitiveType::Face, StdFnProps::default("std::Face")),
("prelude", "Helix") => (PrimitiveType::Helix, StdFnProps::default("std::Helix")), ("prelude", "Helix") => (PrimitiveType::Helix, StdFnProps::default("std::Helix")),
("prelude", "Edge") => (PrimitiveType::Edge, StdFnProps::default("std::Edge")),
("prelude", "Axis2d") => (PrimitiveType::Axis2d, StdFnProps::default("std::Axis2d")),
("prelude", "Axis3d") => (PrimitiveType::Axis3d, StdFnProps::default("std::Axis3d")),
_ => unreachable!(), _ => unreachable!(),
} }
} }

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