Merge remote-tracking branch 'origin/main' into jess/cleaned-imports
This commit is contained in:
31
Makefile
31
Makefile
@ -5,33 +5,40 @@ all: install build check
|
|||||||
# INSTALL
|
# INSTALL
|
||||||
|
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
CARGO ?= ~/.cargo/bin/cargo.exe
|
export WINDOWS := true
|
||||||
WASM_PACK ?= ~/.cargo/bin/wasm-pack.exe
|
ifndef MSYSTEM
|
||||||
else
|
export POWERSHELL := true
|
||||||
CARGO ?= ~/.cargo/bin/cargo
|
|
||||||
WASM_PACK ?= ~/.cargo/bin/wasm-pack
|
|
||||||
endif
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifdef WINDOWS
|
||||||
|
CARGO ?= $(USERPROFILE)/.cargo/bin/cargo.exe
|
||||||
|
WASM_PACK ?= $(USERPROFILE)/.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 $(CARGO) $(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)
|
ifdef POWERSHELL
|
||||||
@ type nul > $@
|
@ type nul > $@
|
||||||
else
|
else
|
||||||
@ touch $@
|
@ touch $@
|
||||||
endif
|
endif
|
||||||
|
|
||||||
$(CARGO):
|
$(CARGO):
|
||||||
ifeq ($(OS),Windows_NT)
|
ifdef WINDOWS
|
||||||
yarn install:rust:windows
|
yarn install:rust:windows
|
||||||
else
|
else
|
||||||
yarn install:rust
|
yarn install:rust
|
||||||
endif
|
endif
|
||||||
|
|
||||||
$(WASM_PACK):
|
$(WASM_PACK):
|
||||||
ifeq ($(OS),Windows_NT)
|
ifdef WINDOWS
|
||||||
yarn install:wasm-pack:cargo
|
yarn install:wasm-pack:cargo
|
||||||
else
|
else
|
||||||
yarn install:wasm-pack:sh
|
yarn install:wasm-pack:sh
|
||||||
@ -57,7 +64,7 @@ build-web: install public/kcl_wasm_lib_bg.wasm build/index.html
|
|||||||
build-desktop: install 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)
|
ifdef WINDOWS
|
||||||
yarn build:wasm:dev:windows
|
yarn build:wasm:dev:windows
|
||||||
else
|
else
|
||||||
yarn build:wasm:dev
|
yarn build:wasm:dev
|
||||||
@ -140,8 +147,8 @@ endif
|
|||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: ## Delete all artifacts
|
clean: ## Delete all artifacts
|
||||||
ifeq ($(OS),Windows_NT)
|
ifdef POWERSHELL
|
||||||
git clean --force -d -X
|
git clean --force -d -x --exclude=.env* --exclude=**/*.env
|
||||||
else
|
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/
|
||||||
@ -152,7 +159,7 @@ endif
|
|||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: install
|
help: install
|
||||||
ifeq ($(OS),Windows_NT)
|
ifdef POWERSHELL
|
||||||
@ 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] }"
|
@ 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
|
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}'
|
||||||
|
@ -78,7 +78,7 @@ assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
|
|||||||
```js
|
```js
|
||||||
// Declare a function that sketches a decagon.
|
// Declare a function that sketches a decagon.
|
||||||
fn decagon(radius) {
|
fn decagon(radius) {
|
||||||
// Each side of the decagon is turned this many degrees from the previous angle.
|
// Each side of the decagon is turned this many radians from the previous angle.
|
||||||
stepAngle = 1 / 10 * TAU
|
stepAngle = 1 / 10 * TAU
|
||||||
|
|
||||||
// Start the decagon sketch at this point.
|
// Start the decagon sketch at this point.
|
||||||
|
@ -9,7 +9,15 @@ Create a helix.
|
|||||||
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
helix(revolutions: number(_), angleStart: number(deg), ccw?: bool, radius?: number(mm), axis?: Axis3d | Edge, length?: number(mm), cylinder?: Solid): Helix
|
helix(
|
||||||
|
revolutions: number(_),
|
||||||
|
angleStart: number(deg),
|
||||||
|
ccw?: bool,
|
||||||
|
radius?: number(mm),
|
||||||
|
axis?: Axis3d | Edge,
|
||||||
|
length?: number(mm),
|
||||||
|
cylinder?: Solid,
|
||||||
|
): Helix
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,10 +6,14 @@ layout: manual
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Convert polar/sphere (azimuth, elevation, distance) coordinates tocartesian (x/y/z grid) coordinates.
|
Convert polar/sphere (azimuth, elevation, distance) coordinates to
|
||||||
|
cartesian (x/y/z grid) coordinates.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
polar(angle: number(deg), length: number(mm)): [number(mm); 2]
|
polar(
|
||||||
|
angle: number(deg),
|
||||||
|
length: number(mm),
|
||||||
|
): [number(mm); 2]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -18,7 +18,14 @@ You can provide more than one sketch to revolve, and they will all be
|
|||||||
revolved around the same axis.
|
revolved around the same axis.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
revolve(@sketches: [Sketch; 1+], axis: Axis2d | Edge, angle?: number(deg), tolerance?: number(mm), tagStart?: tag, tagEnd?: tag): Solid
|
revolve(
|
||||||
|
@sketches: [Sketch; 1+],
|
||||||
|
axis: Axis2d | Edge,
|
||||||
|
angle?: number(deg),
|
||||||
|
tolerance?: number(mm),
|
||||||
|
tagStart?: tag,
|
||||||
|
tagEnd?: tag,
|
||||||
|
): Solid
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,10 +6,16 @@ layout: manual
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Construct a 2-dimensional circle, of the specified radius, centered atthe provided (x, y) origin point.
|
Construct a 2-dimensional circle, of the specified radius, centered at
|
||||||
|
the provided (x, y) origin point.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
circle(@sketch_or_surface: Sketch | Plane | Face, center: Point2d, radius: number, tag?: tag): Sketch
|
circle(
|
||||||
|
@sketch_or_surface: Sketch | Plane | Face,
|
||||||
|
center: Point2d,
|
||||||
|
radius: number,
|
||||||
|
tag?: tag,
|
||||||
|
): Sketch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +11,10 @@ Only works on unclosed sketches for now.
|
|||||||
Mirror occurs around a local sketch axis rather than a global axis.
|
Mirror occurs around a local sketch axis rather than a global axis.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
mirror2d(@sketches: [Sketch; 1+], axis: Axis2d | Edge): Sketch
|
mirror2d(
|
||||||
|
@sketches: [Sketch; 1+],
|
||||||
|
axis: Axis2d | Edge,
|
||||||
|
): Sketch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -235702,7 +235702,7 @@
|
|||||||
"examples": [
|
"examples": [
|
||||||
"// This function adds two numbers.\nfn add(a, b) {\n return a + b\n}\n\n// This function adds an array of numbers.\n// It uses the `reduce` function, to call the `add` function on every\n// element of the `arr` parameter. The starting value is 0.\nfn sum(arr) {\n return reduce(arr, 0, add)\n}\n\n/* The above is basically like this pseudo-code:\nfn sum(arr):\n sumSoFar = 0\n for i in arr:\n sumSoFar = add(sumSoFar, i)\n return sumSoFar */\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum([1, 2, 3]), 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
|
"// This function adds two numbers.\nfn add(a, b) {\n return a + b\n}\n\n// This function adds an array of numbers.\n// It uses the `reduce` function, to call the `add` function on every\n// element of the `arr` parameter. The starting value is 0.\nfn sum(arr) {\n return reduce(arr, 0, add)\n}\n\n/* The above is basically like this pseudo-code:\nfn sum(arr):\n sumSoFar = 0\n for i in arr:\n sumSoFar = add(sumSoFar, i)\n return sumSoFar */\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum([1, 2, 3]), 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
|
||||||
"// This example works just like the previous example above, but it uses\n// an anonymous `add` function as its parameter, instead of declaring a\n// named function outside.\narr = [1, 2, 3]\nsum = reduce(arr, 0, fn(i, result_so_far) {\n return i + result_so_far\n})\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
|
"// This example works just like the previous example above, but it uses\n// an anonymous `add` function as its parameter, instead of declaring a\n// named function outside.\narr = [1, 2, 3]\nsum = reduce(arr, 0, fn(i, result_so_far) {\n return i + result_so_far\n})\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
|
||||||
"// Declare a function that sketches a decagon.\nfn decagon(radius) {\n // Each side of the decagon is turned this many degrees from the previous angle.\n stepAngle = 1 / 10 * TAU\n\n // Start the decagon sketch at this point.\n startOfDecagonSketch = startSketchOn(XY)\n |> startProfileAt([cos(0) * radius, sin(0) * radius], %)\n\n // Use a `reduce` to draw the remaining decagon sides.\n // For each number in the array 1..10, run the given function,\n // which takes a partially-sketched decagon and adds one more edge to it.\n fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {\n // Draw one edge of the decagon.\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n return line(partialDecagon, end = [x, y])\n })\n\n return fullDecagon\n}\n\n/* The `decagon` above is basically like this pseudo-code:\nfn decagon(radius):\n stepAngle = (1/10) * TAU\n plane = startSketchOn('XY')\n startOfDecagonSketch = startProfileAt([(cos(0)*radius), (sin(0) * radius)], plane)\n\n // Here's the reduce part.\n partialDecagon = startOfDecagonSketch\n for i in [1..10]:\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n partialDecagon = line(partialDecagon, end = [x, y])\n fullDecagon = partialDecagon // it's now full\n return fullDecagon */\n\n// Use the `decagon` function declared above, to sketch a decagon with radius 5.\ndecagon(5.0)\n |> close()"
|
"// Declare a function that sketches a decagon.\nfn decagon(radius) {\n // Each side of the decagon is turned this many radians from the previous angle.\n stepAngle = 1 / 10 * TAU\n\n // Start the decagon sketch at this point.\n startOfDecagonSketch = startSketchOn(XY)\n |> startProfileAt([cos(0) * radius, sin(0) * radius], %)\n\n // Use a `reduce` to draw the remaining decagon sides.\n // For each number in the array 1..10, run the given function,\n // which takes a partially-sketched decagon and adds one more edge to it.\n fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {\n // Draw one edge of the decagon.\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n return line(partialDecagon, end = [x, y])\n })\n\n return fullDecagon\n}\n\n/* The `decagon` above is basically like this pseudo-code:\nfn decagon(radius):\n stepAngle = (1/10) * TAU\n plane = startSketchOn('XY')\n startOfDecagonSketch = startProfileAt([(cos(0)*radius), (sin(0) * radius)], plane)\n\n // Here's the reduce part.\n partialDecagon = startOfDecagonSketch\n for i in [1..10]:\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n partialDecagon = line(partialDecagon, end = [x, y])\n fullDecagon = partialDecagon // it's now full\n return fullDecagon */\n\n// Use the `decagon` function declared above, to sketch a decagon with radius 5.\ndecagon(5.0)\n |> close()"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -137,7 +137,7 @@ async function doBasicSketch(
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Length: open menu' }).click()
|
await page.getByRole('button', { name: 'constraints: open menu' }).click()
|
||||||
await page.getByRole('button', { name: 'Equal Length' }).click()
|
await page.getByRole('button', { name: 'Equal Length' }).click()
|
||||||
|
|
||||||
// Open the code pane.
|
// Open the code pane.
|
||||||
|
@ -1353,4 +1353,51 @@ sketch001 = startSketchOn(XZ)
|
|||||||
15
|
15
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`test-toolbar-buttons`, async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
toolbar,
|
||||||
|
scene,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
await test.step('Load an empty file', async () => {
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem('persistCode', '')
|
||||||
|
})
|
||||||
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
// wait until scene is ready to be interacted with
|
||||||
|
await scene.connectionEstablished()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Test toolbar button correct selection', async () => {
|
||||||
|
await toolbar.expectToolbarMode.toBe('modeling')
|
||||||
|
|
||||||
|
await toolbar.startSketchPlaneSelection()
|
||||||
|
|
||||||
|
// Click on a default plane
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
|
// tools cannot be selected immediately, couldn't find an event to await instead.
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
await toolbar.selectCenterRectangle()
|
||||||
|
|
||||||
|
await expect(page.getByTestId('center-rectangle')).toHaveAttribute(
|
||||||
|
'aria-pressed',
|
||||||
|
'true'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Test Toolbar dropdown remembering last selection', async () => {
|
||||||
|
// Select another tool
|
||||||
|
await page.getByTestId('circle-center').click()
|
||||||
|
|
||||||
|
// center-rectangle should still be the active option in the rectangle dropdown
|
||||||
|
await expect(page.getByTestId('center-rectangle')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -49,7 +49,9 @@ export class SceneFixture {
|
|||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
this.streamWrapper = page.getByTestId('stream')
|
this.streamWrapper = page.getByTestId('stream')
|
||||||
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
|
this.networkToggleConnected = page
|
||||||
|
.getByTestId('network-toggle-ok')
|
||||||
|
.or(page.getByTestId('network-toggle-other'))
|
||||||
this.startEditSketchBtn = page
|
this.startEditSketchBtn = page
|
||||||
.getByRole('button', { name: 'Start Sketch' })
|
.getByRole('button', { name: 'Start Sketch' })
|
||||||
.or(page.getByRole('button', { name: 'Edit Sketch' }))
|
.or(page.getByRole('button', { name: 'Edit Sketch' }))
|
||||||
|
@ -169,7 +169,7 @@ export class ToolbarFixture {
|
|||||||
}
|
}
|
||||||
selectCenterRectangle = async () => {
|
selectCenterRectangle = async () => {
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('button', { name: 'caret down Corner rectangle:' })
|
.getByRole('button', { name: 'caret down rectangles:' })
|
||||||
.click()
|
.click()
|
||||||
await expect(
|
await expect(
|
||||||
this.page.getByTestId('dropdown-center-rectangle')
|
this.page.getByTestId('dropdown-center-rectangle')
|
||||||
@ -178,7 +178,7 @@ export class ToolbarFixture {
|
|||||||
}
|
}
|
||||||
selectBoolean = async (operation: 'union' | 'subtract' | 'intersect') => {
|
selectBoolean = async (operation: 'union' | 'subtract' | 'intersect') => {
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('button', { name: 'caret down Union: open menu' })
|
.getByRole('button', { name: 'caret down booleans: open menu' })
|
||||||
.click()
|
.click()
|
||||||
const operationTestId = `dropdown-boolean-${operation}`
|
const operationTestId = `dropdown-boolean-${operation}`
|
||||||
await expect(this.page.getByTestId(operationTestId)).toBeVisible()
|
await expect(this.page.getByTestId(operationTestId)).toBeVisible()
|
||||||
@ -186,25 +186,19 @@ export class ToolbarFixture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectCircleThreePoint = async () => {
|
selectCircleThreePoint = async () => {
|
||||||
await this.page
|
await this.page.getByRole('button', { name: 'caret down circles:' }).click()
|
||||||
.getByRole('button', { name: 'caret down Center circle:' })
|
|
||||||
.click()
|
|
||||||
await expect(
|
await expect(
|
||||||
this.page.getByTestId('dropdown-circle-three-points')
|
this.page.getByTestId('dropdown-circle-three-points')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await this.page.getByTestId('dropdown-circle-three-points').click()
|
await this.page.getByTestId('dropdown-circle-three-points').click()
|
||||||
}
|
}
|
||||||
selectArc = async () => {
|
selectArc = async () => {
|
||||||
await this.page
|
await this.page.getByRole('button', { name: 'caret down arcs:' }).click()
|
||||||
.getByRole('button', { name: 'caret down Tangential Arc:' })
|
|
||||||
.click()
|
|
||||||
await expect(this.page.getByTestId('dropdown-arc')).toBeVisible()
|
await expect(this.page.getByTestId('dropdown-arc')).toBeVisible()
|
||||||
await this.page.getByTestId('dropdown-arc').click()
|
await this.page.getByTestId('dropdown-arc').click()
|
||||||
}
|
}
|
||||||
selectThreePointArc = async () => {
|
selectThreePointArc = async () => {
|
||||||
await this.page
|
await this.page.getByRole('button', { name: 'caret down arcs:' }).click()
|
||||||
.getByRole('button', { name: 'caret down Tangential Arc:' })
|
|
||||||
.click()
|
|
||||||
await expect(
|
await expect(
|
||||||
this.page.getByTestId('dropdown-three-point-arc')
|
this.page.getByTestId('dropdown-three-point-arc')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
@ -61,6 +61,7 @@ function tomlStringOverWriteNamedViewUuids(toml: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Named view tests', () => {
|
test.describe('Named view tests', () => {
|
||||||
|
test.skip() // TODO: Jace is working on these
|
||||||
test('Verify project.toml is not created', async ({ page }, testInfo) => {
|
test('Verify project.toml is not created', async ({ page }, testInfo) => {
|
||||||
// Create project and load it
|
// Create project and load it
|
||||||
const projectName = 'named-views'
|
const projectName = 'named-views'
|
||||||
|
@ -7,6 +7,7 @@ import { expect, test } from '@e2e/playwright/zoo-test'
|
|||||||
* Test file menu actions that trigger something in the frontend
|
* Test file menu actions that trigger something in the frontend
|
||||||
*/
|
*/
|
||||||
test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||||
|
test.skip() // TODO: Reimplement native file menu tests
|
||||||
test.describe('Home page', () => {
|
test.describe('Home page', () => {
|
||||||
test.describe('File role', () => {
|
test.describe('File role', () => {
|
||||||
test('Home.File.Create project', async ({ tronApp, cmdBar, page }) => {
|
test('Home.File.Create project', async ({ tronApp, cmdBar, page }) => {
|
||||||
|
@ -1,13 +1,47 @@
|
|||||||
import * as fsp from 'fs/promises'
|
import * as fsp from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
import { executorInputPath } from '@e2e/playwright/test-utils'
|
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
||||||
import { test } from '@e2e/playwright/zoo-test'
|
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
|
||||||
|
import {
|
||||||
|
executorInputPath,
|
||||||
|
getUtils,
|
||||||
|
testsInputPath,
|
||||||
|
} from '@e2e/playwright/test-utils'
|
||||||
|
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
async function insertPartIntoAssembly(
|
||||||
|
path: string,
|
||||||
|
alias: string,
|
||||||
|
toolbar: ToolbarFixture,
|
||||||
|
cmdBar: CmdBarFixture,
|
||||||
|
page: Page
|
||||||
|
) {
|
||||||
|
await toolbar.insertButton.click()
|
||||||
|
await cmdBar.selectOption({ name: path }).click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'localName',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: { Path: path, LocalName: '' },
|
||||||
|
highlightedHeaderArg: 'localName',
|
||||||
|
commandName: 'Insert',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText(alias)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: { Path: path, LocalName: alias },
|
||||||
|
commandName: 'Insert',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
}
|
||||||
|
|
||||||
// test file is for testing point an click code gen functionality that's assemblies related
|
// test file is for testing point an click code gen functionality that's assemblies related
|
||||||
test.describe('Point-and-click assemblies tests', () => {
|
test.describe('Point-and-click assemblies tests', () => {
|
||||||
test(
|
test(
|
||||||
`Insert kcl part into assembly as whole module import`,
|
`Insert kcl parts into assembly as whole module import`,
|
||||||
{ tag: ['@electron'] },
|
{ tag: ['@electron'] },
|
||||||
async ({
|
async ({
|
||||||
context,
|
context,
|
||||||
@ -23,11 +57,14 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
fail()
|
fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
// One dumb hardcoded screen pixel value
|
const midPoint = { x: 500, y: 250 }
|
||||||
const testPoint = { x: 575, y: 200 }
|
const partPoint = { x: midPoint.x + 30, y: midPoint.y - 30 } // mid point, just off top right
|
||||||
const initialColor: [number, number, number] = [50, 50, 50]
|
const defaultPlanesColor: [number, number, number] = [180, 220, 180]
|
||||||
const partColor: [number, number, number] = [150, 150, 150]
|
const partColor: [number, number, number] = [100, 100, 100]
|
||||||
const tolerance = 50
|
const tolerance = 50
|
||||||
|
const u = await getUtils(page)
|
||||||
|
const gizmo = page.locator('[aria-label*=gizmo]')
|
||||||
|
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
|
||||||
|
|
||||||
await test.step('Setup parts and expect empty assembly scene', async () => {
|
await test.step('Setup parts and expect empty assembly scene', async () => {
|
||||||
const projectName = 'assembly'
|
const projectName = 'assembly'
|
||||||
@ -36,41 +73,36 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
await fsp.mkdir(bracketDir, { recursive: true })
|
await fsp.mkdir(bracketDir, { recursive: true })
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fsp.copyFile(
|
fsp.copyFile(
|
||||||
executorInputPath('cylinder-inches.kcl'),
|
executorInputPath('cylinder.kcl'),
|
||||||
path.join(bracketDir, 'cylinder.kcl')
|
path.join(bracketDir, 'cylinder.kcl')
|
||||||
),
|
),
|
||||||
fsp.copyFile(
|
fsp.copyFile(
|
||||||
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
|
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
|
||||||
path.join(bracketDir, 'bracket.kcl')
|
path.join(bracketDir, 'bracket.kcl')
|
||||||
),
|
),
|
||||||
|
fsp.copyFile(
|
||||||
|
testsInputPath('cube.step'),
|
||||||
|
path.join(bracketDir, 'cube.step')
|
||||||
|
),
|
||||||
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
|
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
await homePage.openProject(projectName)
|
await homePage.openProject(projectName)
|
||||||
await scene.settled(cmdBar)
|
await scene.settled(cmdBar)
|
||||||
await scene.expectPixelColor(initialColor, testPoint, tolerance)
|
await toolbar.closePane('code')
|
||||||
|
await scene.expectPixelColor(defaultPlanesColor, midPoint, tolerance)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Insert first part into the assembly', async () => {
|
await test.step('Insert kcl as first part as module', async () => {
|
||||||
await toolbar.insertButton.click()
|
await insertPartIntoAssembly(
|
||||||
await cmdBar.selectOption({ name: 'cylinder.kcl' }).click()
|
'cylinder.kcl',
|
||||||
await cmdBar.expectState({
|
'cylinder',
|
||||||
stage: 'arguments',
|
toolbar,
|
||||||
currentArgKey: 'localName',
|
cmdBar,
|
||||||
currentArgValue: '',
|
page
|
||||||
headerArguments: { Path: 'cylinder.kcl', LocalName: '' },
|
)
|
||||||
highlightedHeaderArg: 'localName',
|
await toolbar.openPane('code')
|
||||||
commandName: 'Insert',
|
|
||||||
})
|
|
||||||
await page.keyboard.insertText('cylinder')
|
|
||||||
await cmdBar.progressCmdBar()
|
|
||||||
await cmdBar.expectState({
|
|
||||||
stage: 'review',
|
|
||||||
headerArguments: { Path: 'cylinder.kcl', LocalName: 'cylinder' },
|
|
||||||
commandName: 'Insert',
|
|
||||||
})
|
|
||||||
await cmdBar.progressCmdBar()
|
|
||||||
await editor.expectEditor.toContain(
|
await editor.expectEditor.toContain(
|
||||||
`
|
`
|
||||||
import "cylinder.kcl" as cylinder
|
import "cylinder.kcl" as cylinder
|
||||||
@ -78,28 +110,27 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
`,
|
`,
|
||||||
{ shouldNormalise: true }
|
{ shouldNormalise: true }
|
||||||
)
|
)
|
||||||
await scene.expectPixelColor(partColor, testPoint, tolerance)
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
// Check scene for changes
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await u.doAndWaitForCmd(async () => {
|
||||||
|
await gizmo.click({ button: 'right' })
|
||||||
|
await resetCameraButton.click()
|
||||||
|
}, 'zoom_to_fit')
|
||||||
|
await toolbar.closePane('debug')
|
||||||
|
await scene.expectPixelColor(partColor, partPoint, tolerance)
|
||||||
|
await toolbar.openPane('code')
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Insert second part into the assembly', async () => {
|
await test.step('Insert kcl second part as module', async () => {
|
||||||
await toolbar.insertButton.click()
|
await insertPartIntoAssembly(
|
||||||
await cmdBar.selectOption({ name: 'bracket.kcl' }).click()
|
'bracket.kcl',
|
||||||
await cmdBar.expectState({
|
'bracket',
|
||||||
stage: 'arguments',
|
toolbar,
|
||||||
currentArgKey: 'localName',
|
cmdBar,
|
||||||
currentArgValue: '',
|
page
|
||||||
headerArguments: { Path: 'bracket.kcl', LocalName: '' },
|
)
|
||||||
highlightedHeaderArg: 'localName',
|
|
||||||
commandName: 'Insert',
|
|
||||||
})
|
|
||||||
await page.keyboard.insertText('bracket')
|
|
||||||
await cmdBar.progressCmdBar()
|
|
||||||
await cmdBar.expectState({
|
|
||||||
stage: 'review',
|
|
||||||
headerArguments: { Path: 'bracket.kcl', LocalName: 'bracket' },
|
|
||||||
commandName: 'Insert',
|
|
||||||
})
|
|
||||||
await cmdBar.progressCmdBar()
|
|
||||||
await editor.expectEditor.toContain(
|
await editor.expectEditor.toContain(
|
||||||
`
|
`
|
||||||
import "cylinder.kcl" as cylinder
|
import "cylinder.kcl" as cylinder
|
||||||
@ -109,6 +140,152 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
`,
|
`,
|
||||||
{ shouldNormalise: true }
|
{ shouldNormalise: true }
|
||||||
)
|
)
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Insert a second time and expect error', async () => {
|
||||||
|
// TODO: revisit once we have clone with #6209
|
||||||
|
await insertPartIntoAssembly(
|
||||||
|
'bracket.kcl',
|
||||||
|
'bracket',
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
import "cylinder.kcl" as cylinder
|
||||||
|
import "bracket.kcl" as bracket
|
||||||
|
import "bracket.kcl" as bracket
|
||||||
|
cylinder
|
||||||
|
bracket
|
||||||
|
bracket
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Insert foreign parts into assembly as whole module import`,
|
||||||
|
{ tag: ['@electron'] },
|
||||||
|
async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
tronApp,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const midPoint = { x: 500, y: 250 }
|
||||||
|
const partPoint = { x: midPoint.x + 30, y: midPoint.y - 30 } // mid point, just off top right
|
||||||
|
const defaultPlanesColor: [number, number, number] = [180, 220, 180]
|
||||||
|
const partColor: [number, number, number] = [150, 150, 150]
|
||||||
|
const tolerance = 50
|
||||||
|
|
||||||
|
const complexPlmFileName = 'cube_Complex-PLM_Name_-001.sldprt'
|
||||||
|
const camelCasedSolidworksFileName = 'cubeComplexPLMName001'
|
||||||
|
|
||||||
|
await test.step('Setup parts and expect empty assembly scene', async () => {
|
||||||
|
const projectName = 'assembly'
|
||||||
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
const bracketDir = path.join(dir, projectName)
|
||||||
|
await fsp.mkdir(bracketDir, { recursive: true })
|
||||||
|
await Promise.all([
|
||||||
|
fsp.copyFile(
|
||||||
|
testsInputPath('cube.step'),
|
||||||
|
path.join(bracketDir, 'cube.step')
|
||||||
|
),
|
||||||
|
fsp.copyFile(
|
||||||
|
testsInputPath('cube.sldprt'),
|
||||||
|
path.join(bracketDir, complexPlmFileName)
|
||||||
|
),
|
||||||
|
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.openProject(projectName)
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await scene.expectPixelColor(defaultPlanesColor, midPoint, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Insert step part as module', async () => {
|
||||||
|
await insertPartIntoAssembly('cube.step', 'cube', toolbar, cmdBar, page)
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
import "cube.step" as cube
|
||||||
|
cube
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
// TODO: remove this once #5780 is fixed
|
||||||
|
await page.reload()
|
||||||
|
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await scene.expectPixelColor(partColor, partPoint, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Insert second step part by clicking', async () => {
|
||||||
|
await toolbar.openPane('files')
|
||||||
|
await toolbar.expectFileTreeState([
|
||||||
|
complexPlmFileName,
|
||||||
|
'cube.step',
|
||||||
|
'main.kcl',
|
||||||
|
])
|
||||||
|
await toolbar.openFile(complexPlmFileName)
|
||||||
|
|
||||||
|
// Go through the ToastInsert prompt
|
||||||
|
await page.getByText('Insert into my current file').click()
|
||||||
|
|
||||||
|
// Check getPathFilenameInVariableCase output
|
||||||
|
const parsedValueFromFile =
|
||||||
|
await cmdBar.currentArgumentInput.inputValue()
|
||||||
|
expect(parsedValueFromFile).toEqual(camelCasedSolidworksFileName)
|
||||||
|
|
||||||
|
// Continue on with the flow
|
||||||
|
await page.keyboard.insertText('cubeSw')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: { Path: complexPlmFileName, LocalName: 'cubeSw' },
|
||||||
|
commandName: 'Insert',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await toolbar.closePane('files')
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
import "cube.step" as cube
|
||||||
|
import "${complexPlmFileName}" as cubeSw
|
||||||
|
cube
|
||||||
|
cubeSw
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
// TODO: remove this once #5780 is fixed
|
||||||
|
await page.reload()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await scene.expectPixelColor(partColor, partPoint, tolerance)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1021,6 +1021,10 @@ export function executorInputPath(fileName: string): string {
|
|||||||
return path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName)
|
return path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function testsInputPath(fileName: string): string {
|
||||||
|
return path.join('rust', 'kcl-lib', 'tests', 'inputs', fileName)
|
||||||
|
}
|
||||||
|
|
||||||
export async function doAndWaitForImageDiff(
|
export async function doAndWaitForImageDiff(
|
||||||
page: Page,
|
page: Page,
|
||||||
fn: () => Promise<unknown>,
|
fn: () => Promise<unknown>,
|
||||||
|
@ -115,7 +115,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
|
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
.click()
|
.click()
|
||||||
await page.getByRole('button', { name: 'remove constraints' }).click()
|
await page.getByRole('button', { name: 'remove constraints' }).click()
|
||||||
@ -189,7 +189,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
.click()
|
.click()
|
||||||
await page
|
await page
|
||||||
@ -299,7 +299,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
.click()
|
.click()
|
||||||
await page.getByRole('button', { name: constraint }).click()
|
await page.getByRole('button', { name: constraint }).click()
|
||||||
@ -420,7 +420,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
.click()
|
.click()
|
||||||
await page
|
await page
|
||||||
@ -533,7 +533,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
.click()
|
.click()
|
||||||
await page.getByTestId('dropdown-constraint-angle').click()
|
await page.getByTestId('dropdown-constraint-angle').click()
|
||||||
@ -627,7 +627,7 @@ test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
|
|||||||
await page.mouse.click(line3.x, line3.y)
|
await page.mouse.click(line3.x, line3.y)
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
.click()
|
.click()
|
||||||
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
||||||
@ -719,7 +719,7 @@ part002 = startSketchOn(XZ)
|
|||||||
await page.mouse.click(line3.x, line3.y)
|
await page.mouse.click(line3.x, line3.y)
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
.click()
|
.click()
|
||||||
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
||||||
@ -817,7 +817,7 @@ part002 = startSketchOn(XZ)
|
|||||||
const activeLinesContent = await page.locator('.cm-activeLine').all()
|
const activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||||
|
|
||||||
const constraintMenuButton = page.getByRole('button', {
|
const constraintMenuButton = page.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
const constraintButton = page
|
const constraintButton = page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
@ -905,7 +905,7 @@ part002 = startSketchOn(XZ)
|
|||||||
await page.mouse.click(line3.x - 3, line3.y + 20)
|
await page.mouse.click(line3.x - 3, line3.y + 20)
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
const constraintMenuButton = page.getByRole('button', {
|
const constraintMenuButton = page.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
const constraintButton = page.getByRole('button', {
|
const constraintButton = page.getByRole('button', {
|
||||||
name: constraintName,
|
name: constraintName,
|
||||||
@ -990,7 +990,7 @@ part002 = startSketchOn(XZ)
|
|||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
const constraintMenuButton = page.getByRole('button', {
|
const constraintMenuButton = page.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
const constraintButton = page.getByRole('button', {
|
const constraintButton = page.getByRole('button', {
|
||||||
name: constraintName,
|
name: constraintName,
|
||||||
@ -1057,7 +1057,7 @@ part002 = startSketchOn(XZ)
|
|||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
.click()
|
.click()
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
|
@ -124,7 +124,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
|||||||
|
|
||||||
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
||||||
const constrainButton = page.getByRole('button', {
|
const constrainButton = page.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'constraints: open menu',
|
||||||
})
|
})
|
||||||
const absXButton = page.getByRole('button', { name: 'Absolute X' })
|
const absXButton = page.getByRole('button', { name: 'Absolute X' })
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashSet, str::FromStr};
|
use std::{collections::HashSet, fmt, str::FromStr};
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use tower_lsp::lsp_types::{
|
use tower_lsp::lsp_types::{
|
||||||
@ -389,21 +389,23 @@ impl FnData {
|
|||||||
pub fn fn_signature(&self) -> String {
|
pub fn fn_signature(&self) -> String {
|
||||||
let mut signature = String::new();
|
let mut signature = String::new();
|
||||||
|
|
||||||
signature.push('(');
|
if self.args.is_empty() {
|
||||||
for (i, arg) in self.args.iter().enumerate() {
|
signature.push_str("()");
|
||||||
if i > 0 {
|
} else if self.args.len() == 1 {
|
||||||
signature.push_str(", ");
|
signature.push('(');
|
||||||
}
|
signature.push_str(&self.args[0].to_string());
|
||||||
match &arg.kind {
|
signature.push(')');
|
||||||
ArgKind::Special => signature.push_str(&format!("@{}", arg.name)),
|
} else {
|
||||||
ArgKind::Labelled(false) => signature.push_str(&arg.name),
|
signature.push('(');
|
||||||
ArgKind::Labelled(true) => signature.push_str(&format!("{}?", arg.name)),
|
for a in &self.args {
|
||||||
}
|
signature.push_str("\n ");
|
||||||
if let Some(ty) = &arg.ty {
|
signature.push_str(&a.to_string());
|
||||||
signature.push_str(&format!(": {ty}"));
|
signature.push(',');
|
||||||
}
|
}
|
||||||
|
signature.push('\n');
|
||||||
|
signature.push(')');
|
||||||
}
|
}
|
||||||
signature.push(')');
|
|
||||||
if let Some(ty) = &self.return_type {
|
if let Some(ty) = &self.return_type {
|
||||||
signature.push_str(&format!(": {ty}"));
|
signature.push_str(&format!(": {ty}"));
|
||||||
}
|
}
|
||||||
@ -515,6 +517,20 @@ pub struct ArgData {
|
|||||||
pub docs: Option<String>,
|
pub docs: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ArgData {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match &self.kind {
|
||||||
|
ArgKind::Special => write!(f, "@{}", self.name)?,
|
||||||
|
ArgKind::Labelled(false) => f.write_str(&self.name)?,
|
||||||
|
ArgKind::Labelled(true) => write!(f, "{}?", self.name)?,
|
||||||
|
}
|
||||||
|
if let Some(ty) = &self.ty {
|
||||||
|
write!(f, ": {ty}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum ArgKind {
|
pub enum ArgKind {
|
||||||
Special,
|
Special,
|
||||||
@ -766,8 +782,8 @@ trait ApplyMeta {
|
|||||||
description = summary;
|
description = summary;
|
||||||
summary = None;
|
summary = None;
|
||||||
let d = description.as_mut().unwrap();
|
let d = description.as_mut().unwrap();
|
||||||
d.push_str(l);
|
|
||||||
d.push('\n');
|
d.push('\n');
|
||||||
|
d.push_str(l);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -360,15 +360,6 @@ impl KclValue {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Put the number into a KCL value.
|
|
||||||
pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
|
|
||||||
Self::Number {
|
|
||||||
value: f,
|
|
||||||
meta,
|
|
||||||
ty: NumericType::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn from_number_with_type(f: f64, ty: NumericType, meta: Vec<Metadata>) -> Self {
|
pub const fn from_number_with_type(f: f64, ty: NumericType, meta: Vec<Metadata>) -> Self {
|
||||||
Self::Number { value: f, meta, ty }
|
Self::Number { value: f, meta, ty }
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub(super) static ref CHECK_NUMERIC_TYPES: bool = {
|
pub(crate) static ref CHECK_NUMERIC_TYPES: bool = {
|
||||||
let env_var = std::env::var("ZOO_NUM_TYS");
|
let env_var = std::env::var("ZOO_NUM_TYS");
|
||||||
let Ok(env_var) = env_var else {
|
let Ok(env_var) = env_var else {
|
||||||
return false;
|
return false;
|
||||||
@ -416,6 +416,80 @@ impl NumericType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn combine_eq_array(input: &[TyF64]) -> (Vec<f64>, NumericType) {
|
||||||
|
use NumericType::*;
|
||||||
|
|
||||||
|
let mut result = input.iter().map(|t| t.n).collect();
|
||||||
|
|
||||||
|
let mut ty = Any;
|
||||||
|
// Invariant mismatch is true => ty is Known
|
||||||
|
let mut mismatch = false;
|
||||||
|
for i in input {
|
||||||
|
if i.ty == Any || ty == i.ty {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match (&ty, &i.ty) {
|
||||||
|
(Any, t) => {
|
||||||
|
ty = t.clone();
|
||||||
|
}
|
||||||
|
(_, Unknown) | (Default { .. }, Default { .. }) => return (result, Unknown),
|
||||||
|
|
||||||
|
// Known types and compatible, but needs adjustment.
|
||||||
|
(Known(UnitType::Length(_)), Known(UnitType::Length(_)))
|
||||||
|
| (Known(UnitType::Angle(_)), Known(UnitType::Angle(_))) => {
|
||||||
|
mismatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Known but incompatible.
|
||||||
|
(Known(_), Known(_)) => return (result, Unknown),
|
||||||
|
|
||||||
|
// Known and unknown, no adjustment for counting numbers.
|
||||||
|
(Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => {
|
||||||
|
ty = Known(UnitType::Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
(Known(UnitType::Length(l1)), Default { len: l2, .. }) => {
|
||||||
|
mismatch |= l1 != l2;
|
||||||
|
}
|
||||||
|
(Known(UnitType::Angle(a1)), Default { angle: a2, .. }) => {
|
||||||
|
mismatch |= a1 != a2;
|
||||||
|
}
|
||||||
|
|
||||||
|
(Default { len: l1, .. }, Known(UnitType::Length(l2))) => {
|
||||||
|
mismatch |= l1 != l2;
|
||||||
|
ty = Known(UnitType::Length(*l2));
|
||||||
|
}
|
||||||
|
(Default { angle: a1, .. }, Known(UnitType::Angle(a2))) => {
|
||||||
|
mismatch |= a1 != a2;
|
||||||
|
ty = Known(UnitType::Angle(*a2));
|
||||||
|
}
|
||||||
|
|
||||||
|
(Unknown, _) | (_, Any) => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mismatch {
|
||||||
|
return (result, ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result
|
||||||
|
.into_iter()
|
||||||
|
.zip(input)
|
||||||
|
.map(|(n, i)| match (&ty, &i.ty) {
|
||||||
|
(Known(UnitType::Length(l1)), Known(UnitType::Length(l2)) | Default { len: l2, .. }) => {
|
||||||
|
l2.adjust_to(n, *l1)
|
||||||
|
}
|
||||||
|
(Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2)) | Default { angle: a2, .. }) => {
|
||||||
|
a2.adjust_to(n, *a1)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(result, ty)
|
||||||
|
}
|
||||||
|
|
||||||
/// Combine two types for multiplication-like operations.
|
/// Combine two types for multiplication-like operations.
|
||||||
pub fn combine_mul(a: TyF64, b: TyF64) -> (f64, f64, NumericType) {
|
pub fn combine_mul(a: TyF64, b: TyF64) -> (f64, f64, NumericType) {
|
||||||
use NumericType::*;
|
use NumericType::*;
|
||||||
@ -621,7 +695,7 @@ pub enum UnitLen {
|
|||||||
|
|
||||||
impl UnitLen {
|
impl UnitLen {
|
||||||
fn adjust_to(self, value: f64, to: UnitLen) -> f64 {
|
fn adjust_to(self, value: f64, to: UnitLen) -> f64 {
|
||||||
if self == to {
|
if !*CHECK_NUMERIC_TYPES || self == to {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -734,6 +808,11 @@ impl UnitAngle {
|
|||||||
fn adjust_to(self, value: f64, to: UnitAngle) -> f64 {
|
fn adjust_to(self, value: f64, to: UnitAngle) -> f64 {
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
use UnitAngle::*;
|
use UnitAngle::*;
|
||||||
|
|
||||||
|
if !*CHECK_NUMERIC_TYPES {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
match (self, to) {
|
match (self, to) {
|
||||||
(Degrees, Degrees) => value,
|
(Degrees, Degrees) => value,
|
||||||
(Degrees, Radians) => (value / 180.0) * PI,
|
(Degrees, Radians) => (value / 180.0) * PI,
|
||||||
@ -1847,11 +1926,16 @@ n = 10inch / 2mm
|
|||||||
o = 3mm / 3
|
o = 3mm / 3
|
||||||
p = 3_ / 4
|
p = 3_ / 4
|
||||||
q = 4inch / 2_
|
q = 4inch / 2_
|
||||||
|
|
||||||
|
r = min(0, 3, 42)
|
||||||
|
s = min(0, 3mm, -42)
|
||||||
|
t = min(100, 3in, 142mm)
|
||||||
|
u = min(3rad, 4in)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = parse_execute(program).await.unwrap();
|
let result = parse_execute(program).await.unwrap();
|
||||||
if *CHECK_NUMERIC_TYPES {
|
if *CHECK_NUMERIC_TYPES {
|
||||||
assert_eq!(result.exec_state.errors().len(), 2);
|
assert_eq!(result.exec_state.errors().len(), 3);
|
||||||
} else {
|
} else {
|
||||||
assert!(result.exec_state.errors().is_empty());
|
assert!(result.exec_state.errors().is_empty());
|
||||||
}
|
}
|
||||||
@ -1861,7 +1945,9 @@ q = 4inch / 2_
|
|||||||
assert_value_and_type("c", &result, 13.0, NumericType::mm());
|
assert_value_and_type("c", &result, 13.0, NumericType::mm());
|
||||||
assert_value_and_type("d", &result, 13.0, NumericType::mm());
|
assert_value_and_type("d", &result, 13.0, NumericType::mm());
|
||||||
assert_value_and_type("e", &result, 13.0, NumericType::mm());
|
assert_value_and_type("e", &result, 13.0, NumericType::mm());
|
||||||
assert_value_and_type("f", &result, 5.0, NumericType::mm());
|
if *CHECK_NUMERIC_TYPES {
|
||||||
|
assert_value_and_type("f", &result, 5.0, NumericType::mm());
|
||||||
|
}
|
||||||
|
|
||||||
assert_value_and_type("g", &result, 20.0, NumericType::default());
|
assert_value_and_type("g", &result, 20.0, NumericType::default());
|
||||||
assert_value_and_type("h", &result, 20.0, NumericType::mm());
|
assert_value_and_type("h", &result, 20.0, NumericType::mm());
|
||||||
@ -1871,9 +1957,30 @@ q = 4inch / 2_
|
|||||||
|
|
||||||
assert_value_and_type("l", &result, 0.0, NumericType::count());
|
assert_value_and_type("l", &result, 0.0, NumericType::count());
|
||||||
assert_value_and_type("m", &result, 2.0, NumericType::count());
|
assert_value_and_type("m", &result, 2.0, NumericType::count());
|
||||||
assert_value_and_type("n", &result, 127.0, NumericType::count());
|
if *CHECK_NUMERIC_TYPES {
|
||||||
|
assert_value_and_type("n", &result, 127.0, NumericType::count());
|
||||||
|
}
|
||||||
assert_value_and_type("o", &result, 1.0, NumericType::mm());
|
assert_value_and_type("o", &result, 1.0, NumericType::mm());
|
||||||
assert_value_and_type("p", &result, 1.0, NumericType::count());
|
assert_value_and_type("p", &result, 1.0, NumericType::count());
|
||||||
assert_value_and_type("q", &result, 2.0, NumericType::Known(UnitType::Length(UnitLen::Inches)));
|
assert_value_and_type("q", &result, 2.0, NumericType::Known(UnitType::Length(UnitLen::Inches)));
|
||||||
|
|
||||||
|
assert_value_and_type("r", &result, 0.0, NumericType::default());
|
||||||
|
assert_value_and_type("s", &result, -42.0, NumericType::mm());
|
||||||
|
assert_value_and_type("t", &result, 3.0, NumericType::Known(UnitType::Length(UnitLen::Inches)));
|
||||||
|
assert_value_and_type("u", &result, 3.0, NumericType::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn bad_typed_arithmetic() {
|
||||||
|
let program = r#"
|
||||||
|
a = 1rad
|
||||||
|
b = 180 / PI * a + 360
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let result = parse_execute(program).await.unwrap();
|
||||||
|
|
||||||
|
assert_value_and_type("a", &result, 1.0, NumericType::radians());
|
||||||
|
// TODO type is not ideal
|
||||||
|
assert_value_and_type("b", &result, 417.0, NumericType::radians());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -523,15 +523,6 @@ impl Args {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn make_user_val_from_f64(&self, f: f64) -> KclValue {
|
|
||||||
KclValue::from_number(
|
|
||||||
f,
|
|
||||||
vec![Metadata {
|
|
||||||
source_range: self.source_range,
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn make_user_val_from_f64_with_type(&self, f: TyF64) -> KclValue {
|
pub(super) fn make_user_val_from_f64_with_type(&self, f: TyF64) -> KclValue {
|
||||||
KclValue::from_number_with_type(
|
KclValue::from_number_with_type(
|
||||||
f.n,
|
f.n,
|
||||||
|
@ -133,7 +133,7 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
|||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// // Declare a function that sketches a decagon.
|
/// // Declare a function that sketches a decagon.
|
||||||
/// fn decagon(radius) {
|
/// fn decagon(radius) {
|
||||||
/// // Each side of the decagon is turned this many degrees from the previous angle.
|
/// // Each side of the decagon is turned this many radians from the previous angle.
|
||||||
/// stepAngle = (1/10) * TAU
|
/// stepAngle = (1/10) * TAU
|
||||||
///
|
///
|
||||||
/// // Start the decagon sketch at this point.
|
/// // Start the decagon sketch at this point.
|
||||||
|
@ -6,18 +6,31 @@ use kcl_derive_docs::stdlib;
|
|||||||
use super::args::FromArgs;
|
use super::args::FromArgs;
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
execution::{types::NumericType, ExecState, KclValue},
|
execution::{
|
||||||
|
types::{self, NumericType},
|
||||||
|
ExecState, KclValue,
|
||||||
|
},
|
||||||
std::args::{Args, TyF64},
|
std::args::{Args, TyF64},
|
||||||
|
CompilationError,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Compute the remainder after dividing `num` by `div`.
|
/// Compute the remainder after dividing `num` by `div`.
|
||||||
/// If `num` is negative, the result will be too.
|
/// If `num` is negative, the result will be too.
|
||||||
pub async fn rem(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn rem(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let n = args.get_unlabeled_kw_arg("number to divide")?;
|
let n: TyF64 = args.get_unlabeled_kw_arg("number to divide")?;
|
||||||
let d = args.get_kw_arg("divisor")?;
|
let d: TyF64 = args.get_kw_arg("divisor")?;
|
||||||
|
|
||||||
|
let (n, d, ty) = NumericType::combine_div(n, d);
|
||||||
|
if *types::CHECK_NUMERIC_TYPES && ty == NumericType::Unknown {
|
||||||
|
// TODO suggest how to fix this
|
||||||
|
exec_state.warn(CompilationError::err(
|
||||||
|
args.source_range,
|
||||||
|
"Remainder of numbers which have unknown or incompatible units.",
|
||||||
|
));
|
||||||
|
}
|
||||||
let remainder = inner_rem(n, d);
|
let remainder = inner_rem(n, d);
|
||||||
|
|
||||||
Ok(args.make_user_val_from_f64(remainder))
|
Ok(args.make_user_val_from_f64_with_type(TyF64::new(remainder, ty)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the remainder after dividing `num` by `div`.
|
/// Compute the remainder after dividing `num` by `div`.
|
||||||
@ -243,11 +256,19 @@ fn inner_ceil(num: f64) -> Result<f64, KclError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the minimum of the given arguments.
|
/// Compute the minimum of the given arguments.
|
||||||
pub async fn min(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn min(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let nums = args.get_number_array()?;
|
let nums = args.get_number_array_with_types()?;
|
||||||
|
let (nums, ty) = NumericType::combine_eq_array(&nums);
|
||||||
|
if *types::CHECK_NUMERIC_TYPES && ty == NumericType::Unknown {
|
||||||
|
// TODO suggest how to fix this
|
||||||
|
exec_state.warn(CompilationError::err(
|
||||||
|
args.source_range,
|
||||||
|
"Calling `min` on numbers which have unknown or incompatible units.",
|
||||||
|
));
|
||||||
|
}
|
||||||
let result = inner_min(nums);
|
let result = inner_min(nums);
|
||||||
|
|
||||||
Ok(args.make_user_val_from_f64(result))
|
Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, ty)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the minimum of the given arguments.
|
/// Compute the minimum of the given arguments.
|
||||||
@ -280,11 +301,19 @@ fn inner_min(args: Vec<f64>) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the maximum of the given arguments.
|
/// Compute the maximum of the given arguments.
|
||||||
pub async fn max(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn max(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let nums = args.get_number_array()?;
|
let nums = args.get_number_array_with_types()?;
|
||||||
|
let (nums, ty) = NumericType::combine_eq_array(&nums);
|
||||||
|
if *types::CHECK_NUMERIC_TYPES && ty == NumericType::Unknown {
|
||||||
|
// TODO suggest how to fix this
|
||||||
|
exec_state.warn(CompilationError::err(
|
||||||
|
args.source_range,
|
||||||
|
"Calling `max` on numbers which have unknown or incompatible units.",
|
||||||
|
));
|
||||||
|
}
|
||||||
let result = inner_max(nums);
|
let result = inner_max(nums);
|
||||||
|
|
||||||
Ok(args.make_user_val_from_f64(result))
|
Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, ty)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the maximum of the given arguments.
|
/// Compute the maximum of the given arguments.
|
||||||
|
@ -69,7 +69,7 @@ export fn cos(@num: number(rad)): number(_) {}
|
|||||||
/// |> startProfileAt([0, 0], %)
|
/// |> startProfileAt([0, 0], %)
|
||||||
/// |> angledLine({
|
/// |> angledLine({
|
||||||
/// angle = 50,
|
/// angle = 50,
|
||||||
/// length = 15 / sin(toDegrees(135)),
|
/// length = 15 / sin(toRadians(135)),
|
||||||
/// }, %)
|
/// }, %)
|
||||||
/// |> yLine(endAbsolute = 0)
|
/// |> yLine(endAbsolute = 0)
|
||||||
/// |> close()
|
/// |> close()
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 58 KiB |
106
src/Toolbar.tsx
106
src/Toolbar.tsx
@ -17,12 +17,14 @@ import { isDesktop } from '@src/lib/isDesktop'
|
|||||||
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||||
import { editorManager, kclManager } from '@src/lib/singletons'
|
import { editorManager, kclManager } from '@src/lib/singletons'
|
||||||
import type {
|
import type {
|
||||||
|
ToolbarDropdown,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
ToolbarItemCallbackProps,
|
ToolbarItemCallbackProps,
|
||||||
ToolbarItemResolved,
|
ToolbarItemResolved,
|
||||||
|
ToolbarItemResolvedDropdown,
|
||||||
ToolbarModeName,
|
ToolbarModeName,
|
||||||
} from '@src/lib/toolbar'
|
} from '@src/lib/toolbar'
|
||||||
import { toolbarConfig } from '@src/lib/toolbar'
|
import { isToolbarItemResolvedDropdown, toolbarConfig } from '@src/lib/toolbar'
|
||||||
import { isArray } from '@src/lib/utils'
|
import { isArray } from '@src/lib/utils'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
|
|
||||||
@ -131,21 +133,27 @@ export function Toolbar({
|
|||||||
*/
|
*/
|
||||||
const currentModeItems: (
|
const currentModeItems: (
|
||||||
| ToolbarItemResolved
|
| ToolbarItemResolved
|
||||||
| ToolbarItemResolved[]
|
| ToolbarItemResolvedDropdown
|
||||||
| 'break'
|
| 'break'
|
||||||
)[] = useMemo(() => {
|
)[] = useMemo(() => {
|
||||||
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
|
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
|
||||||
if (maybeIconConfig === 'break') {
|
if (maybeIconConfig === 'break') {
|
||||||
return 'break'
|
return 'break'
|
||||||
} else if (isArray(maybeIconConfig)) {
|
} else if (isToolbarDropdown(maybeIconConfig)) {
|
||||||
return maybeIconConfig.map(resolveItemConfig)
|
return {
|
||||||
|
id: maybeIconConfig.id,
|
||||||
|
array: maybeIconConfig.array.map((item) =>
|
||||||
|
resolveItemConfig(item, maybeIconConfig.id)
|
||||||
|
),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return resolveItemConfig(maybeIconConfig)
|
return resolveItemConfig(maybeIconConfig)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function resolveItemConfig(
|
function resolveItemConfig(
|
||||||
maybeIconConfig: ToolbarItem
|
maybeIconConfig: ToolbarItem,
|
||||||
|
dropdownId?: string
|
||||||
): ToolbarItemResolved {
|
): ToolbarItemResolved {
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
disableAllButtons ||
|
disableAllButtons ||
|
||||||
@ -176,6 +184,14 @@ export function Toolbar({
|
|||||||
}
|
}
|
||||||
}, [currentMode, disableAllButtons, configCallbackProps])
|
}, [currentMode, disableAllButtons, configCallbackProps])
|
||||||
|
|
||||||
|
// To remember the last selected item in an ActionButtonDropdown
|
||||||
|
const [lastSelectedMultiActionItem, _] = useState(
|
||||||
|
new Map<
|
||||||
|
number /* index in currentModeItems */,
|
||||||
|
number /* index in maybeIconConfig */
|
||||||
|
>()
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<menu
|
<menu
|
||||||
data-current-mode={currentMode}
|
data-current-mode={currentMode}
|
||||||
@ -199,24 +215,33 @@ export function Toolbar({
|
|||||||
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
|
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (isArray(maybeIconConfig)) {
|
} else if (isToolbarItemResolvedDropdown(maybeIconConfig)) {
|
||||||
// A button with a dropdown
|
// A button with a dropdown
|
||||||
|
const selectedIcon =
|
||||||
|
maybeIconConfig.array.find((c) => c.isActive) ||
|
||||||
|
maybeIconConfig.array[lastSelectedMultiActionItem.get(i) ?? 0]
|
||||||
|
|
||||||
|
// Save the last selected item in the dropdown
|
||||||
|
lastSelectedMultiActionItem.set(
|
||||||
|
i,
|
||||||
|
maybeIconConfig.array.indexOf(selectedIcon)
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<ActionButtonDropdown
|
<ActionButtonDropdown
|
||||||
Element="button"
|
Element="button"
|
||||||
key={maybeIconConfig[0].id}
|
key={selectedIcon.id}
|
||||||
data-testid={maybeIconConfig[0].id + '-dropdown'}
|
data-testid={selectedIcon.id + '-dropdown'}
|
||||||
id={maybeIconConfig[0].id + '-dropdown'}
|
id={selectedIcon.id + '-dropdown'}
|
||||||
name={maybeIconConfig[0].title}
|
name={maybeIconConfig.id}
|
||||||
className={
|
className={
|
||||||
(maybeIconConfig[0].alwaysDark
|
(maybeIconConfig.array[0].alwaysDark
|
||||||
? 'dark bg-chalkboard-90 '
|
? 'dark bg-chalkboard-90 '
|
||||||
: '!bg-transparent ') +
|
: '!bg-transparent ') +
|
||||||
'group/wrapper ' +
|
'group/wrapper ' +
|
||||||
buttonBorderClassName +
|
buttonBorderClassName +
|
||||||
' relative group !gap-0'
|
' relative group !gap-0'
|
||||||
}
|
}
|
||||||
splitMenuItems={maybeIconConfig.map((itemConfig) => ({
|
splitMenuItems={maybeIconConfig.array.map((itemConfig) => ({
|
||||||
id: itemConfig.id,
|
id: itemConfig.id,
|
||||||
label: itemConfig.title,
|
label: itemConfig.title,
|
||||||
hotkey: itemConfig.hotkey,
|
hotkey: itemConfig.hotkey,
|
||||||
@ -236,11 +261,11 @@ export function Toolbar({
|
|||||||
>
|
>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
id={maybeIconConfig[0].id}
|
id={selectedIcon.id}
|
||||||
data-testid={maybeIconConfig[0].id}
|
data-testid={selectedIcon.id}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: maybeIconConfig[0].icon,
|
icon: selectedIcon.icon,
|
||||||
iconColor: maybeIconConfig[0].iconColor,
|
iconColor: selectedIcon.iconColor,
|
||||||
className: iconClassName,
|
className: iconClassName,
|
||||||
bgClassName: bgClassName,
|
bgClassName: bgClassName,
|
||||||
}}
|
}}
|
||||||
@ -248,40 +273,36 @@ export function Toolbar({
|
|||||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||||
buttonBgClassName
|
buttonBgClassName
|
||||||
}
|
}
|
||||||
aria-pressed={maybeIconConfig[0].isActive}
|
aria-pressed={selectedIcon.isActive}
|
||||||
disabled={
|
disabled={
|
||||||
disableAllButtons ||
|
disableAllButtons ||
|
||||||
maybeIconConfig[0].status !== 'available' ||
|
selectedIcon.status !== 'available' ||
|
||||||
maybeIconConfig[0].disabled
|
selectedIcon.disabled
|
||||||
}
|
}
|
||||||
name={maybeIconConfig[0].title}
|
name={selectedIcon.title}
|
||||||
// aria-description is still in ARIA 1.3 draft.
|
// aria-description is still in ARIA 1.3 draft.
|
||||||
// eslint-disable-next-line jsx-a11y/aria-props
|
// eslint-disable-next-line jsx-a11y/aria-props
|
||||||
aria-description={maybeIconConfig[0].description}
|
aria-description={selectedIcon.description}
|
||||||
onClick={() =>
|
onClick={() => selectedIcon.onClick(configCallbackProps)}
|
||||||
maybeIconConfig[0].onClick(configCallbackProps)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span
|
<span className={!selectedIcon.showTitle ? 'sr-only' : ''}>
|
||||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
{selectedIcon.title}
|
||||||
>
|
|
||||||
{maybeIconConfig[0].title}
|
|
||||||
</span>
|
</span>
|
||||||
<ToolbarItemTooltip
|
<ToolbarItemTooltip
|
||||||
itemConfig={maybeIconConfig[0]}
|
itemConfig={selectedIcon}
|
||||||
configCallbackProps={configCallbackProps}
|
configCallbackProps={configCallbackProps}
|
||||||
wrapperClassName="ui-open:!hidden"
|
wrapperClassName="ui-open:!hidden"
|
||||||
contentClassName={tooltipContentClassName}
|
contentClassName={tooltipContentClassName}
|
||||||
>
|
>
|
||||||
{showRichContent ? (
|
{showRichContent ? (
|
||||||
<ToolbarItemTooltipRichContent
|
<ToolbarItemTooltipRichContent
|
||||||
itemConfig={maybeIconConfig[0]}
|
itemConfig={selectedIcon}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ToolbarItemTooltipShortContent
|
<ToolbarItemTooltipShortContent
|
||||||
status={maybeIconConfig[0].status}
|
status={selectedIcon.status}
|
||||||
title={maybeIconConfig[0].title}
|
title={selectedIcon.title}
|
||||||
hotkey={maybeIconConfig[0].hotkey}
|
hotkey={selectedIcon.hotkey}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ToolbarItemTooltip>
|
</ToolbarItemTooltip>
|
||||||
@ -430,7 +451,9 @@ const ToolbarItemTooltipShortContent = ({
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
{hotkey && (
|
{hotkey && (
|
||||||
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
|
<kbd className="inline-block ml-2 flex-none hotkey">
|
||||||
|
{displayHotkeys(hotkey)}
|
||||||
|
</kbd>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@ -461,7 +484,9 @@ const ToolbarItemTooltipRichContent = ({
|
|||||||
{itemConfig.title}
|
{itemConfig.title}
|
||||||
</span>
|
</span>
|
||||||
{itemConfig.status === 'available' && itemConfig.hotkey ? (
|
{itemConfig.status === 'available' && itemConfig.hotkey ? (
|
||||||
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
|
<kbd className="flex-none hotkey">
|
||||||
|
{displayHotkeys(itemConfig.hotkey)}
|
||||||
|
</kbd>
|
||||||
) : itemConfig.status === 'kcl-only' ? (
|
) : itemConfig.status === 'kcl-only' ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
||||||
@ -522,3 +547,14 @@ const ToolbarItemTooltipRichContent = ({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We don't want to display Esc hotkeys to avoid confusion in the Toolbar UI (eg. "EscR")
|
||||||
|
function displayHotkeys(hotkey: string | string[]) {
|
||||||
|
return (isArray(hotkey) ? hotkey : [hotkey]).filter((h) => h !== 'Esc')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToolbarDropdown(
|
||||||
|
item: ToolbarItem | ToolbarDropdown
|
||||||
|
): item is ToolbarDropdown {
|
||||||
|
return 'array' in item
|
||||||
|
}
|
||||||
|
@ -71,6 +71,7 @@ export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
|
|||||||
<button
|
<button
|
||||||
ref={ref as ForwardedRef<HTMLButtonElement>}
|
ref={ref as ForwardedRef<HTMLButtonElement>}
|
||||||
className={classNames}
|
className={classNames}
|
||||||
|
tabIndex={-1}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{iconStart && <ActionIcon {...iconStart} />}
|
{iconStart && <ActionIcon {...iconStart} />}
|
||||||
|
@ -69,6 +69,7 @@ export function ActionButtonDropdown({
|
|||||||
close()
|
close()
|
||||||
}}
|
}}
|
||||||
className="group/button flex items-center gap-6 px-3 py-1 font-sans text-xs hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
|
className="group/button flex items-center gap-6 px-3 py-1 font-sans text-xs hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
|
||||||
|
tabIndex={-1}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
data-testid={'dropdown-' + item.id}
|
data-testid={'dropdown-' + item.id}
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
import type { CommandArgument } from '@src/lib/commandTypes'
|
import type { CommandArgument } from '@src/lib/commandTypes'
|
||||||
@ -6,6 +7,11 @@ import {
|
|||||||
commandBarActor,
|
commandBarActor,
|
||||||
useCommandBarState,
|
useCommandBarState,
|
||||||
} from '@src/machines/commandBarMachine'
|
} from '@src/machines/commandBarMachine'
|
||||||
|
import type { AnyStateMachine, SnapshotFrom } from 'xstate'
|
||||||
|
|
||||||
|
// TODO: remove the need for this selector once we decouple all actors from React
|
||||||
|
const machineContextSelector = (snapshot?: SnapshotFrom<AnyStateMachine>) =>
|
||||||
|
snapshot?.context
|
||||||
|
|
||||||
function CommandBarBasicInput({
|
function CommandBarBasicInput({
|
||||||
arg,
|
arg,
|
||||||
@ -22,6 +28,19 @@ function CommandBarBasicInput({
|
|||||||
const commandBarState = useCommandBarState()
|
const commandBarState = useCommandBarState()
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const argMachineContext = useSelector(
|
||||||
|
arg.machineActor,
|
||||||
|
machineContextSelector
|
||||||
|
)
|
||||||
|
const defaultValue = useMemo(
|
||||||
|
() =>
|
||||||
|
arg.defaultValue
|
||||||
|
? arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(commandBarState.context, argMachineContext)
|
||||||
|
: arg.defaultValue
|
||||||
|
: '',
|
||||||
|
[arg.defaultValue, commandBarState.context, argMachineContext]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
@ -53,11 +72,7 @@ function CommandBarBasicInput({
|
|||||||
required
|
required
|
||||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
placeholder="Enter a value"
|
placeholder="Enter a value"
|
||||||
defaultValue={
|
defaultValue={defaultValue}
|
||||||
(commandBarState.context.argumentsToSubmit[arg.name] as
|
|
||||||
| string
|
|
||||||
| undefined) || (arg.defaultValue as string)
|
|
||||||
}
|
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Backspace' && event.shiftKey) {
|
if (event.key === 'Backspace' && event.shiftKey) {
|
||||||
stepBack()
|
stepBack()
|
||||||
|
@ -19,7 +19,7 @@ import { useKclContext } from '@src/lang/KclProvider'
|
|||||||
import type { KCLError } from '@src/lang/errors'
|
import type { KCLError } from '@src/lang/errors'
|
||||||
import { kclErrorsByFilename } from '@src/lang/errors'
|
import { kclErrorsByFilename } from '@src/lang/errors'
|
||||||
import { normalizeLineEndings } from '@src/lib/codeEditor'
|
import { normalizeLineEndings } from '@src/lib/codeEditor'
|
||||||
import { FILE_EXT } from '@src/lib/constants'
|
import { FILE_EXT, INSERT_FOREIGN_TOAST_ID } from '@src/lib/constants'
|
||||||
import { sortFilesAndDirectories } from '@src/lib/desktopFS'
|
import { sortFilesAndDirectories } from '@src/lib/desktopFS'
|
||||||
import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
|
import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
|
||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
@ -28,6 +28,9 @@ import { codeManager, kclManager } from '@src/lib/singletons'
|
|||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
|
|
||||||
|
import { ToastInsert } from '@src/components/ToastInsert'
|
||||||
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
import styles from './FileTree.module.css'
|
import styles from './FileTree.module.css'
|
||||||
|
|
||||||
function getIndentationCSS(level: number) {
|
function getIndentationCSS(level: number) {
|
||||||
@ -264,16 +267,26 @@ const FileTreeItem = ({
|
|||||||
if (fileOrDir.children !== null) return // Don't open directories
|
if (fileOrDir.children !== null) return // Don't open directories
|
||||||
|
|
||||||
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
||||||
// Import non-kcl files
|
toast.custom(
|
||||||
// We want to update both the state and editor here.
|
ToastInsert({
|
||||||
codeManager.updateCodeStateEditor(
|
onInsert: () => {
|
||||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
const relativeFilePath = fileOrDir.path.replace(
|
||||||
codeManager.code
|
project.path + window.electron.sep,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
name: 'Insert',
|
||||||
|
groupId: 'code',
|
||||||
|
argDefaultValues: { path: relativeFilePath },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
toast.dismiss(INSERT_FOREIGN_TOAST_ID)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ duration: 30000, id: INSERT_FOREIGN_TOAST_ID }
|
||||||
)
|
)
|
||||||
await codeManager.writeToFile()
|
|
||||||
|
|
||||||
// Prevent seeing the model built one piece at a time when changing files
|
|
||||||
await kclManager.executeCode()
|
|
||||||
} else {
|
} else {
|
||||||
// Let the lsp servers know we closed a file.
|
// Let the lsp servers know we closed a file.
|
||||||
onFileClose(currentFile?.path || null, project?.path || null)
|
onFileClose(currentFile?.path || null, project?.path || null)
|
||||||
|
40
src/components/ToastInsert.tsx
Normal file
40
src/components/ToastInsert.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import { ActionButton } from '@src/components/ActionButton'
|
||||||
|
|
||||||
|
export function ToastInsert({ onInsert }: { onInsert: () => void }) {
|
||||||
|
return (
|
||||||
|
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
|
||||||
|
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||||
|
<p className="text-md">
|
||||||
|
Non-KCL files aren't editable here in Zoo Studio, but you may insert
|
||||||
|
them using the button below or the Insert command.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex justify-between gap-8">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: 'checkmark',
|
||||||
|
}}
|
||||||
|
name="insert"
|
||||||
|
onClick={onInsert}
|
||||||
|
>
|
||||||
|
Insert into my current file
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: 'close',
|
||||||
|
}}
|
||||||
|
name="dismiss"
|
||||||
|
onClick={() => {
|
||||||
|
toast.dismiss()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -86,7 +86,7 @@ textarea,
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
@apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs;
|
@apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs;
|
||||||
@apply focus-visible:outline-chalkboard-100;
|
@apply focus-visible:outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
@ -94,7 +94,7 @@ button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark button {
|
.dark button {
|
||||||
@apply border-chalkboard-70 focus-visible:outline-chalkboard-10;
|
@apply border-chalkboard-70;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark button:hover {
|
.dark button:hover {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { Models } from '@kittycad/lib/dist/types/src'
|
import type { Models } from '@kittycad/lib/dist/types/src'
|
||||||
|
import type { FileImportFormat_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
|
||||||
import type { UnitAngle, UnitLength } from '@rust/kcl-lib/bindings/ModelingCmd'
|
import type { UnitAngle, UnitLength } from '@rust/kcl-lib/bindings/ModelingCmd'
|
||||||
|
|
||||||
@ -37,13 +38,24 @@ export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
|
|||||||
export const DEFAULT_FILE_NAME = 'Untitled'
|
export const DEFAULT_FILE_NAME = 'Untitled'
|
||||||
/** The file endings that will appear in
|
/** The file endings that will appear in
|
||||||
* the file explorer if found in a project directory */
|
* the file explorer if found in a project directory */
|
||||||
export const RELEVANT_FILE_TYPES = [
|
// TODO: make stp part of this enum as an alias to step
|
||||||
|
// TODO: make glb part of this enum as it is in fact supported
|
||||||
|
export type NativeFileType = 'kcl'
|
||||||
|
export type RelevantFileType =
|
||||||
|
| FileImportFormat_type
|
||||||
|
| NativeFileType
|
||||||
|
| 'stp'
|
||||||
|
| 'glb'
|
||||||
|
export const NATIVE_FILE_TYPE: NativeFileType = 'kcl'
|
||||||
|
export const RELEVANT_FILE_TYPES: RelevantFileType[] = [
|
||||||
'kcl',
|
'kcl',
|
||||||
'fbx',
|
'fbx',
|
||||||
'gltf',
|
'gltf',
|
||||||
'glb',
|
'glb',
|
||||||
'obj',
|
'obj',
|
||||||
'ply',
|
'ply',
|
||||||
|
'sldprt',
|
||||||
|
'stp',
|
||||||
'step',
|
'step',
|
||||||
'stl',
|
'stl',
|
||||||
] as const
|
] as const
|
||||||
@ -131,6 +143,9 @@ export const CREATE_FILE_URL_PARAM = 'create-file'
|
|||||||
/** Toast id for the app auto-updater toast */
|
/** Toast id for the app auto-updater toast */
|
||||||
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||||
|
|
||||||
|
/** Toast id for the insert foreign part toast */
|
||||||
|
export const INSERT_FOREIGN_TOAST_ID = 'insert-foreign-toast'
|
||||||
|
|
||||||
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
|
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
|
||||||
export const KCL_AXIS_X = 'X'
|
export const KCL_AXIS_X = 'X'
|
||||||
export const KCL_AXIS_Y = 'Y'
|
export const KCL_AXIS_Y = 'Y'
|
||||||
|
@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
|
|
||||||
import type { Configuration } from '@rust/kcl-lib/bindings/Configuration'
|
import type { Configuration } from '@rust/kcl-lib/bindings/Configuration'
|
||||||
|
|
||||||
import { listProjects } from '@src/lib/desktop'
|
import { isRelevantFile, listProjects } from '@src/lib/desktop'
|
||||||
import type { DeepPartial } from '@src/lib/types'
|
import type { DeepPartial } from '@src/lib/types'
|
||||||
|
|
||||||
// Mock the electron window global
|
// Mock the electron window global
|
||||||
@ -112,6 +112,41 @@ describe('desktop utilities', () => {
|
|||||||
mockElectron.kittycad.mockResolvedValue({})
|
mockElectron.kittycad.mockResolvedValue({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('isRelevantFile', () => {
|
||||||
|
it('finds supported extension files relevant', () => {
|
||||||
|
expect(isRelevantFile('part.kcl')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.fbx')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.gltf')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.glb')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.obj')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.ply')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.sldprt')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.stp')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.step')).toEqual(true)
|
||||||
|
expect(isRelevantFile('part.stl')).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: we should be lowercasing the extension here to check. .sldprt or .SLDPRT should be supported
|
||||||
|
// But the api doesn't allow it today, so revisit this and the tests once this is done
|
||||||
|
it('finds (now) supported uppercase extension files *not* relevant', () => {
|
||||||
|
expect(isRelevantFile('part.KCL')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.FBX')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.GLTF')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.GLB')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.OBJ')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.PLY')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.SLDPRT')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.STP')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.STEP')).toEqual(false)
|
||||||
|
expect(isRelevantFile('part.STL')).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("doesn't find .docx or .SLDASM relevant", () => {
|
||||||
|
expect(isRelevantFile('paper.docx')).toEqual(false)
|
||||||
|
expect(isRelevantFile('assembly.SLDASM')).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('listProjects', () => {
|
describe('listProjects', () => {
|
||||||
it('does not list .git directories', async () => {
|
it('does not list .git directories', async () => {
|
||||||
const projects = await listProjects(mockConfig)
|
const projects = await listProjects(mockConfig)
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
PROJECT_FOLDER,
|
PROJECT_FOLDER,
|
||||||
PROJECT_IMAGE_NAME,
|
PROJECT_IMAGE_NAME,
|
||||||
PROJECT_SETTINGS_FILE_NAME,
|
PROJECT_SETTINGS_FILE_NAME,
|
||||||
|
RELEVANT_FILE_TYPES,
|
||||||
SETTINGS_FILE_NAME,
|
SETTINGS_FILE_NAME,
|
||||||
TELEMETRY_FILE_NAME,
|
TELEMETRY_FILE_NAME,
|
||||||
TELEMETRY_RAW_FILE_NAME,
|
TELEMETRY_RAW_FILE_NAME,
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
import type { FileEntry, Project } from '@src/lib/project'
|
import type { FileEntry, Project } from '@src/lib/project'
|
||||||
import { err } from '@src/lib/trap'
|
import { err } from '@src/lib/trap'
|
||||||
import type { DeepPartial } from '@src/lib/types'
|
import type { DeepPartial } from '@src/lib/types'
|
||||||
|
import { getInVariableCase } from '@src/lib/utils'
|
||||||
|
|
||||||
export async function renameProjectDirectory(
|
export async function renameProjectDirectory(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
@ -199,16 +201,10 @@ export async function listProjects(
|
|||||||
return projects
|
return projects
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMPORT_FILE_EXTENSIONS = [
|
// TODO: we should be lowercasing the extension here to check. .sldprt or .SLDPRT should be supported
|
||||||
// TODO Use ImportFormat enum
|
// But the api doesn't allow it today, so revisit this and the tests once this is done
|
||||||
'stp',
|
export const isRelevantFile = (filename: string): boolean =>
|
||||||
'glb',
|
RELEVANT_FILE_TYPES.some((ext) => filename.endsWith('.' + ext))
|
||||||
'fbxb',
|
|
||||||
'kcl',
|
|
||||||
]
|
|
||||||
|
|
||||||
const isRelevantFile = (filename: string): boolean =>
|
|
||||||
IMPORT_FILE_EXTENSIONS.some((ext) => filename.endsWith('.' + ext))
|
|
||||||
|
|
||||||
const collectAllFilesRecursiveFrom = async (
|
const collectAllFilesRecursiveFrom = async (
|
||||||
path: string,
|
path: string,
|
||||||
@ -731,3 +727,12 @@ export const writeProjectThumbnailFile = async (
|
|||||||
}
|
}
|
||||||
return window.electron.writeFile(filePath, asArray)
|
return window.electron.writeFile(filePath, asArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPathFilenameInVariableCase(path: string) {
|
||||||
|
// from https://nodejs.org/en/learn/manipulating-files/nodejs-file-paths#example
|
||||||
|
const basenameNoExt = window.electron.path.basename(
|
||||||
|
path,
|
||||||
|
window.electron.path.extname(path)
|
||||||
|
)
|
||||||
|
return getInVariableCase(basenameNoExt)
|
||||||
|
}
|
||||||
|
@ -1,31 +1,17 @@
|
|||||||
import type { Models } from '@kittycad/lib/dist/types/src'
|
|
||||||
import type { Stats } from 'fs'
|
import type { Stats } from 'fs'
|
||||||
import * as fs from 'fs/promises'
|
import * as fs from 'fs/promises'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
import { PROJECT_ENTRYPOINT } from '@src/lib/constants'
|
import {
|
||||||
|
NATIVE_FILE_TYPE,
|
||||||
|
PROJECT_ENTRYPOINT,
|
||||||
|
RELEVANT_FILE_TYPES,
|
||||||
|
type RelevantFileType,
|
||||||
|
} from '@src/lib/constants'
|
||||||
|
|
||||||
// Create a const object with the values
|
const shouldWrapExtension = (extension: string) =>
|
||||||
const FILE_IMPORT_FORMATS = {
|
RELEVANT_FILE_TYPES.includes(extension as RelevantFileType) &&
|
||||||
fbx: 'fbx',
|
extension !== NATIVE_FILE_TYPE
|
||||||
gltf: 'gltf',
|
|
||||||
obj: 'obj',
|
|
||||||
ply: 'ply',
|
|
||||||
sldprt: 'sldprt',
|
|
||||||
step: 'step',
|
|
||||||
stl: 'stl',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// Extract the values into an array
|
|
||||||
const fileImportFormats: Models['FileImportFormat_type'][] =
|
|
||||||
Object.values(FILE_IMPORT_FORMATS)
|
|
||||||
export const allFileImportFormats: string[] = [
|
|
||||||
...fileImportFormats,
|
|
||||||
'stp',
|
|
||||||
'fbxb',
|
|
||||||
'glb',
|
|
||||||
]
|
|
||||||
export const relevantExtensions = ['kcl', ...allFileImportFormats]
|
|
||||||
|
|
||||||
/// Get the current project file from the path.
|
/// Get the current project file from the path.
|
||||||
/// This is used for double-clicking on a file in the file explorer,
|
/// This is used for double-clicking on a file in the file explorer,
|
||||||
@ -83,11 +69,14 @@ export default async function getCurrentProjectFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the extension on what we are trying to open is a relevant file type.
|
// Check if the extension on what we are trying to open is a relevant file type.
|
||||||
const extension = path.extname(sourcePath).slice(1)
|
const extension = path.extname(sourcePath).slice(1).toLowerCase()
|
||||||
|
|
||||||
if (!relevantExtensions.includes(extension) && extension !== 'toml') {
|
if (
|
||||||
|
!RELEVANT_FILE_TYPES.includes(extension as RelevantFileType) &&
|
||||||
|
extension !== 'toml'
|
||||||
|
) {
|
||||||
return new Error(
|
return new Error(
|
||||||
`File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${relevantExtensions.join(
|
`File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${RELEVANT_FILE_TYPES.join(
|
||||||
', '
|
', '
|
||||||
)}`
|
)}`
|
||||||
)
|
)
|
||||||
@ -99,7 +88,9 @@ export default async function getCurrentProjectFile(
|
|||||||
|
|
||||||
// If we got an import model file, we need to check if we have a file in the project for
|
// If we got an import model file, we need to check if we have a file in the project for
|
||||||
// this import model.
|
// this import model.
|
||||||
if (allFileImportFormats.includes(extension)) {
|
// TODO: once we have some sort of a load file into project it would make sense to stop creating these wrapper files
|
||||||
|
// and let people save their own kcl file importing
|
||||||
|
if (shouldWrapExtension(extension)) {
|
||||||
const importFileName = path.basename(sourcePath)
|
const importFileName = path.basename(sourcePath)
|
||||||
// Check if we have a file in the project for this import model.
|
// Check if we have a file in the project for this import model.
|
||||||
const kclWrapperFilename = `${importFileName}.kcl`
|
const kclWrapperFilename = `${importFileName}.kcl`
|
||||||
@ -115,7 +106,8 @@ export default async function getCurrentProjectFile(
|
|||||||
// But we recommend you keep the import statement as it is.
|
// But we recommend you keep the import statement as it is.
|
||||||
// For more information on the import statement, see the documentation at:
|
// For more information on the import statement, see the documentation at:
|
||||||
// https://zoo.dev/docs/kcl/import
|
// https://zoo.dev/docs/kcl/import
|
||||||
const model = import("${importFileName}")`
|
import "${importFileName}" as model
|
||||||
|
model`
|
||||||
await fs.writeFile(kclWrapperFilePath, content)
|
await fs.writeFile(kclWrapperFilePath, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,12 +17,14 @@ import {
|
|||||||
EXECUTION_TYPE_REAL,
|
EXECUTION_TYPE_REAL,
|
||||||
FILE_EXT,
|
FILE_EXT,
|
||||||
} from '@src/lib/constants'
|
} from '@src/lib/constants'
|
||||||
|
import { getPathFilenameInVariableCase } from '@src/lib/desktop'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { copyFileShareLink } from '@src/lib/links'
|
import { copyFileShareLink } from '@src/lib/links'
|
||||||
import { baseUnitsUnion } from '@src/lib/settings/settingsTypes'
|
import { baseUnitsUnion } from '@src/lib/settings/settingsTypes'
|
||||||
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
||||||
import { err, reportRejection } from '@src/lib/trap'
|
import { err, reportRejection } from '@src/lib/trap'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
|
import type { CommandBarContext } from '@src/machines/commandBarMachine'
|
||||||
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
||||||
|
|
||||||
interface OnSubmitProps {
|
interface OnSubmitProps {
|
||||||
@ -122,6 +124,14 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
|||||||
localName: {
|
localName: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
|
defaultValue: (context: CommandBarContext) => {
|
||||||
|
if (!context.argumentsToSubmit['path']) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = context.argumentsToSubmit['path'] as string
|
||||||
|
return getPathFilenameInVariableCase(path)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onSubmit: (data) => {
|
onSubmit: (data) => {
|
||||||
|
@ -27,12 +27,31 @@ function userCall(name: string): Operation {
|
|||||||
sourceRange: defaultSourceRange(),
|
sourceRange: defaultSourceRange(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function userReturn(): Operation {
|
function userReturn(): Operation {
|
||||||
return {
|
return {
|
||||||
type: 'GroupEnd',
|
type: 'GroupEnd',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moduleBegin(name: string): Operation {
|
||||||
|
return {
|
||||||
|
type: 'GroupBegin',
|
||||||
|
group: {
|
||||||
|
type: 'ModuleInstance',
|
||||||
|
name,
|
||||||
|
moduleId: 0,
|
||||||
|
},
|
||||||
|
sourceRange: defaultSourceRange(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moduleEnd(): Operation {
|
||||||
|
return {
|
||||||
|
type: 'GroupEnd',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('operations filtering', () => {
|
describe('operations filtering', () => {
|
||||||
it('drops stdlib operations inside a user-defined function call', async () => {
|
it('drops stdlib operations inside a user-defined function call', async () => {
|
||||||
const operations = [
|
const operations = [
|
||||||
@ -65,6 +84,25 @@ describe('operations filtering', () => {
|
|||||||
const actual = filterOperations(operations)
|
const actual = filterOperations(operations)
|
||||||
expect(actual).toEqual([stdlib('std1'), stdlib('std2'), stdlib('std3')])
|
expect(actual).toEqual([stdlib('std1'), stdlib('std2'), stdlib('std3')])
|
||||||
})
|
})
|
||||||
|
it('does not drop module instances that contain no operations', async () => {
|
||||||
|
const operations = [
|
||||||
|
stdlib('std1'),
|
||||||
|
moduleBegin('foo'),
|
||||||
|
moduleEnd(),
|
||||||
|
stdlib('std2'),
|
||||||
|
moduleBegin('bar'),
|
||||||
|
moduleEnd(),
|
||||||
|
stdlib('std3'),
|
||||||
|
]
|
||||||
|
const actual = filterOperations(operations)
|
||||||
|
expect(actual).toEqual([
|
||||||
|
stdlib('std1'),
|
||||||
|
moduleBegin('foo'),
|
||||||
|
stdlib('std2'),
|
||||||
|
moduleBegin('bar'),
|
||||||
|
stdlib('std3'),
|
||||||
|
])
|
||||||
|
})
|
||||||
it('preserves user-defined function calls at the end of the list', async () => {
|
it('preserves user-defined function calls at the end of the list', async () => {
|
||||||
const operations = [stdlib('std1'), userCall('foo')]
|
const operations = [stdlib('std1'), userCall('foo')]
|
||||||
const actual = filterOperations(operations)
|
const actual = filterOperations(operations)
|
||||||
|
@ -1168,7 +1168,7 @@ export function filterOperations(operations: Operation[]): Operation[] {
|
|||||||
* for use in the feature tree UI
|
* for use in the feature tree UI
|
||||||
*/
|
*/
|
||||||
const operationFilters = [
|
const operationFilters = [
|
||||||
isNotGroupWithNoOperations,
|
isNotUserFunctionWithNoOperations,
|
||||||
isNotInsideGroup,
|
isNotInsideGroup,
|
||||||
isNotGroupEnd,
|
isNotGroupEnd,
|
||||||
]
|
]
|
||||||
@ -1202,22 +1202,28 @@ function isNotInsideGroup(operations: Operation[]): Operation[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A filter to exclude GroupBegin operations and their corresponding GroupEnd
|
* A filter to exclude GroupBegin operations and their corresponding GroupEnd
|
||||||
* that don't have any operations inside them from a list of operations.
|
* that don't have any operations inside them from a list of operations, if it's
|
||||||
|
* a function call.
|
||||||
*/
|
*/
|
||||||
function isNotGroupWithNoOperations(operations: Operation[]): Operation[] {
|
function isNotUserFunctionWithNoOperations(
|
||||||
|
operations: Operation[]
|
||||||
|
): Operation[] {
|
||||||
return operations.filter((op, index) => {
|
return operations.filter((op, index) => {
|
||||||
if (
|
if (
|
||||||
op.type === 'GroupBegin' &&
|
op.type === 'GroupBegin' &&
|
||||||
// If this is a begin at the end of the array, it's preserved.
|
op.group.type === 'FunctionCall' &&
|
||||||
|
// If this is a "begin" at the end of the array, it's preserved.
|
||||||
index < operations.length - 1 &&
|
index < operations.length - 1 &&
|
||||||
operations[index + 1].type === 'GroupEnd'
|
operations[index + 1].type === 'GroupEnd'
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
|
const previousOp = index > 0 ? operations[index - 1] : undefined
|
||||||
if (
|
if (
|
||||||
op.type === 'GroupEnd' &&
|
op.type === 'GroupEnd' &&
|
||||||
// If this is an end at the beginning of the array, it's preserved.
|
// If this is an "end" at the beginning of the array, it's preserved.
|
||||||
index > 0 &&
|
previousOp !== undefined &&
|
||||||
operations[index - 1].type === 'GroupBegin'
|
previousOp.type === 'GroupBegin' &&
|
||||||
|
previousOp.group.type === 'FunctionCall'
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
1163
src/lib/toolbar.ts
1163
src/lib/toolbar.ts
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange'
|
import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange'
|
||||||
import { topLevelRange } from '@src/lang/util'
|
import { topLevelRange } from '@src/lang/util'
|
||||||
import {
|
import {
|
||||||
|
getInVariableCase,
|
||||||
hasDigitsLeftOfDecimal,
|
hasDigitsLeftOfDecimal,
|
||||||
hasLeadingZero,
|
hasLeadingZero,
|
||||||
isClockwise,
|
isClockwise,
|
||||||
@ -1308,3 +1309,24 @@ describe('testing isClockwise', () => {
|
|||||||
expect(isClockwise(counterClockwiseTriangle)).toBe(true)
|
expect(isClockwise(counterClockwiseTriangle)).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('testing getInVariableCase', () => {
|
||||||
|
it('properly parses cylinder into cylinder', () => {
|
||||||
|
expect(getInVariableCase('cylinder')).toBe('cylinder')
|
||||||
|
})
|
||||||
|
it('properly parses my-ugly_Cased_Part123 into myUglyCasedPart', () => {
|
||||||
|
expect(getInVariableCase('my-ugly_Cased_Part123')).toBe(
|
||||||
|
'myUglyCasedPart123'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('properly parses PascalCase into pascalCase', () => {
|
||||||
|
expect(getInVariableCase('PascalCase')).toBe('pascalCase')
|
||||||
|
})
|
||||||
|
it('properly parses my/File/Path into myFilePath', () => {
|
||||||
|
expect(getInVariableCase('my/File/Path')).toBe('myFilePath')
|
||||||
|
})
|
||||||
|
it('properly parses prefixes 1120t74-pipe.step', () => {
|
||||||
|
expect(getInVariableCase('1120t74-pipe')).toBe('m1120T74Pipe')
|
||||||
|
expect(getInVariableCase('1120t74-pipe', 'p')).toBe('p1120T74Pipe')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -473,3 +473,25 @@ export function binaryToUuid(
|
|||||||
export function getModuleId(sourceRange: SourceRange) {
|
export function getModuleId(sourceRange: SourceRange) {
|
||||||
return sourceRange[2]
|
return sourceRange[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInVariableCase(name: string, prefixIfDigit = 'm') {
|
||||||
|
// As of 2025-04-08, standard case for KCL variables is camelCase
|
||||||
|
const startsWithANumber = !Number.isNaN(Number(name.charAt(0)))
|
||||||
|
const paddedName = startsWithANumber ? `${prefixIfDigit}${name}` : name
|
||||||
|
|
||||||
|
// From https://www.30secondsofcode.org/js/s/string-case-conversion/#word-boundary-identification
|
||||||
|
const r = /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g
|
||||||
|
const boundaryIdentification = paddedName.match(r)
|
||||||
|
if (!boundaryIdentification) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const likelyPascalCase = boundaryIdentification
|
||||||
|
.map((x) => x.slice(0, 1).toUpperCase() + x.slice(1).toLowerCase())
|
||||||
|
.join('')
|
||||||
|
if (!likelyPascalCase) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return likelyPascalCase.slice(0, 1).toLowerCase() + likelyPascalCase.slice(1)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user