Compare commits

..

48 Commits

Author SHA1 Message Date
11ac71fbe7 start of sketch on chamfers that are on sketch on face extrusions 2024-09-23 22:58:23 +10:00
0615d92a98 Merge branch 'main' into kurt-sketch-on-chamfer 2024-09-23 21:31:29 +10:00
ebd03daa21 fix click selections for chamfers, add tests 2024-09-23 21:29:52 +10:00
63b996a9ed fix test 2024-09-23 16:34:19 +10:00
max
7848d63177 add multiple selections support for focusPath (#3944) 2024-09-23 08:07:31 +02:00
04af2937d1 clean up 2024-09-23 15:31:38 +10:00
e828cdda6d sketch on chamfer tests 2024-09-23 15:15:10 +10:00
ab8d83e32e step app from getting in weird state when selection face to sketch on 2024-09-23 10:26:46 +10:00
57b30c2d66 Merge remote-tracking branch 'origin' into kurt-sketch-on-chamfer 2024-09-22 21:11:49 +10:00
619b059ae1 Bump vite from 5.4.3 to 5.4.6 (#3911) 2024-09-22 02:06:09 +00:00
429fc3eb1b Release KCL 0.2.17 (#3945) 2024-09-21 16:02:25 -05:00
615f661cbb Bump express from 4.19.2 to 4.21.0 (#3929) 2024-09-21 11:26:37 -04:00
6e0675cfda Cut release v0.25.3 (#3942) 2024-09-21 23:31:00 +10:00
3e79b90884 Use kittycad-modeling-cmds from crates.io (#3939) 2024-09-20 16:33:38 -05:00
5a0a635995 No more empty outputs from modeling API (#3931)
* WIP: No more empty outputs from modeling API

Part of https://github.com/KittyCAD/modeling-api/issues/518

* Remove unused import

* Keep Empty in the API

* Fix TS type error due to upgrade to ts_rs 10.0.0

* Fix warning about unused use

* Fix more type errors from ts_rs upgrade

* De-flake settings override desktop test

* Update export test file sizes

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-09-20 16:28:52 -05:00
93d9b10e11 Bump bson from 2.12.0 to 2.13.0 in /src/wasm-lib (#3933)
Bumps [bson](https://github.com/mongodb/bson-rust) from 2.12.0 to 2.13.0.
- [Release notes](https://github.com/mongodb/bson-rust/releases)
- [Commits](https://github.com/mongodb/bson-rust/compare/v2.12.0...v2.13.0)

---
updated-dependencies:
- dependency-name: bson
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 10:21:11 -07:00
166487433c Bump ts-rs from 9.0.1 to 10.0.0 in /src/wasm-lib (#3934)
Bumps [ts-rs](https://github.com/Aleph-Alpha/ts-rs) from 9.0.1 to 10.0.0.
- [Release notes](https://github.com/Aleph-Alpha/ts-rs/releases)
- [Changelog](https://github.com/Aleph-Alpha/ts-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Aleph-Alpha/ts-rs/compare/v9.0.1...v10.0.0)

---
updated-dependencies:
- dependency-name: ts-rs
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 10:20:42 -07:00
5512f99997 Add a test step to confirm the solid bg of the settings dialog (#3927) 2024-09-20 12:04:36 -04:00
22e96dbb88 working 2024-09-20 16:30:19 +10:00
8a08e84ff4 Merge remote-tracking branch 'origin' into kurt-sketch-on-chamfer 2024-09-20 14:11:14 +10:00
01cc9e751b implement from for unit length (#3932)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-19 17:24:09 -07:00
a5aa69badc Merge remote-tracking branch 'origin' into kurt-sketch-on-chamfer 2024-09-20 10:23:28 +10:00
bfac6b7dc8 bump the world (kcl-lib) (#3930)
* bump the world

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit e095d2a990.

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-19 16:31:32 -07:00
d1f9a02ffa fix dumb ass logic bug with edge cuts, actually extends edge cuts array versus overwriting it lolz (#3928)
* dumb ass logic bug with edge cuts

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* change order

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 1ad9eb315e.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)"

This reverts commit 4004c9c1db.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 57d0d05d06.

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-19 14:50:36 -07:00
d8236dd8da Fix zoom callback on camera controls (#3924) 2024-09-19 21:26:27 +00:00
dabf256e2b change to index map, re-fmt, and fillet face id (#3926)
* change to index map, re-fmt, and fillet face id

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-19 21:06:29 +00:00
4285e81001 Nadro/2833/zoom level increase when swapping sketch modes (#3854)
* fix: fixing logic around setting the perspective and position of perspective camera when leaving sketch mode

* fix: typo

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 60b12ffc54.

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)"

This reverts commit 9ab973c6c4.

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* remove unused vars

* fix: fixed the positions of the mouse clicks since the zoom level has changed?

* fix: updating test to make it work with my new zoom level

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-09-19 14:39:32 -05:00
370375c328 Should we bump power precedence? (#3900)
* Should we bump power predecence

* add test

* fix stupid test name
2024-09-19 19:31:41 +10:00
a84f53e2ed sketch on chamfer start 2024-09-19 16:37:35 +10:00
9f22882c68 Add tests that verify folder deletion behavior (#3719)
* Move file tree deletion tests into file-tree.spect.ts

* Move other file tree tests into file-tree.spec.ts

* Add tests that prove we show a deletion confirmation for folders

* Fix lint warning
2024-09-19 15:15:02 +10:00
db5331d9b9 Rename UserVal::set to new to be clearer (#3913) 2024-09-19 00:23:19 +00:00
5cc92f0162 Replace kittycad crate with kittycad-modeling-cmds (#3909)
lib.rs/kittycad-modeling-cmds is the source of our Modeling API. It gets included in our backend APIs, and those APIs generate OpenAPI specs which are read by `openapitor` which generates the lib.rs/kittycad crate. So basically, our modeling app is using the _generated code_ instead of the _handwritten code_.

This sucks -- if you add a new field to the modeling-api crate, you have to merge PRs to the engine, api-deux, and kittycad.rs before finally you can get the new field into the modeling-app. I was pretty embarrased when @mlfarrell asked how to get a new field into the modeling app and had to explain this whole bullshit cycle. Let's fix it.

Switching to use the kittycad-modeling-cmds (aka kcmc) crate directly should speed up our dev cycle.
2024-09-18 17:04:04 -05:00
2978e80226 Bump vite from 5.4.2 to 5.4.3 (#3788)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.2 to 5.4.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-09-18 14:22:00 +10:00
4a74c60150 Shell two at once from the same sketch on face (#3908)
* shell two at once from the same obhject

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-17 18:49:08 -07:00
00fa40bbc9 Bug fix: make dismiss during export not fire success toast (#3882)
* Bug fix: make dismiss during export not fire success toast

* Fix export fail test, since this failure errors early now

* Remove throttling from send side

* Move toast.loading out to when first engine command is sent, so it is shown immediately

* Use shared, named constants for toast messages

* Hook up a couple other error toasts to the `pendingExport.toastId`
2024-09-17 19:06:06 -04:00
max
62b78840b6 Fix canExtrudeSelectionItem and getSelectionType for multiple selections (#3884)
* fix isSketchPipe in canExtrudeSelectionItem

* fix count in getSelectionType

used to count only same type as first selection
2024-09-17 22:32:07 +02:00
f828c36e58 renaming extrude to sweep to generalize the command (#3773)
* fix: just a one liner? forcing a revolve to be an extrude artifact

* fix: first step in renaming ExtrusionArtifact to SweepArtifact

* fix: renaming extrusion to sweep for a few things? need to do another pass

* fix: cleaning up comments, fixing unit tests for new key names

* fix: unit test update

* fix: removing TODO comments that are not needed

* fix: renaming more extrude/extrusion values to sweep

* fix: test:nowatch -u to update the vitests

* Look at this (photo)Graph *in the voice of Nickelback*

* fix: removing TODOs

* fix: forgot to update the extrudeEdge string in other files

* chore: adding e2e test to see if users can sketch on revolved face

* fix: removing garbage string

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-17 13:22:53 -05:00
8c5b146c94 Nadro/3716/mvp revolve (#3728)
* chore: Implemented a executeAst interrupt to stop processing a KCL program

* fix: added a catch since this promise was not being caught

* fix: fmt formatting, need to fix some tsc errors next.

* fix: fixing tsc errors

* fix: cleaning up comment

* fix: only rejecting pending modeling commands

* fix: adding constant for rejection message, adding rejection in WASM send command

* fix: tsc, lint, fmt checks

* feat: first pass over revolve with basic hard coded X axis

* fix: updated revolve status for DEV only

* fix: adding some TODOs to warn others about the Revolve MVP

* fix: fmt, lint, tsc checks

* fix: codespell got me

* fix: xstate v5 upgrade

* fix: removing this fix for a different PR. Not needed for initial MVP

* fix: renaming extrude function to sweep since it fixes extrude and revolve now

* fix: updating selection logic to support revolve

* fix: renaming extrude to sweep since it adds revolve

* fix: swapping as for type in function parameters

* fix: updated from object destruct to structuredClone

* fix: addressing PR comments

* fix: one other typo for return value of revolve
2024-09-17 08:29:52 -05:00
61c7d9844d Make light theme borders more contrasting, update sidebar icons (#3883)
* Make light theme borders more contrasting

* Update icons in sidebar

* fix disabled styles on ActionIcon

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Update src/components/CustomIcon.tsx

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* trigger CI

* fmt

* Update "Make" button test locator to be more specific

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-09-16 17:12:54 -04:00
8d48c17395 Fix: Opposite adjacent edge selection (#3896)
* fix opposite adjacent edge selection

* make test more robust
2024-09-17 05:38:58 +10:00
0ff820d4da Unify execution state into a single struct (#3877)
* Add ExecState that combines ProgramMemory and DynamicState

* Remove unneeded clones

* Add exec_state parameter to all KCL stdlib functions

* Move pipe value into ExecState

* Add test for pipe substitution not leaking into function calls

* KCL: Better message on assertEqual function

Also add a new no-visual test for performance testing.

* Fix new array module to use ExecState

---------

Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-09-16 19:10:33 +00:00
c4ff1c2ef1 Update types.md for constants (#3899)
Update types.md
2024-09-16 11:50:59 -07:00
b6aba2f29c Bump once_cell from 1.19.0 to 1.20.0 in /src/wasm-lib (#3889)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.19.0 to 1.20.0.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.19.0...v1.20.0)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 11:34:58 -07:00
7467f7ea50 Bump pyo3 from 0.22.2 to 0.22.3 in /src/wasm-lib (#3890)
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.22.2 to 0.22.3.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/v0.22.3/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.22.2...v0.22.3)

---
updated-dependencies:
- dependency-name: pyo3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 11:34:36 -07:00
0c6d3e0ccf Bump pretty_assertions from 1.4.0 to 1.4.1 in /src/wasm-lib (#3893)
Bumps [pretty_assertions](https://github.com/rust-pretty-assertions/rust-pretty-assertions) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/rust-pretty-assertions/rust-pretty-assertions/releases)
- [Changelog](https://github.com/rust-pretty-assertions/rust-pretty-assertions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-pretty-assertions/rust-pretty-assertions/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: pretty_assertions
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 11:34:22 -07:00
e82917ea01 Bump anyhow from 1.0.88 to 1.0.89 in /src/wasm-lib (#3892)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.88 to 1.0.89.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.88...1.0.89)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 11:32:54 -07:00
857c1aad3d Bump tokio-tungstenite from 0.23.1 to 0.24.0 in /src/wasm-lib (#3891)
Bumps [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite) from 0.23.1 to 0.24.0.
- [Changelog](https://github.com/snapview/tokio-tungstenite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/snapview/tokio-tungstenite/commits/v0.24.0)

---
updated-dependencies:
- dependency-name: tokio-tungstenite
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 11:31:55 -07:00
dc73acb1b1 KCL: Better message on assertEqual function (#3898)
Also add a new no-visual test for performance testing.
2024-09-16 11:43:49 -05:00
163 changed files with 12898 additions and 5927 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -13,14 +13,16 @@ arrays can hold objects and vice versa.
`true` or `false` work when defining values. `true` or `false` work when defining values.
## Variable declaration ## Constant declaration
Variables are defined with the `let` keyword like so: Constants are defined with the `let` keyword like so:
``` ```
let myBool = false let myBool = false
``` ```
Currently you cannot redeclare a constant.
## Array ## Array
An array is defined with `[]` braces. What is inside the brackets can An array is defined with `[]` braces. What is inside the brackets can

View File

@ -0,0 +1,298 @@
import type { Page, Locator } from '@playwright/test'
import { expect, test as base } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import fsp from 'fs/promises'
import { join } from 'path'
import { uuidv4 } from 'lib/utils'
type CmdBarSerilised =
| {
stage: 'commandBarClosed'
// TODO no more properties needed but needs to be implemented in _serialiseCmdBar
}
| {
stage: 'pickCommand'
// TODO this will need more properties when implemented in _serialiseCmdBar
}
| {
stage: 'arguments'
currentArgKey: string
currentArgValue: string
headerArguments: Record<string, string>
highlightedHeaderArg: string
commandName: string
}
| {
stage: 'review'
headerArguments: Record<string, string>
commandName: string
}
export class AuthenticatedApp {
private readonly exeIndicator: Locator
private readonly diagnosticsTooltip: Locator
private readonly diagnosticsGutterIcon: Locator
private readonly codeContent: Locator
private readonly extrudeButton: Locator
readonly startSketchBtn: Locator
readonly rectangleBtn: Locator
readonly exitSketchBtn: Locator
u: Awaited<ReturnType<typeof getUtils>>
constructor(public readonly page: Page) {
this.codeContent = page.locator('.cm-content')
this.extrudeButton = page.getByTestId('extrude')
this.startSketchBtn = page.getByTestId('sketch')
this.rectangleBtn = page.getByTestId('corner-rectangle')
this.exitSketchBtn = page.getByTestId('sketch-exit')
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
// this.diagnosticsTooltip = page.locator('.cm-tooltip')
this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')
this.u = {} as any
}
async initialise(code = '') {
const u = await getUtils(this.page)
this.u = u
await this.page.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
}, code)
await this.page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName),
'utf-8'
)
}
makeMouseHelpers = (
x: number,
y: number,
{ steps }: { steps: number } = { steps: 5000 }
) => [
() => this.page.mouse.click(x, y),
() => this.page.mouse.move(x, y, { steps }),
]
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
*
* Expects the viewPort to be 1000x500 */
clickNoWhere = () => this.page.mouse.click(998, 60)
// Toolbars
expectExtrudeButtonToBeDisabled = async () =>
await expect(this.extrudeButton).toBeDisabled()
expectExtrudeButtonToBeEnabled = async () =>
await expect(this.extrudeButton).not.toBeDisabled()
clickExtrudeButton = async () => await this.extrudeButton.click()
private _serialiseCmdBar = async (): Promise<CmdBarSerilised> => {
const reviewForm = await this.page.locator('#review-form')
const getHeaderArgs = async () => {
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
const entries = await Promise.all(
inputs.map((input) => {
const key = input
.locator('[data-test-name="arg-name"]')
.innerText()
.then((a) => a.trim())
const value = input
.getByTestId('header-arg-value')
.innerText()
.then((a) => a.trim())
return Promise.all([key, value])
})
)
return Object.fromEntries(entries)
}
const getCommandName = () =>
this.page.getByTestId('command-name').textContent()
if (await reviewForm.isVisible()) {
const [headerArguments, commandName] = await Promise.all([
getHeaderArgs(),
getCommandName(),
])
return {
stage: 'review',
headerArguments,
commandName: commandName || '',
}
}
const [
currentArgKey,
currentArgValue,
headerArguments,
highlightedHeaderArg,
commandName,
] = await Promise.all([
this.page.getByTestId('cmd-bar-arg-name').textContent(),
this.page.getByTestId('cmd-bar-arg-value').textContent(),
getHeaderArgs(),
this.page
.locator('[data-is-current-arg="true"]')
.locator('[data-test-name="arg-name"]')
.textContent(),
getCommandName(),
])
return {
stage: 'arguments',
currentArgKey: currentArgKey || '',
currentArgValue: currentArgValue || '',
headerArguments,
highlightedHeaderArg: highlightedHeaderArg || '',
commandName: commandName || '',
}
}
expectCmdBarToBe = async (expected: CmdBarSerilised) => {
return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
}
progressCmdBar = async () => {
if (Math.random() > 0.5) {
const arrowButton = this.page.getByRole('button', {
name: 'arrow right Continue',
})
if (await arrowButton.isVisible()) {
await arrowButton.click()
} else {
await this.page
.getByRole('button', { name: 'checkmark Submit command' })
.click()
}
} else {
await this.page.keyboard.press('Enter')
}
}
expectCodeHighlightedToBe = async (
code: string,
{ timeout }: { timeout: number } = { timeout: 5000 }
) =>
await expect
.poll(
async () => {
const texts = (
await this.page.getByTestId('hover-highlight').allInnerTexts()
).map((s) => s.replace(/\s+/g, '').trim())
return texts.join('')
},
{ timeout }
)
.toBe(code.replace(/\s+/g, '').trim())
expectActiveLinesToBe = async (lines: Array<string>) => {
await expect
.poll(async () => {
return (await this.page.locator('.cm-activeLine').allInnerTexts()).map(
(l) => l.trim()
)
})
.toEqual(lines.map((l) => l.trim()))
}
private _expectEditorToContain =
(not = false) =>
(
code: string,
{
shouldNormalise = false,
timeout = 5_000,
}: { shouldNormalise?: boolean; timeout?: number } = {}
) => {
if (!shouldNormalise) {
const expectStart = expect(this.codeContent)
if (not) {
return expectStart.not.toContainText(code, { timeout })
}
return expectStart.toContainText(code, { timeout })
}
const normalisedCode = code.replaceAll(/\s+/g, '').trim()
const expectStart = expect.poll(
async () => {
const editorText = await this.codeContent.textContent()
return editorText?.replaceAll(/\s+/g, '').trim()
},
{
timeout,
}
)
if (not) {
return expectStart.not.toContain(normalisedCode)
}
return expectStart.toContain(normalisedCode)
}
expectEditor = {
toContain: this._expectEditorToContain(),
not: { toContain: this._expectEditorToContain(true) },
}
moveCameraTo = async (
pos: { x: number; y: number; z: number },
target: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }
) => {
await this.u.openAndClearDebugPanel()
await this.u.doAndWaitForImageDiff(
() =>
this.u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: pos,
center: target,
up: { x: 0, y: 0, z: 1 },
},
}),
300
)
await this.u.closeDebugPanel()
}
waitForExecutionDone = async () => {
await expect(this.exeIndicator).toBeVisible()
}
private _serialiseDiagnostics = async (): Promise<Array<string>> => {
const diagnostics = await this.diagnosticsGutterIcon.all()
const diagnosticsContent: string[] = []
for (const diag of diagnostics) {
await diag.hover()
// await expect(this.diagnosticsTooltip)
const content = await this.diagnosticsTooltip.allTextContents()
diagnosticsContent.push(content.join(''))
}
return [...new Set(diagnosticsContent)].map((d) => d.trim())
}
expectDiagnosticsToBe = async (expected: Array<string>) =>
await expect
.poll(async () => {
const result = await this._serialiseDiagnostics()
return result
})
.toEqual(expected.map((e) => e.trim()))
startSketchPlaneSelection = async () =>
this.u.doAndWaitForImageDiff(() => this.startSketchBtn.click(), 500)
}
export const test = base.extend<{
app: AuthenticatedApp
}>({
app: async ({ page }, use) => {
const authenticatedApp = new AuthenticatedApp(page)
await use(authenticatedApp)
},
})
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
export { expect } from '@playwright/test'

View File

@ -104,7 +104,7 @@ test(
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBe(477481) .toBe(482669)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')

View File

@ -1,6 +1,15 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils' import * as fs from 'fs'
import {
executorInputPath,
getUtils,
setup,
setupElectron,
tearDown,
} from './test-utils'
import { join } from 'path'
import { FILE_EXT } from 'lib/constants'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo) await setup(context, page, testInfo)
@ -277,3 +286,584 @@ test.describe('when using the file tree to', () => {
} }
) )
}) })
test.describe('Renaming in the file tree', () => {
test(
'A file you have open',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'fileToRename.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const checkUnRenamedFS = () => {
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
return fs.existsSync(filePath)
}
const newFileName = 'newFileName'
const checkRenamedFS = () => {
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
return fs.existsSync(filePath)
}
const fileToRename = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
const renamedFile = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) })
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const renameInput = page.getByPlaceholder('fileToRename.kcl')
const codeLocator = page.locator('.cm-content')
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy()
await fileToRename.click()
await expect(projectMenuButton).toContainText('fileToRename.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('circle(')
await u.closeKclCodePanel()
})
await test.step('Rename the file', async () => {
await fileToRename.click({ button: 'right' })
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await renameInput.fill(newFileName)
await page.keyboard.press('Enter')
})
await test.step('Verify the file is renamed', async () => {
await expect(fileToRename).not.toBeAttached()
await expect(renamedFile).toBeVisible()
expect(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy()
})
await test.step('Verify we navigated', async () => {
await expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
const url = page.url()
expect(url).toContain(newFileName)
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
await expect(projectMenuButton).not.toContainText('main.kcl')
expect(url).not.toContain('fileToRename.kcl')
expect(url).not.toContain('main.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('circle(')
})
await electronApp.close()
}
)
test(
'A file you do not have open',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'fileToRename.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const newFileName = 'newFileName'
const checkUnRenamedFS = () => {
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
return fs.existsSync(filePath)
}
const checkRenamedFS = () => {
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
return fs.existsSync(filePath)
}
const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const fileToRename = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
const renamedFile = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: newFileName + FILE_EXT }),
})
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const renameInput = page.getByPlaceholder('fileToRename.kcl')
const codeLocator = page.locator('.cm-content')
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy()
})
await test.step('Rename the file', async () => {
await fileToRename.click({ button: 'right' })
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await renameInput.fill(newFileName)
await page.keyboard.press('Enter')
})
await test.step('Verify the file is renamed', async () => {
await expect(fileToRename).not.toBeAttached()
await expect(renamedFile).toBeVisible()
expect(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy()
})
await test.step('Verify we have not navigated', async () => {
await expect(projectMenuButton).toContainText('main.kcl')
await expect(projectMenuButton).not.toContainText(
newFileName + FILE_EXT
)
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain(newFileName)
expect(url).not.toContain('fileToRename.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('fillet(')
})
await electronApp.close()
}
)
test(
`A folder you're not inside`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToRename = page.getByRole('button', {
name: 'folderToRename',
})
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const originalFolderName = 'folderToRename'
const renameInput = page.getByPlaceholder(originalFolderName)
const newFolderName = 'newFolderName'
const checkUnRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', originalFolderName)
return fs.existsSync(folderPath)
}
const checkRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', newFolderName)
return fs.existsSync(folderPath)
}
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
await u.openFilePanel()
await expect(folderToRename).toBeVisible()
expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy()
})
await test.step('Rename the folder', async () => {
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await renameInput.fill(newFolderName)
await page.keyboard.press('Enter')
})
await test.step('Verify the folder is renamed, and no navigation occurred', async () => {
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
await expect(projectMenuButton).toContainText('main.kcl')
await expect(renamedFolder).toBeVisible()
await expect(folderToRename).not.toBeAttached()
expect(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy()
})
await electronApp.close()
}
)
test(
`A folder you are inside`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToRename = page.getByRole('button', {
name: 'folderToRename',
})
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const originalFolderName = 'folderToRename'
const renameInput = page.getByPlaceholder(originalFolderName)
const newFolderName = 'newFolderName'
const checkUnRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', originalFolderName)
return fs.existsSync(folderPath)
}
const checkRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', newFolderName)
return fs.existsSync(folderPath)
}
await test.step('Open project and navigate into folder', async () => {
await expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
await u.openFilePanel()
await expect(folderToRename).toBeVisible()
await folderToRename.click()
await expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
const newUrl = page.url()
expect(newUrl).toContain('folderToRename')
expect(newUrl).toContain('someFileWithin.kcl')
expect(newUrl).not.toContain('main.kcl')
expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy()
})
await test.step('Rename the folder', async () => {
await page.waitForTimeout(60000)
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await renameInput.fill(newFolderName)
await page.keyboard.press('Enter')
})
await test.step('Verify the folder is renamed, and navigated to new path', async () => {
const urlSnippet = encodeURIComponent(
join(newFolderName, 'someFileWithin.kcl')
)
await page.waitForURL(new RegExp(urlSnippet))
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
await expect(renamedFolder).toBeVisible()
await expect(folderToRename).not.toBeAttached()
// URL is synchronous, so we check the other stuff first
const url = page.url()
expect(url).not.toContain('main.kcl')
expect(url).toContain(newFolderName)
expect(url).toContain('someFileWithin.kcl')
expect(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy()
})
await electronApp.close()
}
)
})
test.describe('Deleting items from the file pane', () => {
test(
`delete file when main.kcl exists, navigate to main.kcl`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const testDir = join(dir, 'testProject')
await fsp.mkdir(testDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(testDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(testDir, 'fileToDelete.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectCard = page.getByText('testProject')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const fileToDelete = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'fileToDelete.kcl' }) })
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and navigate to fileToDelete.kcl', async () => {
await projectCard.click()
await u.waitForPageLoad()
await u.openFilePanel()
await fileToDelete.click()
await u.waitForPageLoad()
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
await u.closeKclCodePanel()
})
await test.step('Delete fileToDelete.kcl', async () => {
await fileToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click()
})
await test.step('Check deletion and navigation', async () => {
await u.waitForPageLoad()
await expect(fileToDelete).not.toBeVisible()
await u.closeFilePanel()
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('circle(')
await expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()
}
)
test.fixme(
'TODO - delete file we have open when main.kcl does not exist',
async () => {}
)
test(
`Delete folder we are not in, don't navigate`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.mkdir(join(dir, 'Test Project', 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectCard = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and open project pane', async () => {
await projectCard.click()
await u.waitForPageLoad()
await expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
})
await test.step('Delete folderToDelete', async () => {
await folderToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click()
})
await test.step('Check deletion and no navigation', async () => {
await expect(folderToDelete).not.toBeAttached()
await expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()
}
)
test(
`Delete folder we are in, navigate to main.kcl`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.mkdir(join(dir, 'Test Project', 'folderToDelete'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'folderToDelete', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectCard = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToDelete = page.getByRole('button', {
name: 'folderToDelete',
})
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and navigate into folderToDelete', async () => {
await projectCard.click()
await u.waitForPageLoad()
await expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await folderToDelete.click()
await expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
})
await test.step('Delete folderToDelete', async () => {
await folderToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click()
})
await test.step('Check deletion and navigation to main.kcl', async () => {
await expect(folderToDelete).not.toBeAttached()
await expect(fileWithinFolder).not.toBeAttached()
await expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()
}
)
test.fixme('TODO - delete folder we are in, with no main.kcl', async () => {})
})

View File

@ -38,7 +38,7 @@ test(
await expect(page.getByText(notFoundText).first()).not.toBeVisible() await expect(page.getByText(notFoundText).first()).not.toBeVisible()
// Find the make button // Find the make button
const makeButton = page.getByRole('button', { name: 'Make' }) const makeButton = page.getByRole('button', { name: 'Make part' })
// Make sure the button is visible but disabled // Make sure the button is visible but disabled
await expect(makeButton).toBeVisible() await expect(makeButton).toBeVisible()
await expect(makeButton).toBeDisabled() await expect(makeButton).toBeDisabled()

View File

@ -0,0 +1,312 @@
import { test, expect, AuthenticatedApp } from './authenticatedAppFixture'
// test file is for testing point an click code gen functionality that's not sketch mode related
test.describe('verify sketch on chamfer works', () => {
const _sketchOnAChamfer =
(app: AuthenticatedApp) =>
async ({
clickCoords,
cameraPos,
cameraTarget,
beforeChamferSnippet,
afterChamferSelectSnippet,
afterRectangle1stClickSnippet,
afterRectangle2ndClickSnippet,
}: {
clickCoords: { x: number; y: number }
cameraPos: { x: number; y: number; z: number }
cameraTarget: { x: number; y: number; z: number }
beforeChamferSnippet: string
afterChamferSelectSnippet: string
afterRectangle1stClickSnippet: string
afterRectangle2ndClickSnippet: string
}) => {
const [clickChamfer] = app.makeMouseHelpers(clickCoords.x, clickCoords.y)
const [rectangle1stClick] = app.makeMouseHelpers(573, 149)
const [rectangle2ndClick, rectangle2ndMove] = app.makeMouseHelpers(
598,
380,
{ steps: 5 }
)
await app.moveCameraTo(cameraPos, cameraTarget)
await test.step('check chamfer selection changes cursor positon', async () => {
await expect(async () => {
// sometimes initial click doesn't register
await clickChamfer()
await app.expectActiveLinesToBe([beforeChamferSnippet.slice(-5)])
}).toPass({ timeout: 40_000, intervals: [500] })
})
await test.step('starting a new and selecting a chamfer should animate to the new sketch and possible break up the initial chamfer if it had one than more tag', async () => {
await app.startSketchPlaneSelection()
await clickChamfer()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
await app.expectEditor.toContain(afterChamferSelectSnippet)
})
await test.step('make sure a basic sketch can be added', async () => {
await app.rectangleBtn.click()
await rectangle1stClick()
await app.expectEditor.toContain(afterRectangle1stClickSnippet)
await app.u.doAndWaitForImageDiff(() => rectangle2ndMove(), 50)
await rectangle2ndClick()
await app.expectEditor.toContain(afterRectangle2ndClickSnippet)
})
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
await app.exitSketchBtn.click()
await app.waitForExecutionDone()
})
await test.step('Check there is no errors after code created in previous steps executes', async () => {
await app.expectDiagnosticsToBe([])
})
}
test('works on all edge selections and can break up multi edges in a chamfer array', async ({
app,
}) => {
test.skip(
process.platform === 'win32',
'Fails on windows in CI, can not be replicated locally on windows.'
)
const file = await app.getInputFile('e2e-can-sketch-on-chamfer.kcl')
await app.initialise(file)
const sketchOnAChamfer = _sketchOnAChamfer(app)
await sketchOnAChamfer({
clickCoords: { x: 570, y: 220 },
cameraPos: { x: 16020, y: -2000, z: 10500 },
cameraTarget: { x: -150, y: -4500, z: -80 },
beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)
chamfer({length:30,tags:[
seg01,
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02),
getOppositeEdge(seg01)
]}, %)`,
afterChamferSelectSnippet:
'const sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
await sketchOnAChamfer({
clickCoords: { x: 690, y: 250 },
cameraPos: { x: 16020, y: -2000, z: 10500 },
cameraTarget: { x: -150, y: -4500, z: -80 },
beforeChamferSnippet: `angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)chamfer({
length: 30,
tags: [
seg01,
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02)
]
}, %)`,
afterChamferSelectSnippet:
'const sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 },
cameraTarget: { x: 8300, y: 1100, z: 4800 },
beforeChamferSnippet: `angledLine([0, 268.43], %, $rectangleSegmentA001)chamfer({
length: 30,
tags: [
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02)
]
}, %)`,
afterChamferSelectSnippet:
'const sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
/// last one
await sketchOnAChamfer({
clickCoords: { x: 620, y: 300 },
cameraPos: { x: -1100, y: -7700, z: 1600 },
cameraTarget: { x: 1450, y: 670, z: 4000 },
beforeChamferSnippet: `chamfer({
length: 30,
tags: [getNextAdjacentEdge(yo)]
}, %)`,
afterChamferSelectSnippet:
'const sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
await test.step('verif at the end of the test that final code is what is expected', async () => {
await app.expectEditor.toContain(
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
const extrude001 = extrude(100, sketch001)
|> chamfer({
length: 30,
tags: [getOppositeEdge(seg01)]
}, %, $seg03)
|> chamfer({ length: 30, tags: [seg01] }, %, $seg04)
|> chamfer({
length: 30,
tags: [getNextAdjacentEdge(seg02)]
}, %, $seg05)
|> chamfer({
length: 30,
tags: [getNextAdjacentEdge(yo)]
}, %, $seg06)
const sketch005 = startSketchOn(extrude001, seg06)
|> startProfileAt([-23.43, 19.69], %)
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch004 = startSketchOn(extrude001, seg05)
|> startProfileAt([82.57, 322.96], %)
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([
segAng(rectangleSegmentA004) - 90,
103.07
], %, $rectangleSegmentB003)
|> angledLine([
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %, $rectangleSegmentC003)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch003 = startSketchOn(extrude001, seg04)
|> startProfileAt([-209.64, 255.28], %)
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96, 254.59], %)
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`,
{ shouldNormalise: true }
)
})
})
test('works on chamfers on sketchOnFace extrudes', async ({ app, page }) => {
test.skip(
process.platform === 'win32',
'Fails on windows in CI, can not be replicated locally on windows.'
)
const file = await app.getInputFile(
'e2e-can-sketch-on-sketchOnFace-chamfers.kcl'
)
await app.initialise(file)
const sketchOnAChamfer = _sketchOnAChamfer(app)
// clickCoords: { x: 627, y: 287 },
await sketchOnAChamfer({
clickCoords: { x: 858, y: 194 },
cameraPos: { x: 8822, y: 1223, z: 9140 },
cameraTarget: { x: 10856, y: -7390, z: 2832 },
beforeChamferSnippet: `chamfer({
length: 18,
tags: [getNextAdjacentEdge(seg01), seg02]
}, %)`,
afterChamferSelectSnippet:
'const sketch005 = startSketchOn(extrude004, seg05)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
await page.waitForTimeout(100)
})
})

View File

@ -12,7 +12,6 @@ import {
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fs from 'fs' import fs from 'fs'
import { join } from 'path' import { join } from 'path'
import { FILE_EXT } from 'lib/constants'
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo) await tearDown(page, testInfo)
@ -204,7 +203,7 @@ test.describe('Can export from electron app', () => {
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBe(477481) .toBe(482669)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')
@ -1371,455 +1370,6 @@ test(
} }
) )
test.describe('Renaming in the file tree', () => {
test(
'A file you have open',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'fileToRename.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const checkUnRenamedFS = () => {
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
return fs.existsSync(filePath)
}
const newFileName = 'newFileName'
const checkRenamedFS = () => {
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
return fs.existsSync(filePath)
}
const fileToRename = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
const renamedFile = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) })
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const renameInput = page.getByPlaceholder('fileToRename.kcl')
const codeLocator = page.locator('.cm-content')
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy()
await fileToRename.click()
await expect(projectMenuButton).toContainText('fileToRename.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('circle(')
await u.closeKclCodePanel()
})
await test.step('Rename the file', async () => {
await fileToRename.click({ button: 'right' })
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await renameInput.fill(newFileName)
await page.keyboard.press('Enter')
})
await test.step('Verify the file is renamed', async () => {
await expect(fileToRename).not.toBeAttached()
await expect(renamedFile).toBeVisible()
expect(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy()
})
await test.step('Verify we navigated', async () => {
await expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
const url = page.url()
expect(url).toContain(newFileName)
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
await expect(projectMenuButton).not.toContainText('main.kcl')
expect(url).not.toContain('fileToRename.kcl')
expect(url).not.toContain('main.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('circle(')
})
await electronApp.close()
}
)
test(
'A file you do not have open',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'fileToRename.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const newFileName = 'newFileName'
const checkUnRenamedFS = () => {
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
return fs.existsSync(filePath)
}
const checkRenamedFS = () => {
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
return fs.existsSync(filePath)
}
const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const fileToRename = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
const renamedFile = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: newFileName + FILE_EXT }),
})
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const renameInput = page.getByPlaceholder('fileToRename.kcl')
const codeLocator = page.locator('.cm-content')
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy()
})
await test.step('Rename the file', async () => {
await fileToRename.click({ button: 'right' })
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await renameInput.fill(newFileName)
await page.keyboard.press('Enter')
})
await test.step('Verify the file is renamed', async () => {
await expect(fileToRename).not.toBeAttached()
await expect(renamedFile).toBeVisible()
expect(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy()
})
await test.step('Verify we have not navigated', async () => {
await expect(projectMenuButton).toContainText('main.kcl')
await expect(projectMenuButton).not.toContainText(
newFileName + FILE_EXT
)
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain(newFileName)
expect(url).not.toContain('fileToRename.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('fillet(')
})
await electronApp.close()
}
)
test(
`A folder you're not inside`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToRename = page.getByRole('button', {
name: 'folderToRename',
})
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const originalFolderName = 'folderToRename'
const renameInput = page.getByPlaceholder(originalFolderName)
const newFolderName = 'newFolderName'
const checkUnRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', originalFolderName)
return fs.existsSync(folderPath)
}
const checkRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', newFolderName)
return fs.existsSync(folderPath)
}
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
await u.openFilePanel()
await expect(folderToRename).toBeVisible()
expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy()
})
await test.step('Rename the folder', async () => {
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await renameInput.fill(newFolderName)
await page.keyboard.press('Enter')
})
await test.step('Verify the folder is renamed, and no navigation occurred', async () => {
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
await expect(projectMenuButton).toContainText('main.kcl')
await expect(renamedFolder).toBeVisible()
await expect(folderToRename).not.toBeAttached()
expect(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy()
})
await electronApp.close()
}
)
test(
`A folder you are inside`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
recursive: true,
})
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const folderToRename = page.getByRole('button', {
name: 'folderToRename',
})
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
const fileWithinFolder = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
})
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const originalFolderName = 'folderToRename'
const renameInput = page.getByPlaceholder(originalFolderName)
const newFolderName = 'newFolderName'
const checkUnRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', originalFolderName)
return fs.existsSync(folderPath)
}
const checkRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', newFolderName)
return fs.existsSync(folderPath)
}
await test.step('Open project and navigate into folder', async () => {
await expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
await u.openFilePanel()
await expect(folderToRename).toBeVisible()
await folderToRename.click()
await expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
const newUrl = page.url()
expect(newUrl).toContain('folderToRename')
expect(newUrl).toContain('someFileWithin.kcl')
expect(newUrl).not.toContain('main.kcl')
expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy()
})
await test.step('Rename the folder', async () => {
await page.waitForTimeout(2000)
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await renameInput.fill(newFolderName)
await page.keyboard.press('Enter')
})
await test.step('Verify the folder is renamed, and navigated to new path', async () => {
const urlSnippet = encodeURIComponent(
join(newFolderName, 'someFileWithin.kcl')
)
await page.waitForURL(new RegExp(urlSnippet))
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
await expect(renamedFolder).toBeVisible()
await expect(folderToRename).not.toBeAttached()
// URL is synchronous, so we check the other stuff first
const url = page.url()
expect(url).not.toContain('main.kcl')
expect(url).toContain(newFolderName)
expect(url).toContain('someFileWithin.kcl')
expect(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy()
})
await electronApp.close()
}
)
})
test.describe('Deleting files from the file pane', () => {
test(
`when main.kcl exists, navigate to main.kcl`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const testDir = join(dir, 'testProject')
await fsp.mkdir(testDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(testDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('basic_fillet_cube_end.kcl'),
join(testDir, 'fileToDelete.kcl')
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectCard = page.getByText('testProject')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const fileToDelete = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'fileToDelete.kcl' }) })
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and navigate to fileToDelete.kcl', async () => {
await projectCard.click()
await u.waitForPageLoad()
await u.openFilePanel()
await fileToDelete.click()
await u.waitForPageLoad()
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
await u.closeKclCodePanel()
})
await test.step('Delete fileToDelete.kcl', async () => {
await fileToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click()
})
await test.step('Check deletion and navigation', async () => {
await u.waitForPageLoad()
await expect(fileToDelete).not.toBeVisible()
await u.closeFilePanel()
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('circle(')
await expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()
}
)
test.fixme('TODO - when main.kcl does not exist', async () => {})
})
test( test(
'Original project name persist after onboarding', 'Original project name persist after onboarding',
{ tag: '@electron' }, { tag: '@electron' },

View File

@ -346,10 +346,7 @@ const sketch001 = startSketchAt([-0, -0])
// Find the toast. // Find the toast.
// Look out for the toast message // Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`) const exportingToastMessage = page.getByText(`Exporting...`)
await expect(exportingToastMessage).toBeVisible()
const errorToastMessage = page.getByText(`Error while exporting`) const errorToastMessage = page.getByText(`Error while exporting`)
await expect(errorToastMessage).toBeVisible()
const engineErrorToastMessage = page.getByText(`Nothing to export`) const engineErrorToastMessage = page.getByText(`Nothing to export`)
await expect(engineErrorToastMessage).toBeVisible() await expect(engineErrorToastMessage).toBeVisible()

View File

@ -40,7 +40,7 @@ test.describe('Sketch tests', () => {
const screwRadius = 3 const screwRadius = 3
const wireRadius = 2 const wireRadius = 2
const wireOffset = 0.5 const wireOffset = 0.5
const screwHole = startSketchOn('XY') const screwHole = startSketchOn('XY')
${startProfileAt1} ${startProfileAt1}
|> arc({ |> arc({
@ -48,7 +48,7 @@ test.describe('Sketch tests', () => {
angle_start: 0, angle_start: 0,
angle_end: 360 angle_end: 360
}, %) }, %)
const part001 = startSketchOn('XY') const part001 = startSketchOn('XY')
${startProfileAt2} ${startProfileAt2}
|> xLine(width * .5, %) |> xLine(width * .5, %)
@ -57,7 +57,7 @@ test.describe('Sketch tests', () => {
|> close(%) |> close(%)
|> hole(screwHole, %) |> hole(screwHole, %)
|> extrude(thickness, %) |> extrude(thickness, %)
const part002 = startSketchOn('-XZ') const part002 = startSketchOn('-XZ')
${startProfileAt3} ${startProfileAt3}
|> xLine(width / 4, %) |> xLine(width / 4, %)
@ -618,19 +618,19 @@ test.describe('Sketch tests', () => {
await u.closeDebugPanel() await u.closeDebugPanel()
await click00r(30, 0) await click00r(30, 0)
codeStr += ` |> startProfileAt([1.53, 0], %)` codeStr += ` |> startProfileAt([2.03, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(30, 0) await click00r(30, 0)
codeStr += ` |> line([1.53, 0], %)` codeStr += ` |> line([2.04, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 30) await click00r(0, 30)
codeStr += ` |> line([0, -1.53], %)` codeStr += ` |> line([0, -2.03], %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(-30, 0) await click00r(-30, 0)
codeStr += ` |> line([-1.53, 0], %)` codeStr += ` |> line([-2.04, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(undefined, undefined) await click00r(undefined, undefined)
@ -954,4 +954,68 @@ const sketch002 = startSketchOn(extrude001, 'END')
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor) await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3) ).toBeLessThan(3)
}) })
test('Can attempt to sketch on revolved face', async ({
page,
browserName,
}) => {
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const lugHeadLength = 0.25
const lugDiameter = 0.5
const lugLength = 2
fn lug = (origin, length, diameter, plane) => {
const lugSketch = startSketchOn(plane)
|> startProfileAt([origin[0] + lugDiameter / 2, origin[1]], %)
|> angledLineOfYLength({ angle: 60, length: lugHeadLength }, %)
|> xLineTo(0 + .001, %)
|> yLineTo(0, %)
|> close(%)
|> revolve({ axis: "Y" }, %)
return lugSketch
}
lug([0, 0], 10, .5, "XY")`
)
})
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
/***
* Test Plan
* Start the sketch mode
* Click the middle of the screen which should click the top face that is revolved
* Wait till you see the line tool be enabled
* Wait till you see the exit sketch enabled
*
* This is supposed to test that you are allowed to go into sketch mode to sketch on a revolved face
*/
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(async () => {
await page.mouse.click(600, 250)
await page.waitForTimeout(1000)
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).toHaveAttribute('aria-pressed', 'true')
}).toPass({ timeout: 40_000, intervals: [1_000] })
})
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -441,6 +441,34 @@ export async function getUtils(page: Page, test_?: typeof test) {
} }
return maxDiff return maxDiff
}, },
getPixelRGBs: async (
coords: { x: number; y: number },
radius: number
): Promise<[number, number, number][]> => {
const buffer = await page.screenshot({
fullPage: true,
})
const screenshot = await PNG.sync.read(buffer)
const pixMultiplier: number = await page.evaluate(
'window.devicePixelRatio'
)
const allCords: [number, number][] = [[coords.x, coords.y]]
for (let i = 1; i < radius; i++) {
allCords.push([coords.x + i, coords.y])
allCords.push([coords.x - i, coords.y])
allCords.push([coords.x, coords.y + i])
allCords.push([coords.x, coords.y - i])
}
return allCords.map(([x, y]) => {
const index =
(screenshot.width * y * pixMultiplier + x * pixMultiplier) * 4 // rbga is 4 channels
return [
screenshot.data[index],
screenshot.data[index + 1],
screenshot.data[index + 2],
]
})
},
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) => doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
new Promise<boolean>((resolve) => { new Promise<boolean>((resolve) => {
;(async () => { ;(async () => {

View File

@ -31,6 +31,8 @@ test.describe('Testing selections', () => {
const xAxisClick = () => const xAxisClick = () =>
page.mouse.click(700, 253).then(() => page.waitForTimeout(100)) page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
const xAxisClickAfterExitingSketch = () =>
page.mouse.click(639, 278).then(() => page.waitForTimeout(100))
const emptySpaceHover = () => const emptySpaceHover = () =>
test.step('Hover over empty space', async () => { test.step('Hover over empty space', async () => {
await page.mouse.move(700, 143, { steps: 5 }) await page.mouse.move(700, 143, { steps: 5 })
@ -44,9 +46,13 @@ test.describe('Testing selections', () => {
) )
}) })
const topHorzSegmentClick = () => const topHorzSegmentClick = () =>
page.mouse.click(709, 290).then(() => page.waitForTimeout(100)) page.mouse
.click(startXPx, 500 - PUR * 20)
.then(() => page.waitForTimeout(100))
const bottomHorzSegmentClick = () => const bottomHorzSegmentClick = () =>
page.mouse.click(767, 396).then(() => page.waitForTimeout(100)) page.mouse
.click(startXPx + PUR * 10, 500 - PUR * 10)
.then(() => page.waitForTimeout(100))
await u.clearCommandLogs() await u.clearCommandLogs()
await expect( await expect(
@ -196,6 +202,8 @@ test.describe('Testing selections', () => {
// select a line, this verifies that sketches in the scene can be selected outside of sketch mode // select a line, this verifies that sketches in the scene can be selected outside of sketch mode
await topHorzSegmentClick() await topHorzSegmentClick()
await xAxisClickAfterExitingSketch()
await page.waitForTimeout(100)
await emptySpaceHover() await emptySpaceHover()
// enter sketch again // enter sketch again
@ -425,7 +433,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|> line([0, 20.03], %) |> line([0, 20.03], %)
|> line([62.61, 0], %, $seg03) |> line([62.61, 0], %, $seg03)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
` `
) )
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
@ -469,7 +477,9 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
await expect(page.getByText('Unable to delete part')).toBeVisible() await expect(page.getByText('Unable to delete part')).toBeVisible()
}) })
test('Hovering over 3d features highlights code', async ({ page }) => { test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engin', async ({
page,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => { await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
localStorage.setItem( localStorage.setItem(
@ -528,11 +538,22 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
const extrusionTop: Coords2d = [800, 240] const extrusionTopCap: Coords2d = [800, 240]
const flatExtrusionFace: Coords2d = [960, 160] const flatExtrusionFace: Coords2d = [960, 160]
const arc: Coords2d = [840, 160] const tangentialArcTo: Coords2d = [840, 160]
const close: Coords2d = [720, 200] const close: Coords2d = [720, 200]
const nothing: Coords2d = [600, 200] const nothing: Coords2d = [600, 200]
const closeEdge: Coords2d = [744, 233]
const closeAdjacentEdge: Coords2d = [743, 277]
const closeOppositeEdge: Coords2d = [687, 169]
const tangentialArcEdge: Coords2d = [811, 142]
const tangentialArcOppositeEdge: Coords2d = [820, 180]
const tangentialArcAdjacentEdge: Coords2d = [688, 123]
const straightSegmentEdge: Coords2d = [819, 369]
const straightSegmentOppositeEdge: Coords2d = [822, 368]
const straightSegmentAdjacentEdge: Coords2d = [893, 165]
await page.mouse.move(nothing[0], nothing[1]) await page.mouse.move(nothing[0], nothing[1])
await page.mouse.click(nothing[0], nothing[1]) await page.mouse.click(nothing[0], nothing[1])
@ -540,26 +561,261 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(200) await page.waitForTimeout(200)
await page.mouse.move(extrusionTop[0], extrusionTop[1]) const checkCodeAtHoverPosition = async (
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() name = '',
await page.mouse.move(nothing[0], nothing[1]) coord: Coords2d,
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible() highlightCode: string,
activeLine = highlightCode
) => {
await test.step(`test selection for: ${name}`, async () => {
const highlightedLocator = page.getByTestId('hover-highlight')
const activeLineLocator = page.locator('.cm-activeLine')
await page.mouse.move(arc[0], arc[1]) await test.step(`hover should highlight correct code, clicking should put the cursor in the right place, and send selection to engine`, async () => {
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() await expect(async () => {
await page.mouse.move(nothing[0], nothing[1]) await page.mouse.move(nothing[0], nothing[1])
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible() await page.mouse.move(coord[0], coord[1])
await expect(highlightedLocator.first()).toBeVisible()
await expect
.poll(async () => {
let textContents = await highlightedLocator.allTextContents()
const textContentsStr = textContents
.join('')
.replace(/\s+/g, '')
console.log(textContentsStr)
return textContentsStr
})
.toBe(highlightCode)
await page.mouse.move(nothing[0], nothing[1])
}).toPass({ timeout: 40_000, intervals: [500] })
})
await test.step(`click should put the cursor in the right place`, async () => {
// await page.mouse.move(nothing[0], nothing[1], { steps: 5 })
// await expect(highlightedLocator.first()).not.toBeVisible()
await page.mouse.click(coord[0], coord[1])
await expect
.poll(async () => {
const activeLines = await activeLineLocator.allInnerTexts()
return activeLines.join('')
})
.toContain(activeLine)
// check pixels near the click location are yellow
})
await test.step(`check the engine agrees with selections`, async () => {
// ultimately the only way we know if the engine agrees with the selection from the FE
// perspective is if it highlights the pixels near where we clicked yellow.
await expect
.poll(async () => {
const RGBs = await u.getPixelRGBs({ x: coord[0], y: coord[1] }, 3)
for (const rgb of RGBs) {
const [r, g, b] = rgb
const RGAverage = (r + g) / 2
const isRedGreenSameIsh = Math.abs(r - g) < 3
const isBlueLessThanRG = RGAverage - b > 45
const isYellowy = isRedGreenSameIsh && isBlueLessThanRG
if (isYellowy) return true
}
return false
})
.toBeTruthy()
await page.mouse.click(nothing[0], nothing[1])
})
})
}
await page.mouse.move(close[0], close[1]) await checkCodeAtHoverPosition(
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() 'extrusionTopCap',
await page.mouse.move(nothing[0], nothing[1]) extrusionTopCap,
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible() 'startProfileAt([20,0],%)',
'startProfileAt([20, 0], %)'
)
await checkCodeAtHoverPosition(
'flatExtrusionFace',
flatExtrusionFace,
`angledLineThatIntersects({angle:3.14,intersectTag:a,offset:0},%)extrude(5+7,%)`,
'}, %)'
)
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1]) await checkCodeAtHoverPosition(
await expect(page.getByTestId('hover-highlight')).toHaveCount(6) // multiple lines 'tangentialArcTo',
await page.mouse.move(nothing[0], nothing[1]) tangentialArcTo,
'tangentialArcTo([13.14+0,13.14],%)extrude(5+7,%)',
'tangentialArcTo([13.14 + 0, 13.14], %)'
)
await checkCodeAtHoverPosition(
'tangentialArcEdge',
tangentialArcEdge,
`tangentialArcTo([13.14+0,13.14],%)`,
'tangentialArcTo([13.14 + 0, 13.14], %)'
)
await checkCodeAtHoverPosition(
'tangentialArcOppositeEdge',
tangentialArcOppositeEdge,
`tangentialArcTo([13.14+0,13.14],%)`,
'tangentialArcTo([13.14 + 0, 13.14], %)'
)
await checkCodeAtHoverPosition(
'tangentialArcAdjacentEdge',
tangentialArcAdjacentEdge,
`tangentialArcTo([13.14+0,13.14],%)`,
'tangentialArcTo([13.14 + 0, 13.14], %)'
)
await checkCodeAtHoverPosition(
'close',
close,
'close(%)extrude(5+7,%)',
'close(%)'
)
await checkCodeAtHoverPosition(
'closeEdge',
closeEdge,
`close(%)`,
'close(%)'
)
await checkCodeAtHoverPosition(
'closeAdjacentEdge',
closeAdjacentEdge,
`close(%)`,
'close(%)'
)
await checkCodeAtHoverPosition(
'closeOppositeEdge',
closeOppositeEdge,
`close(%)`,
'close(%)'
)
await checkCodeAtHoverPosition(
'straightSegmentEdge',
straightSegmentEdge,
`angledLineToY({angle:30,to:11.14},%)`,
'angledLineToY({ angle: 30, to: 11.14 }, %)'
)
await checkCodeAtHoverPosition(
'straightSegmentOppositeEdge',
straightSegmentOppositeEdge,
`angledLineToY({angle:30,to:11.14},%)`,
'angledLineToY({ angle: 30, to: 11.14 }, %)'
)
await checkCodeAtHoverPosition(
'straightSegmentAdjacentEdge',
straightSegmentAdjacentEdge,
`angledLineThatIntersects({angle:3.14,intersectTag:a,offset:0},%)`,
'}, %)'
)
await page.waitForTimeout(200)
await u.removeCurrentCode()
await u.codeLocator.fill(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
const extrude001 = extrude(100, sketch001)
|> chamfer({
length: 30,
tags: [
seg01,
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02),
getOppositeEdge(seg01)
]
}, %)
`)
await expect(
page.getByTestId('model-state-indicator-execution-done')
).toBeVisible()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 16118, y: -1654, z: 5855 },
center: { x: 4915, y: -3893, z: 4874 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible() await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(nothing[0], nothing[1])
const oppositeChamfer: Coords2d = [577, 230]
const baseChamfer: Coords2d = [726, 258]
const adjacentChamfer1: Coords2d = [653, 99]
const adjacentChamfer2: Coords2d = [653, 430]
await checkCodeAtHoverPosition(
'oppositeChamfer',
oppositeChamfer,
`angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)chamfer({length:30,tags:[seg01,getNextAdjacentEdge(yo),getNextAdjacentEdge(seg02),getOppositeEdge(seg01)]},%)`,
'}, %)'
)
await checkCodeAtHoverPosition(
'baseChamfer',
baseChamfer,
`angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)chamfer({length:30,tags:[seg01,getNextAdjacentEdge(yo),getNextAdjacentEdge(seg02),getOppositeEdge(seg01)]},%)`,
'}, %)'
)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: -6414, y: 160, z: 6145 },
center: { x: 5919, y: 1236, z: 5251 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(nothing[0], nothing[1])
await checkCodeAtHoverPosition(
'adjacentChamfer1',
adjacentChamfer1,
`lineTo([profileStartX(%),profileStartY(%)],%,$seg02)chamfer({length:30,tags:[seg01,getNextAdjacentEdge(yo),getNextAdjacentEdge(seg02),getOppositeEdge(seg01)]},%)`,
'}, %)'
)
await checkCodeAtHoverPosition(
'adjacentChamfer2',
adjacentChamfer2,
`angledLine([segAng(rectangleSegmentA001),-segLen(rectangleSegmentA001)],%,$yo)chamfer({length:30,tags:[seg01,getNextAdjacentEdge(yo),getNextAdjacentEdge(seg02),getOppositeEdge(seg01)]},%)`,
'}, %)'
)
}) })
test("Extrude button should be disabled if there's no extrudable geometry when nothing is selected", async ({ test("Extrude button should be disabled if there's no extrudable geometry when nothing is selected", async ({
page, page,
@ -888,7 +1144,7 @@ const extrude001 = extrude(50, sketch001)
|> line([4.95, -8], %) |> line([4.95, -8], %)
|> line([-20.38, -10.12], %) |> line([-20.38, -10.12], %)
|> line([-15.79, 17.08], %) |> line([-15.79, 17.08], %)
fn yohey = (pos) => { fn yohey = (pos) => {
const sketch004 = startSketchOn('XZ') const sketch004 = startSketchOn('XZ')
${extrudeAndEditBlockedInFunction} ${extrudeAndEditBlockedInFunction}
@ -898,7 +1154,7 @@ const extrude001 = extrude(50, sketch001)
|> line([-15.79, 17.08], %) |> line([-15.79, 17.08], %)
return '' return ''
} }
yohey([15.79, -34.6]) yohey([15.79, -34.6])
` `
) )

View File

@ -69,12 +69,15 @@ test.describe('Testing settings', () => {
page, page,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await test.step(`Setup`, async () => {
await u.waitForAuthSkipAppStart() await page.setViewportSize({ width: 1200, height: 500 })
await page await u.waitForAuthSkipAppStart()
.getByRole('button', { name: 'Start Sketch' }) await page
.waitFor({ state: 'visible' }) .getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
})
// Selectors and constants
const paneButtonLocator = page.getByTestId('debug-pane-button') const paneButtonLocator = page.getByTestId('debug-pane-button')
const headingLocator = page.getByRole('heading', { const headingLocator = page.getByRole('heading', {
name: 'Settings', name: 'Settings',
@ -82,11 +85,23 @@ test.describe('Testing settings', () => {
}) })
const inputLocator = page.locator('input[name="modeling-showDebugPanel"]') const inputLocator = page.locator('input[name="modeling-showDebugPanel"]')
// Open the settings modal with the browser keyboard shortcut await test.step('Open settings dialog and set "Show debug panel" to on', async () => {
await page.keyboard.press('ControlOrMeta+Shift+,') await page.keyboard.press('ControlOrMeta+Shift+,')
await expect(headingLocator).toBeVisible()
await expect(headingLocator).toBeVisible() /** Test to close https://github.com/KittyCAD/modeling-app/issues/2713 */
await page.locator('#showDebugPanel').getByText('OffOn').click() await test.step(`Confirm that this dialog has a solid background`, async () => {
await expect
.poll(() => u.getGreatestPixDiff({ x: 600, y: 250 }, [28, 28, 28]), {
timeout: 1000,
message:
'Checking for solid background, should not see default plane colors',
})
.toBeLessThan(15)
})
await page.locator('#showDebugPanel').getByText('OffOn').click()
})
// Close it and open again with keyboard shortcut, while KCL editor is focused // Close it and open again with keyboard shortcut, while KCL editor is focused
// Put the cursor in the editor // Put the cursor in the editor
@ -262,8 +277,6 @@ test.describe('Testing settings', () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Selectors and constants // Selectors and constants
const userThemeColor = '120' const userThemeColor = '120'
const projectThemeColor = '50' const projectThemeColor = '50'
@ -277,7 +290,6 @@ test.describe('Testing settings', () => {
const projectLink = page.getByText('bracket') const projectLink = page.getByText('bracket')
const logoLink = page.getByTestId('app-logo') const logoLink = page.getByTestId('app-logo')
// Open the app and set the user theme color
await test.step('Set user theme color on home', async () => { await test.step('Set user theme color on home', async () => {
await expect(settingsOpenButton).toBeVisible() await expect(settingsOpenButton).toBeVisible()
await settingsOpenButton.click() await settingsOpenButton.click()
@ -296,13 +308,15 @@ test.describe('Testing settings', () => {
await expect(projectSettingsTab).toBeChecked() await expect(projectSettingsTab).toBeChecked()
await themeColorSetting.fill(projectThemeColor) await themeColorSetting.fill(projectThemeColor)
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor) await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click()
}) })
await test.step('Refresh the application and see project setting applied', async () => { await test.step('Refresh the application and see project setting applied', async () => {
// Make sure we're done navigating before we reload
await expect(settingsCloseButton).not.toBeVisible()
await page.reload({ waitUntil: 'domcontentloaded' }) await page.reload({ waitUntil: 'domcontentloaded' })
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor) await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click()
}) })
await test.step(`Navigate back to the home view and see user setting applied`, async () => { await test.step(`Navigate back to the home view and see user setting applied`, async () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.25.2", "version": "0.25.3",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {
@ -183,7 +183,7 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^5.4.2", "vite": "^5.4.6",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

@ -124,7 +124,7 @@ export function Toolbar({
}, [currentMode, disableAllButtons, configCallbackProps]) }, [currentMode, disableAllButtons, configCallbackProps])
return ( return (
<menu className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-20 dark:border-chalkboard-80 border-t-0 shadow-sm"> <menu className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-30 dark:border-chalkboard-80 border-t-0 shadow-sm">
<ul <ul
{...props} {...props}
ref={toolbarButtonsRef} ref={toolbarButtonsRef}

View File

@ -31,6 +31,7 @@ import { reportRejection } from 'lib/trap'
const ORTHOGRAPHIC_CAMERA_SIZE = 20 const ORTHOGRAPHIC_CAMERA_SIZE = 20
const FRAMES_TO_ANIMATE_IN = 30 const FRAMES_TO_ANIMATE_IN = 30
const ORTHOGRAPHIC_MAGIC_FOV = 4
const tempQuaternion = new Quaternion() // just used for maths const tempQuaternion = new Quaternion() // just used for maths
@ -84,7 +85,7 @@ export class CameraControls {
pendingPan: Vector2 | null = null pendingPan: Vector2 | null = null
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
isFovAnimationInProgress = false isFovAnimationInProgress = false
fovBeforeOrtho = 45 perspectiveFovBeforeOrtho = 45
get isPerspective() { get isPerspective() {
return this.camera instanceof PerspectiveCamera return this.camera instanceof PerspectiveCamera
} }
@ -398,7 +399,7 @@ export class CameraControls {
const zoomFudgeFactor = 2280 const zoomFudgeFactor = 2280
distance = zoomFudgeFactor / (this.camera.zoom * 45) distance = zoomFudgeFactor / (this.camera.zoom * 45)
} }
const panSpeed = (distance / 1000 / 45) * this.fovBeforeOrtho const panSpeed = (distance / 1000 / 45) * this.perspectiveFovBeforeOrtho
this.pendingPan.x += -deltaMove.x * panSpeed this.pendingPan.x += -deltaMove.x * panSpeed
this.pendingPan.y += deltaMove.y * panSpeed this.pendingPan.y += deltaMove.y * panSpeed
} }
@ -443,8 +444,19 @@ export class CameraControls {
} }
onMouseWheel = (event: WheelEvent) => { onMouseWheel = (event: WheelEvent) => {
const interaction = this.getInteractionType(event)
if (interaction === 'none') return
event.preventDefault()
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
this.zoomDataFromLastFrame = event.deltaY if (interaction === 'zoom') {
this.zoomDataFromLastFrame = event.deltaY
} else {
// This case will get handled when we add pan and rotate using Apple trackpad.
console.error(
`Unexpected interaction type for engineToClient wheel event: ${interaction}`
)
}
return return
} }
@ -454,8 +466,16 @@ export class CameraControls {
// zoom commands to engine. This means dropping some zoom // zoom commands to engine. This means dropping some zoom
// commands too. // commands too.
// From onMouseMove zoom handling which seems to be really smooth // From onMouseMove zoom handling which seems to be really smooth
this.handleStart() this.handleStart()
this.pendingZoom = 1 + (event.deltaY / window.devicePixelRatio) * 0.001 if (interaction === 'zoom') {
this.pendingZoom = 1 + (event.deltaY / window.devicePixelRatio) * 0.001
} else {
// This case will get handled when we add pan and rotate using Apple trackpad.
console.error(
`Unexpected interaction type for wheel event: ${interaction}`
)
}
this.handleEnd() this.handleEnd()
} }
@ -516,19 +536,15 @@ export class CameraControls {
_usePerspectiveCamera = () => { _usePerspectiveCamera = () => {
const { x: px, y: py, z: pz } = this.camera.position const { x: px, y: py, z: pz } = this.camera.position
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
const zoom = this.camera.zoom
this.camera = this.createPerspectiveCamera() this.camera = this.createPerspectiveCamera()
this.camera.position.set(px, py, pz) this.camera.position.set(px, py, pz)
this.camera.quaternion.set(qx, qy, qz, qw) this.camera.quaternion.set(qx, qy, qz, qw)
const zoomFudgeFactor = 2280
const distance = zoomFudgeFactor / (zoom * this.lastPerspectiveFov)
const direction = new Vector3().subVectors( const direction = new Vector3().subVectors(
this.camera.position, this.camera.position,
this.target this.target
) )
direction.normalize() direction.normalize()
this.camera.position.copy(this.target).addScaledVector(direction, distance)
} }
usePerspectiveCamera = async (forceSend = false) => { usePerspectiveCamera = async (forceSend = false) => {
this._usePerspectiveCamera() this._usePerspectiveCamera()
@ -980,9 +996,9 @@ export class CameraControls {
) )
this.isFovAnimationInProgress = true this.isFovAnimationInProgress = true
let currentFov = this.lastPerspectiveFov let currentFov = this.lastPerspectiveFov
this.fovBeforeOrtho = currentFov this.perspectiveFovBeforeOrtho = currentFov
const targetFov = 4 const targetFov = ORTHOGRAPHIC_MAGIC_FOV
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
let frameWaitOnFinish = 10 let frameWaitOnFinish = 10
@ -1018,9 +1034,9 @@ export class CameraControls {
) )
} }
this.isFovAnimationInProgress = true this.isFovAnimationInProgress = true
const targetFov = this.fovBeforeOrtho // Target FOV for perspective const targetFov = this.perspectiveFovBeforeOrtho // Target FOV for perspective
this.lastPerspectiveFov = 4 this.lastPerspectiveFov = ORTHOGRAPHIC_MAGIC_FOV
let currentFov = 4 let currentFov = ORTHOGRAPHIC_MAGIC_FOV
const initialCameraUp = this.camera.up.clone() const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
@ -1056,9 +1072,8 @@ export class CameraControls {
) )
} }
this.isFovAnimationInProgress = true this.isFovAnimationInProgress = true
const targetFov = this.fovBeforeOrtho // Target FOV for perspective const targetFov = this.perspectiveFovBeforeOrtho // Target FOV for perspective
this.lastPerspectiveFov = 4 let currentFov = ORTHOGRAPHIC_MAGIC_FOV
let currentFov = 4
const initialCameraUp = this.camera.up.clone() const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
@ -1127,7 +1142,7 @@ export class CameraControls {
this.deferReactUpdate(this.reactCameraProperties) this.deferReactUpdate(this.reactCameraProperties)
Object.values(this._camChangeCallbacks).forEach((cb) => cb()) Object.values(this._camChangeCallbacks).forEach((cb) => cb())
} }
getInteractionType = (event: any) => getInteractionType = (event: MouseEvent) =>
_getInteractionType( _getInteractionType(
this.interactionGuards, this.interactionGuards,
event, event,
@ -1235,16 +1250,21 @@ function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
function _getInteractionType( function _getInteractionType(
interactionGuards: MouseGuard, interactionGuards: MouseGuard,
event: any, event: MouseEvent | WheelEvent,
enablePan: boolean, enablePan: boolean,
enableRotate: boolean, enableRotate: boolean,
enableZoom: boolean enableZoom: boolean
): interactionType | 'none' { ): interactionType | 'none' {
let state: interactionType | 'none' = 'none' if (event instanceof WheelEvent) {
if (enablePan && interactionGuards.pan.callback(event)) return 'pan' if (enableZoom && interactionGuards.zoom.scrollCallback(event))
if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate' return 'zoom'
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom' } else {
return state if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
if (enableRotate && interactionGuards.rotate.callback(event))
return 'rotate'
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
}
return 'none'
} }
/** /**

View File

@ -29,8 +29,8 @@ export const ActionIcon = ({
size = 'md', size = 'md',
children, children,
}: ActionIconProps) => { }: ActionIconProps) => {
const computedIconClassName = `h-auto text-inherit dark:text-current !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}` const computedIconClassName = `h-auto text-inherit dark:text-current group-disabled:text-chalkboard-60 group-disabled:text-chalkboard-60 ${iconClassName}`
const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}` const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-80 group-disabled:bg-chalkboard-30 dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
return ( return (
<div <div

View File

@ -681,6 +681,21 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
logs: (
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="logs"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.5 15C6.5 14.1716 5.82843 13.5 5 13.5C4.17157 13.5 3.5 14.1716 3.5 15C3.5 15.8284 4.17157 16.5 5 16.5C5.82843 16.5 6.5 15.8284 6.5 15ZM6.5 10C6.5 9.17157 5.82843 8.5 5 8.5C4.17157 8.5 3.5 9.17157 3.5 10C3.5 10.8284 4.17157 11.5 5 11.5C5.82843 11.5 6.5 10.8284 6.5 10ZM5 3.5C5.82843 3.5 6.5 4.17157 6.5 5C6.5 5.82843 5.82843 6.5 5 6.5C4.17157 6.5 3.5 5.82843 3.5 5C3.5 4.17157 4.17157 3.5 5 3.5ZM8.5 5.5H16.5V4.5H8.5V5.5ZM8.5 10.5H16.5V9.5H8.5V10.5ZM16.5 15.5H8.5V14.5H16.5V15.5Z"
fill="currentColor"
/>
</svg>
),
'make-variable': ( 'make-variable': (
<svg <svg
viewBox="0 0 20 20" viewBox="0 0 20 20"

View File

@ -38,7 +38,7 @@ import {
import { applyConstraintAngleLength } from './Toolbar/setAngleLength' import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { import {
Selections, Selections,
canExtrudeSelection, canSweepSelection,
handleSelectionBatch, handleSelectionBatch,
isSelectionLastLine, isSelectionLastLine,
isRangeInbetweenCharacters, isRangeInbetweenCharacters,
@ -62,8 +62,8 @@ import {
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { Program, parse, recast } from 'lang/wasm' import { Program, parse, recast } from 'lang/wasm'
import { import {
doesSceneHaveSweepableSketch,
getNodePathFromSourceRange, getNodePathFromSourceRange,
hasExtrudableGeometry,
isSingleCursorInPipe, isSingleCursorInPipe,
} from 'lang/queryAst' } from 'lang/queryAst'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
@ -415,20 +415,9 @@ export const ModelingMachineProvider = ({
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
} }
// Artificially delay the export in playwright tests exportFromEngine({
toast format: format,
.promise( }).catch(reportRejection)
exportFromEngine({
format: format,
}),
{
loading: 'Starting print...',
success: 'Started print successfully',
error: 'Error while starting print',
}
)
.catch(reportRejection)
}, },
'Engine export': ({ event }) => { 'Engine export': ({ event }) => {
if (event.type !== 'Export') return if (event.type !== 'Export') return
@ -482,18 +471,9 @@ export const ModelingMachineProvider = ({
format.selection = { type: 'default_scene' } format.selection = { type: 'default_scene' }
} }
toast exportFromEngine({
.promise( format: format as Models['OutputFormat_type'],
exportFromEngine({ }).catch(reportRejection)
format: format as Models['OutputFormat_type'],
}),
{
loading: 'Exporting...',
success: 'Exported successfully',
error: 'Error while exporting',
}
)
.catch(reportRejection)
}, },
'Submit to Text-to-CAD API': ({ event }) => { 'Submit to Text-to-CAD API': ({ event }) => {
if (event.type !== 'Text-to-CAD') return if (event.type !== 'Text-to-CAD') return
@ -528,12 +508,32 @@ export const ModelingMachineProvider = ({
// they have no selection, we should enable the button // they have no selection, we should enable the button
// so they can select the face through the cmdbar // so they can select the face through the cmdbar
// BUT only if there's extrudable geometry // BUT only if there's extrudable geometry
if (hasExtrudableGeometry(kclManager.ast)) return true if (doesSceneHaveSweepableSketch(kclManager.ast)) return true
return false return false
} }
if (!isPipe) return false if (!isPipe) return false
return canExtrudeSelection(selectionRanges) return canSweepSelection(selectionRanges)
},
'has valid revolve selection': ({ context: { selectionRanges } }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected
// TODO: I believe this guard only allows for extruding a single face at a time
const isPipe = isSketchPipe(selectionRanges)
if (
selectionRanges.codeBasedSelections.length === 0 ||
isRangeInbetweenCharacters(selectionRanges) ||
isSelectionLastLine(selectionRanges, codeManager.code)
) {
// they have no selection, we should enable the button
// so they can select the face through the cmdbar
// BUT only if there's extrudable geometry
if (doesSceneHaveSweepableSketch(kclManager.ast)) return true
return false
}
if (!isPipe) return false
return canSweepSelection(selectionRanges)
}, },
'has valid selection for deletion': ({ 'has valid selection for deletion': ({
context: { selectionRanges }, context: { selectionRanges },
@ -571,7 +571,9 @@ export const ModelingMachineProvider = ({
else if (kclManager.ast.body.length === 0) else if (kclManager.ast.body.length === 0)
errorMessage += 'due to Empty Scene' errorMessage += 'due to Empty Scene'
console.error(errorMessage) console.error(errorMessage)
toast.error(errorMessage) toast.error(errorMessage, {
id: kclManager.engineCommandManager.pendingExport?.toastId,
})
return false return false
} }
}, },
@ -603,9 +605,15 @@ export const ModelingMachineProvider = ({
kclManager.ast, kclManager.ast,
input.sketchPathToNode, input.sketchPathToNode,
input.extrudePathToNode, input.extrudePathToNode,
input.cap input.faceInfo
) )
if (trap(sketched)) return Promise.reject(sketched) if (err(sketched)) {
const sketchedError = new Error(
'Incompatible face, please try another'
)
trap(sketchedError)
return Promise.reject(sketchedError)
}
const { modifiedAst, pathToNode: pathToNewSketchNode } = sketched const { modifiedAst, pathToNode: pathToNewSketchNode } = sketched
await kclManager.executeAstMock(modifiedAst) await kclManager.executeAstMock(modifiedAst)

View File

@ -2,7 +2,7 @@
@apply relative z-0 rounded-r max-w-full flex-auto; @apply relative z-0 rounded-r max-w-full flex-auto;
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
@apply bg-chalkboard-10/50 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-20; @apply bg-chalkboard-10/50 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-30;
scroll-margin-block-start: 41px; scroll-margin-block-start: 41px;
} }
@ -19,7 +19,7 @@
@apply z-10 relative rounded-tr; @apply z-10 relative rounded-tr;
@apply flex h-[41px] items-center justify-between gap-2 px-2; @apply flex h-[41px] items-center justify-between gap-2 px-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90; @apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-10 border-b border-chalkboard-20; @apply bg-chalkboard-10 border-b border-chalkboard-30;
} }
:global(.dark) .header { :global(.dark) .header {

View File

@ -1,10 +1,4 @@
import { import { IconDefinition, faBugSlash } from '@fortawesome/free-solid-svg-icons'
IconDefinition,
faBugSlash,
faCode,
faCodeCommit,
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu' import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane' import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
@ -68,7 +62,7 @@ export const sidebarPanes: SidebarPane[] = [
{ {
id: 'code', id: 'code',
title: 'KCL Code', title: 'KCL Code',
icon: faCode, icon: 'code',
Content: KclEditorPane, Content: KclEditorPane,
keybinding: 'Shift + C', keybinding: 'Shift + C',
Menu: KclEditorMenu, Menu: KclEditorMenu,
@ -94,7 +88,7 @@ export const sidebarPanes: SidebarPane[] = [
{ {
id: 'variables', id: 'variables',
title: 'Variables', title: 'Variables',
icon: faSquareRootVariable, icon: 'make-variable',
Content: MemoryPane, Content: MemoryPane,
Menu: MemoryPaneMenu, Menu: MemoryPaneMenu,
keybinding: 'Shift + V', keybinding: 'Shift + V',
@ -102,7 +96,7 @@ export const sidebarPanes: SidebarPane[] = [
{ {
id: 'logs', id: 'logs',
title: 'Logs', title: 'Logs',
icon: faCodeCommit, icon: 'logs',
Content: LogsPane, Content: LogsPane,
keybinding: 'Shift + L', keybinding: 'Shift + L',
}, },

View File

@ -55,7 +55,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
id: 'export', id: 'export',
title: 'Export part', title: 'Export part',
icon: 'floppyDiskArrow', icon: 'floppyDiskArrow',
iconClassName: '!p-0',
keybinding: 'Ctrl + Shift + E', keybinding: 'Ctrl + Shift + E',
action: () => action: () =>
commandBarSend({ commandBarSend({
@ -67,7 +66,6 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
id: 'make', id: 'make',
title: 'Make part', title: 'Make part',
icon: 'printer3d', icon: 'printer3d',
iconClassName: '!p-0',
keybinding: 'Ctrl + Shift + M', keybinding: 'Ctrl + Shift + M',
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
action: async () => { action: async () => {
@ -181,7 +179,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
className={ className={
(context.store?.openPanes.length === 0 ? 'rounded-r ' : '') + (context.store?.openPanes.length === 0 ? 'rounded-r ' : '') +
'relative z-[2] pointer-events-auto p-0 col-start-1 col-span-1 h-fit w-fit flex flex-col ' + 'relative z-[2] pointer-events-auto p-0 col-start-1 col-span-1 h-fit w-fit flex flex-col ' +
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' 'bg-chalkboard-10 border border-solid border-chalkboard-30 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 shadow-sm '
} }
> >
<ul <ul
@ -204,7 +202,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
</ul> </ul>
{filteredActions.length > 0 && ( {filteredActions.length > 0 && (
<> <>
<hr className="w-full border-chalkboard-20 dark:border-chalkboard-80" /> <hr className="w-full border-chalkboard-30 dark:border-chalkboard-80" />
<ul <ul
id="sidebar-actions" id="sidebar-actions"
className="w-fit p-2 flex flex-col gap-2" className="w-fit p-2 flex flex-col gap-2"
@ -292,7 +290,7 @@ function ModelingPaneButton({
return ( return (
<div id={paneConfig.id + '-button-holder'}> <div id={paneConfig.id + '-button-holder'}>
<button <button
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary" className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick} onClick={onClick}
name={paneConfig.title} name={paneConfig.title}
data-testid={paneConfig.id + '-pane-button'} data-testid={paneConfig.id + '-pane-button'}
@ -302,13 +300,9 @@ function ModelingPaneButton({
> >
<ActionIcon <ActionIcon
icon={paneConfig.icon} icon={paneConfig.icon}
className={'p-1 ' + paneConfig.iconClassName || ''} className={paneConfig.iconClassName || ''}
size={paneConfig.iconSize || 'sm'} size={paneConfig.iconSize || 'md'}
iconClassName={ iconClassName={paneIsOpen ? ' !text-chalkboard-10' : ''}
paneIsOpen
? ' !text-chalkboard-10'
: '!text-chalkboard-80 dark:!text-chalkboard-30'
}
bgClassName={ bgClassName={
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent') 'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
} }

View File

@ -260,7 +260,7 @@ export const Stream = () => {
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches({ idle: 'showPlanes' })) return if (state.matches({ idle: 'showPlanes' })) return
if (btnName(e).left) { if (btnName(e.nativeEvent).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sendSelectEventToEngine(e, videoRef.current) sendSelectEventToEngine(e, videoRef.current)
} }

View File

@ -9,14 +9,19 @@ import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections' import { getEventForSelectWithPoint } from 'lib/selections'
import { import {
getCapCodeRef, getCapCodeRef,
getExtrudeEdgeCodeRef, getSweepEdgeCodeRef,
getExtrusionFromSuspectedExtrudeSurface, getSweepFromSuspectedSweepSurface,
getEdgeCuteConsumedCodeRef,
getSolid2dCodeRef, getSolid2dCodeRef,
getWallCodeRef, getWallCodeRef,
getArtifactOfTypes,
SegmentArtifact,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities' import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { CallExpression } from 'lang/wasm'
import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine'
export function useEngineConnectionSubscriptions() { export function useEngineConnectionSubscriptions() {
const { send, context, state } = useModelingContext() const { send, context, state } = useModelingContext()
@ -47,7 +52,7 @@ export function useEngineConnectionSubscriptions() {
if (err(codeRef)) return if (err(codeRef)) return
editorManager.setHighlightRange([codeRef.range]) editorManager.setHighlightRange([codeRef.range])
} else if (artifact?.type === 'wall') { } else if (artifact?.type === 'wall') {
const extrusion = getExtrusionFromSuspectedExtrudeSurface( const extrusion = getSweepFromSuspectedSweepSurface(
data.entity_id, data.entity_id,
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
@ -61,8 +66,8 @@ export function useEngineConnectionSubscriptions() {
? [codeRef.range] ? [codeRef.range]
: [codeRef.range, extrusion.codeRef.range] : [codeRef.range, extrusion.codeRef.range]
) )
} else if (artifact?.type === 'extrudeEdge') { } else if (artifact?.type === 'sweepEdge') {
const codeRef = getExtrudeEdgeCodeRef( const codeRef = getSweepEdgeCodeRef(
artifact, artifact,
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
@ -72,6 +77,17 @@ export function useEngineConnectionSubscriptions() {
editorManager.setHighlightRange([ editorManager.setHighlightRange([
artifact?.codeRef?.range || [0, 0], artifact?.codeRef?.range || [0, 0],
]) ])
} else if (artifact?.type === 'edgeCut') {
const codeRef = artifact.codeRef
const consumedCodeRef = getEdgeCuteConsumedCodeRef(
artifact,
engineCommandManager.artifactGraph
)
editorManager.setHighlightRange(
err(consumedCodeRef)
? [codeRef.range]
: [codeRef.range, consumedCodeRef.range]
)
} else { } else {
editorManager.setHighlightRange([[0, 0]]) editorManager.setHighlightRange([[0, 0]])
} }
@ -172,17 +188,26 @@ export function useEngineConnectionSubscriptions() {
} }
const faceId = planeOrFaceId const faceId = planeOrFaceId
const artifact = engineCommandManager.artifactGraph.get(faceId) const artifact = engineCommandManager.artifactGraph.get(faceId)
const extrusion = getExtrusionFromSuspectedExtrudeSurface( const extrusion = getSweepFromSuspectedSweepSurface(
faceId, faceId,
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return if (
artifact?.type !== 'cap' &&
artifact?.type !== 'wall' &&
!(
artifact?.type === 'edgeCut' && artifact.subType === 'chamfer'
)
)
return
const codeRef = const codeRef =
artifact.type === 'cap' artifact.type === 'cap'
? getCapCodeRef(artifact, engineCommandManager.artifactGraph) ? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
: getWallCodeRef(artifact, engineCommandManager.artifactGraph) : artifact.type === 'wall'
? getWallCodeRef(artifact, engineCommandManager.artifactGraph)
: artifact.codeRef
const faceInfo = await getFaceDetails(faceId) const faceInfo = await getFaceDetails(faceId)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
@ -193,6 +218,72 @@ export function useEngineConnectionSubscriptions() {
err(codeRef) ? [0, 0] : codeRef.range err(codeRef) ? [0, 0] : codeRef.range
) )
const getEdgeCutMeta = (): null | EdgeCutInfo => {
let chamferInfo: {
segment: SegmentArtifact
type: EdgeCutInfo['subType']
} | null = null
if (
artifact?.type === 'edgeCut' &&
artifact.subType === 'chamfer'
) {
const consumedArtifact = getArtifactOfTypes(
{
key: artifact.consumedEdgeId,
types: ['segment', 'sweepEdge'],
},
engineCommandManager.artifactGraph
)
if (err(consumedArtifact)) return null
if (consumedArtifact.type === 'segment') {
chamferInfo = {
type: 'base',
segment: consumedArtifact,
}
} else {
const segment = getArtifactOfTypes(
{ key: consumedArtifact.segId, types: ['segment'] },
engineCommandManager.artifactGraph
)
if (err(segment)) return null
chamferInfo = {
type: consumedArtifact.subType,
segment,
}
}
}
if (!chamferInfo) return null
const segmentCallExpr = getNodeFromPath<CallExpression>(
kclManager.ast,
chamferInfo?.segment.codeRef.pathToNode || [],
'CallExpression'
)
if (err(segmentCallExpr)) return null
if (segmentCallExpr.node.type !== 'CallExpression') return null
const sketchNodeArgs = segmentCallExpr.node.arguments
const tagDeclarator = sketchNodeArgs.find(
({ type }) => type === 'TagDeclarator'
)
if (!tagDeclarator || tagDeclarator.type !== 'TagDeclarator')
return null
return {
type: 'edgeCut',
subType: chamferInfo.type,
tagName: tagDeclarator.value,
}
}
const edgeCutMeta = getEdgeCutMeta()
const _faceInfo: ExtrudeFacePlane['faceInfo'] = edgeCutMeta
? edgeCutMeta
: artifact.type === 'cap'
? {
type: 'cap',
subType: artifact.subType,
}
: { type: 'wall' }
const extrudePathToNode = !err(extrusion) const extrudePathToNode = !err(extrusion)
? getNodePathFromSourceRange( ? getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,
@ -211,7 +302,7 @@ export function useEngineConnectionSubscriptions() {
) as [number, number, number], ) as [number, number, number],
sketchPathToNode, sketchPathToNode,
extrudePathToNode, extrudePathToNode,
cap: artifact.type === 'cap' ? artifact.subType : 'none', faceInfo: _faceInfo,
faceId: faceId, faceId: faceId,
}, },
}) })

View File

@ -416,7 +416,7 @@ export class KclManager {
ast: Program, ast: Program,
execute: boolean, execute: boolean,
optionalParams?: { optionalParams?: {
focusPath?: PathToNode focusPath?: Array<PathToNode>
zoomToFit?: boolean zoomToFit?: boolean
zoomOnRangeAndType?: { zoomOnRangeAndType?: {
range: SourceRange range: SourceRange
@ -435,27 +435,34 @@ export class KclManager {
let returnVal: Selections | undefined = undefined let returnVal: Selections | undefined = undefined
if (optionalParams?.focusPath) { if (optionalParams?.focusPath) {
const _node1 = getNodeFromPath<any>(
astWithUpdatedSource,
optionalParams?.focusPath
)
if (err(_node1)) return Promise.reject(_node1)
const { node } = _node1
const { start, end } = node
if (!start || !end)
return {
selections: undefined,
newAst: astWithUpdatedSource,
}
returnVal = { returnVal = {
codeBasedSelections: [ codeBasedSelections: [],
{ otherSelections: [],
}
for (const path of optionalParams.focusPath) {
const getNodeFromPathResult = getNodeFromPath<any>(
astWithUpdatedSource,
path
)
if (err(getNodeFromPathResult))
return Promise.reject(getNodeFromPathResult)
const { node } = getNodeFromPathResult
const { start, end } = node
if (!start || !end)
return {
selections: undefined,
newAst: astWithUpdatedSource,
}
if (start && end) {
returnVal.codeBasedSelections.push({
type: 'default', type: 'default',
range: [start, end], range: [start, end],
}, })
], }
otherSelections: [],
} }
} }

View File

@ -1766,17 +1766,17 @@ const key = 'c'`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const { nonCodeMeta } = ast const { nonCodeMeta } = ast
expect(nonCodeMeta.nonCodeNodes[0][0]).toEqual(nonCodeMetaInstance) expect(nonCodeMeta.nonCodeNodes[0]?.[0]).toEqual(nonCodeMetaInstance)
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though) // extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
const codeWithExtraStartWhitespace = '\n\n\n' + code const codeWithExtraStartWhitespace = '\n\n\n' + code
const ast2 = parse(codeWithExtraStartWhitespace) const ast2 = parse(codeWithExtraStartWhitespace)
if (err(ast2)) throw ast2 if (err(ast2)) throw ast2
const { nonCodeMeta: nonCodeMeta2 } = ast2 const { nonCodeMeta: nonCodeMeta2 } = ast2
expect(nonCodeMeta2.nonCodeNodes[0][0].value).toStrictEqual( expect(nonCodeMeta2.nonCodeNodes[0]?.[0].value).toStrictEqual(
nonCodeMetaInstance.value nonCodeMetaInstance.value
) )
expect(nonCodeMeta2.nonCodeNodes[0][0].start).not.toBe( expect(nonCodeMeta2.nonCodeNodes[0]?.[0].start).not.toBe(
nonCodeMetaInstance.start nonCodeMetaInstance.start
) )
}) })

View File

@ -410,6 +410,11 @@ describe('testing math operators', () => {
const mem = await exe(code) const mem = await exe(code)
expect(mem.get('myVar')?.value).toBe(5) expect(mem.get('myVar')?.value).toBe(5)
}) })
it('can do power of math', async () => {
const code = 'const myNeg2 = 4 ^ 2 - 3 ^ 2 * 2'
const mem = await exe(code)
expect(mem.get('myNeg2')?.value).toBe(-2)
})
}) })
describe('Testing Errors', () => { describe('Testing Errors', () => {

View File

@ -400,7 +400,7 @@ const sketch001 = startSketchOn(part001, seg01)`)
ast, ast,
sketchPathToNode, sketchPathToNode,
extrudePathToNode, extrudePathToNode,
'end' { type: 'cap', subType: 'end' }
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
const { modifiedAst } = extruded const { modifiedAst } = extruded

View File

@ -41,6 +41,7 @@ import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { SimplifiedArgDetails } from './std/stdTypes' import { SimplifiedArgDetails } from './std/stdTypes'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Program, node: Program,
@ -251,7 +252,7 @@ export function extrudeSketch(
node: Program, node: Program,
pathToNode: PathToNode, pathToNode: PathToNode,
shouldPipe = false, shouldPipe = false,
distance = createLiteral(4) as Expr distance: Expr = createLiteral(4)
): ):
| { | {
modifiedAst: Program modifiedAst: Program
@ -259,7 +260,7 @@ export function extrudeSketch(
pathToExtrudeArg: PathToNode pathToExtrudeArg: PathToNode
} }
| Error { | Error {
const _node = { ...node } const _node = structuredClone(node)
const _node1 = getNodeFromPath(_node, pathToNode) const _node1 = getNodeFromPath(_node, pathToNode)
if (err(_node1)) return _node1 if (err(_node1)) return _node1
const { node: sketchExpression } = _node1 const { node: sketchExpression } = _node1
@ -342,11 +343,107 @@ export function extrudeSketch(
} }
} }
export function revolveSketch(
node: Program,
pathToNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4)
):
| {
modifiedAst: Program
pathToNode: PathToNode
pathToRevolveArg: PathToNode
}
| Error {
const _node = structuredClone(node)
const _node1 = getNodeFromPath(_node, pathToNode)
if (err(_node1)) return _node1
const { node: sketchExpression } = _node1
// determine if sketchExpression is in a pipeExpression or not
const _node2 = getNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(_node2)) return _node2
const { node: pipeExpression } = _node2
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
const _node3 = getNodeFromPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator'
)
if (err(_node3)) return _node3
const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3
const revolveCall = createCallExpressionStdLib('revolve', [
createObjectExpression({
angle: angle,
// TODO: hard coded 'X' axis for revolve MVP, should be changed.
axis: createLiteral('X'),
}),
createIdentifier(variableDeclarator.id.name),
])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...pipeExpression.body, revolveCall]
: [sketchExpression as any, revolveCall]
)
variableDeclarator.init = pipeChain
const pathToRevolveArg: PathToNode = [
...pathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: _node,
pathToNode,
pathToRevolveArg,
}
}
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
const sketchIndexInPathToNode =
pathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = pathToDecleration[sketchIndexInPathToNode][0]
if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number')
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToRevolveArg: PathToNode = [
['body', ''],
[sketchIndexInBody + 1, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: _node,
pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']],
pathToRevolveArg,
}
}
export function sketchOnExtrudedFace( export function sketchOnExtrudedFace(
node: Program, node: Program,
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
extrudePathToNode: PathToNode, extrudePathToNode: PathToNode,
cap: 'none' | 'start' | 'end' = 'none' info: ExtrudeFacePlane['faceInfo'] = { type: 'wall' }
): { modifiedAst: Program; pathToNode: PathToNode } | Error { ): { modifiedAst: Program; pathToNode: PathToNode } | Error {
let _node = { ...node } let _node = { ...node }
const newSketchName = findUniqueName( const newSketchName = findUniqueName(
@ -380,21 +477,22 @@ export function sketchOnExtrudedFace(
const { node: extrudeVarDec } = _node3 const { node: extrudeVarDec } = _node3
const extrudeName = extrudeVarDec.id?.name const extrudeName = extrudeVarDec.id?.name
let _tag = null let _tag
if (cap === 'none') { if (info.type !== 'cap') {
const __tag = addTagForSketchOnFace( const __tag = addTagForSketchOnFace(
{ {
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
node: _node, node: _node,
}, },
expression.callee.name expression.callee.name,
info.type === 'edgeCut' ? info : null
) )
if (err(__tag)) return __tag if (err(__tag)) return __tag
const { modifiedAst, tag } = __tag const { modifiedAst, tag } = __tag
_tag = createIdentifier(tag) _tag = createIdentifier(tag)
_node = modifiedAst _node = modifiedAst
} else { } else {
_tag = createLiteral(cap.toUpperCase()) _tag = createLiteral(info.subType.toUpperCase())
} }
const newSketch = createVariableDeclaration( const newSketch = createVariableDeclaration(

View File

@ -35,7 +35,7 @@ import { Selections, canFilletSelection } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { import {
ArtifactGraph, ArtifactGraph,
getExtrusionFromSuspectedPath, getSweepFromSuspectedPath,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons' import { kclManager, engineCommandManager, editorManager } from 'lib/singletons'
@ -65,7 +65,7 @@ export function modifyAstWithFilletAndTag(
ast: Program, ast: Program,
selection: Selections, selection: Selections,
radius: KclCommandValue radius: KclCommandValue
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error { ): { modifiedAst: Program; pathToFilletNode: Array<PathToNode> } | Error {
const astResult = insertRadiusIntoAst(ast, radius) const astResult = insertRadiusIntoAst(ast, radius)
if (err(astResult)) return astResult if (err(astResult)) return astResult
@ -73,7 +73,8 @@ export function modifyAstWithFilletAndTag(
const artifactGraph = engineCommandManager.artifactGraph const artifactGraph = engineCommandManager.artifactGraph
let clonedAst = structuredClone(ast) let clonedAst = structuredClone(ast)
let lastPathToFilletNode: PathToNode = [] const clonedAstForGetExtrude = structuredClone(ast)
let pathToFilletNodes: Array<PathToNode> = []
for (const selectionRange of selection.codeBasedSelections) { for (const selectionRange of selection.codeBasedSelections) {
const singleSelection = { const singleSelection = {
@ -82,7 +83,7 @@ export function modifyAstWithFilletAndTag(
} }
const getPathToExtrudeForSegmentSelectionResult = const getPathToExtrudeForSegmentSelectionResult =
getPathToExtrudeForSegmentSelection( getPathToExtrudeForSegmentSelection(
clonedAst, clonedAstForGetExtrude,
singleSelection, singleSelection,
programMemory, programMemory,
artifactGraph artifactGraph
@ -101,9 +102,9 @@ export function modifyAstWithFilletAndTag(
if (trap(addFilletResult)) return addFilletResult if (trap(addFilletResult)) return addFilletResult
const { modifiedAst, pathToFilletNode } = addFilletResult const { modifiedAst, pathToFilletNode } = addFilletResult
clonedAst = modifiedAst clonedAst = modifiedAst
lastPathToFilletNode = pathToFilletNode pathToFilletNodes.push(pathToFilletNode)
} }
return { modifiedAst: clonedAst, pathToFilletNode: lastPathToFilletNode } return { modifiedAst: clonedAst, pathToFilletNode: pathToFilletNodes }
} }
function insertRadiusIntoAst( function insertRadiusIntoAst(
@ -152,7 +153,7 @@ export function getPathToExtrudeForSegmentSelection(
) )
if (trap(sketchGroup)) return sketchGroup if (trap(sketchGroup)) return sketchGroup
const extrusion = getExtrusionFromSuspectedPath(sketchGroup.id, artifactGraph) const extrusion = getSweepFromSuspectedPath(sketchGroup.id, artifactGraph)
if (err(extrusion)) return extrusion if (err(extrusion)) return extrusion
const pathToExtrudeNode = getNodePathFromSourceRange( const pathToExtrudeNode = getNodePathFromSourceRange(
@ -166,7 +167,7 @@ export function getPathToExtrudeForSegmentSelection(
async function updateAstAndFocus( async function updateAstAndFocus(
modifiedAst: Program, modifiedAst: Program,
pathToFilletNode: PathToNode pathToFilletNode: Array<PathToNode>
) { ) {
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode, focusPath: pathToFilletNode,
@ -233,7 +234,8 @@ function mutateAstWithTagForSketchSegment(
pathToNode: pathToSegmentNode, pathToNode: pathToSegmentNode,
node: astClone, node: astClone,
}, },
segmentNode.node.callee.name segmentNode.node.callee.name,
null
) )
if (err(taggedSegment)) return taggedSegment if (err(taggedSegment)) return taggedSegment
const { tag } = taggedSegment const { tag } = taggedSegment

View File

@ -8,7 +8,7 @@ import {
hasExtrudeSketchGroup, hasExtrudeSketchGroup,
findUsesOfTagInPipe, findUsesOfTagInPipe,
hasSketchPipeBeenExtruded, hasSketchPipeBeenExtruded,
hasExtrudableGeometry, doesSceneHaveSweepableSketch,
traverse, traverse,
} from './queryAst' } from './queryAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
@ -488,7 +488,7 @@ const sketch002 = startSketchOn(extrude001, $seg01)
}) })
}) })
describe('Testing hasExtrudableGeometry', () => { describe('Testing doesSceneHaveSweepableSketch', () => {
it('finds sketch001 pipe to be extruded', async () => { it('finds sketch001 pipe to be extruded', async () => {
const exampleCode = `const sketch001 = startSketchOn('XZ') const exampleCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([3.29, 7.86], %) |> startProfileAt([3.29, 7.86], %)
@ -506,7 +506,7 @@ const sketch002 = startSketchOn(extrude001, $seg01)
` `
const ast = parse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const extrudable = hasExtrudableGeometry(ast) const extrudable = doesSceneHaveSweepableSketch(ast)
expect(extrudable).toBeTruthy() expect(extrudable).toBeTruthy()
}) })
it('find sketch002 NOT pipe to be extruded', async () => { it('find sketch002 NOT pipe to be extruded', async () => {
@ -520,7 +520,7 @@ const extrude001 = extrude(10, sketch001)
` `
const ast = parse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const extrudable = hasExtrudableGeometry(ast) const extrudable = doesSceneHaveSweepableSketch(ast)
expect(extrudable).toBeFalsy() expect(extrudable).toBeFalsy()
}) })
}) })

View File

@ -880,7 +880,7 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
if ( if (
node.type === 'CallExpression' && node.type === 'CallExpression' &&
node.callee.type === 'Identifier' && node.callee.type === 'Identifier' &&
node.callee.name === 'extrude' && (node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
node.arguments?.[1]?.type === 'Identifier' && node.arguments?.[1]?.type === 'Identifier' &&
node.arguments[1].name === varDec.id.name node.arguments[1].name === varDec.id.name
) { ) {
@ -892,7 +892,7 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
} }
/** File must contain at least one sketch that has not been extruded already */ /** File must contain at least one sketch that has not been extruded already */
export function hasExtrudableGeometry(ast: Program) { export function doesSceneHaveSweepableSketch(ast: Program) {
const theMap: any = {} const theMap: any = {}
traverse(ast as any, { traverse(ast as any, {
enter(node) { enter(node) {
@ -925,7 +925,7 @@ export function hasExtrudableGeometry(ast: Program) {
} }
} else if ( } else if (
node.type === 'CallExpression' && node.type === 'CallExpression' &&
node.callee.name === 'extrude' && (node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
node.arguments[1]?.type === 'Identifier' && node.arguments[1]?.type === 'Identifier' &&
theMap?.[node?.arguments?.[1]?.name] theMap?.[node?.arguments?.[1]?.name]
) { ) {

View File

@ -33,7 +33,6 @@ Map {
70, 70,
], ],
}, },
"extrusionId": "UUID",
"planeId": "UUID", "planeId": "UUID",
"segIds": [ "segIds": [
"UUID", "UUID",
@ -43,6 +42,7 @@ Map {
"UUID", "UUID",
], ],
"solid2dId": "UUID", "solid2dId": "UUID",
"sweepId": "UUID",
"type": "path", "type": "path",
}, },
"UUID-2" => { "UUID-2" => {
@ -175,6 +175,7 @@ Map {
"UUID", "UUID",
], ],
"pathId": "UUID", "pathId": "UUID",
"subType": "extrusion",
"surfaceIds": [ "surfaceIds": [
"UUID", "UUID",
"UUID", "UUID",
@ -183,99 +184,99 @@ Map {
"UUID", "UUID",
"UUID", "UUID",
], ],
"type": "extrusion", "type": "sweep",
}, },
"UUID-9" => { "UUID-9" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"sweepId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-10" => { "UUID-10" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [ "pathIds": [
"UUID", "UUID",
], ],
"segId": "UUID", "segId": "UUID",
"sweepId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-11" => { "UUID-11" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"sweepId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-12" => { "UUID-12" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"sweepId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-13" => { "UUID-13" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"subType": "start", "subType": "start",
"sweepId": "UUID",
"type": "cap", "type": "cap",
}, },
"UUID-14" => { "UUID-14" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"subType": "end", "subType": "end",
"sweepId": "UUID",
"type": "cap", "type": "cap",
}, },
"UUID-15" => { "UUID-15" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-16" => { "UUID-16" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "adjacent",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-17" => { "UUID-17" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-18" => { "UUID-18" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "adjacent",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-19" => { "UUID-19" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-20" => { "UUID-20" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "adjacent",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-21" => { "UUID-21" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-22" => { "UUID-22" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "adjacent",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-23" => { "UUID-23" => {
"codeRef": { "codeRef": {
@ -308,7 +309,6 @@ Map {
395, 395,
], ],
}, },
"extrusionId": "UUID",
"planeId": "UUID", "planeId": "UUID",
"segIds": [ "segIds": [
"UUID", "UUID",
@ -317,6 +317,7 @@ Map {
"UUID", "UUID",
], ],
"solid2dId": "UUID", "solid2dId": "UUID",
"sweepId": "UUID",
"type": "path", "type": "path",
}, },
"UUID-25" => { "UUID-25" => {
@ -425,6 +426,7 @@ Map {
"UUID", "UUID",
], ],
"pathId": "UUID", "pathId": "UUID",
"subType": "extrusion",
"surfaceIds": [ "surfaceIds": [
"UUID", "UUID",
"UUID", "UUID",
@ -432,78 +434,78 @@ Map {
"UUID", "UUID",
"UUID", "UUID",
], ],
"type": "extrusion", "type": "sweep",
}, },
"UUID-31" => { "UUID-31" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"sweepId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-32" => { "UUID-32" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"sweepId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-33" => { "UUID-33" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"segId": "UUID", "segId": "UUID",
"sweepId": "UUID",
"type": "wall", "type": "wall",
}, },
"UUID-34" => { "UUID-34" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"subType": "start", "subType": "start",
"sweepId": "UUID",
"type": "cap", "type": "cap",
}, },
"UUID-35" => { "UUID-35" => {
"edgeCutEdgeIds": [], "edgeCutEdgeIds": [],
"extrusionId": "UUID",
"pathIds": [], "pathIds": [],
"subType": "end", "subType": "end",
"sweepId": "UUID",
"type": "cap", "type": "cap",
}, },
"UUID-36" => { "UUID-36" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-37" => { "UUID-37" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "adjacent",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-38" => { "UUID-38" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-39" => { "UUID-39" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "adjacent",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-40" => { "UUID-40" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "opposite", "subType": "opposite",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
"UUID-41" => { "UUID-41" => {
"extrusionId": "UUID",
"segId": "UUID", "segId": "UUID",
"subType": "adjacent", "subType": "adjacent",
"type": "extrudeEdge", "sweepId": "UUID",
"type": "sweepEdge",
}, },
} }
`; `;

View File

@ -7,7 +7,7 @@ import {
filterArtifacts, filterArtifacts,
expandPlane, expandPlane,
expandPath, expandPath,
expandExtrusion, expandSweep,
ArtifactGraph, ArtifactGraph,
expandSegment, expandSegment,
getArtifactsToUpdate, getArtifactsToUpdate,
@ -194,13 +194,13 @@ describe('testing createArtifactGraph', () => {
}) })
it('there should be two extrusions, for the original and the sketchOnFace, the first extrusion should have 6 sides of the cube', () => { it('there should be two extrusions, for the original and the sketchOnFace, the first extrusion should have 6 sides of the cube', () => {
const extrusions = [ const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map(
...filterArtifacts({ types: ['extrusion'] }, theMap), (extrusion) => expandSweep(extrusion[1], theMap)
].map((extrusion) => expandExtrusion(extrusion[1], theMap)) )
expect(extrusions).toHaveLength(2) expect(extrusions).toHaveLength(2)
extrusions.forEach((extrusion, index) => { extrusions.forEach((extrusion, index) => {
if (err(extrusion)) throw extrusion if (err(extrusion)) throw extrusion
expect(extrusion.type).toBe('extrusion') expect(extrusion.type).toBe('sweep')
const firstExtrusionIsACubeIE6Sides = 6 const firstExtrusionIsACubeIE6Sides = 6
const secondExtrusionIsATriangularPrismIE5Sides = 5 const secondExtrusionIsATriangularPrismIE5Sides = 5
expect(extrusion.surfaces.length).toBe( expect(extrusion.surfaces.length).toBe(
@ -535,7 +535,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'path', type: 'path',
segIds: [], segIds: [],
planeId: 'UUID-1', planeId: 'UUID-1',
extrusionId: '', sweepId: '',
codeRef: { codeRef: {
pathToNode: [['body', '']], pathToNode: [['body', '']],
range: [43, 70], range: [43, 70],
@ -544,7 +544,8 @@ describe('testing getArtifactsToUpdate', () => {
]) ])
expect(getUpdateObjects('extrude')).toEqual([ expect(getUpdateObjects('extrude')).toEqual([
{ {
type: 'extrusion', type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String), pathId: expect.any(String),
surfaceIds: [], surfaceIds: [],
edgeIds: [], edgeIds: [],
@ -557,7 +558,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'path', type: 'path',
segIds: expect.any(Array), segIds: expect.any(Array),
planeId: expect.any(String), planeId: expect.any(String),
extrusionId: expect.any(String), sweepId: expect.any(String),
codeRef: { codeRef: {
range: [43, 70], range: [43, 70],
pathToNode: [['body', '']], pathToNode: [['body', '']],
@ -580,7 +581,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'path', type: 'path',
segIds: expect.any(Array), segIds: expect.any(Array),
planeId: expect.any(String), planeId: expect.any(String),
extrusionId: expect.any(String), sweepId: expect.any(String),
codeRef: { codeRef: {
range: [43, 70], range: [43, 70],
pathToNode: [['body', '']], pathToNode: [['body', '']],
@ -617,7 +618,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'wall', type: 'wall',
segId: expect.any(String), segId: expect.any(String),
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
extrusionId: expect.any(String), sweepId: expect.any(String),
pathIds: [], pathIds: [],
}, },
{ {
@ -631,7 +632,8 @@ describe('testing getArtifactsToUpdate', () => {
}, },
}, },
{ {
type: 'extrusion', type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String), pathId: expect.any(String),
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
@ -644,7 +646,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'wall', type: 'wall',
segId: expect.any(String), segId: expect.any(String),
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
extrusionId: expect.any(String), sweepId: expect.any(String),
pathIds: [], pathIds: [],
}, },
{ {
@ -658,7 +660,8 @@ describe('testing getArtifactsToUpdate', () => {
}, },
}, },
{ {
type: 'extrusion', type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String), pathId: expect.any(String),
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
@ -671,7 +674,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'wall', type: 'wall',
segId: expect.any(String), segId: expect.any(String),
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
extrusionId: expect.any(String), sweepId: expect.any(String),
pathIds: [], pathIds: [],
}, },
{ {
@ -686,7 +689,8 @@ describe('testing getArtifactsToUpdate', () => {
edgeCutId: expect.any(String), edgeCutId: expect.any(String),
}, },
{ {
type: 'extrusion', type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String), pathId: expect.any(String),
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
@ -699,7 +703,7 @@ describe('testing getArtifactsToUpdate', () => {
type: 'wall', type: 'wall',
segId: expect.any(String), segId: expect.any(String),
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
extrusionId: expect.any(String), sweepId: expect.any(String),
pathIds: [], pathIds: [],
}, },
{ {
@ -713,7 +717,8 @@ describe('testing getArtifactsToUpdate', () => {
}, },
}, },
{ {
type: 'extrusion', type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String), pathId: expect.any(String),
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
@ -726,11 +731,12 @@ describe('testing getArtifactsToUpdate', () => {
type: 'cap', type: 'cap',
subType: 'start', subType: 'start',
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
extrusionId: expect.any(String), sweepId: expect.any(String),
pathIds: [], pathIds: [],
}, },
{ {
type: 'extrusion', type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String), pathId: expect.any(String),
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),
@ -743,11 +749,12 @@ describe('testing getArtifactsToUpdate', () => {
type: 'cap', type: 'cap',
subType: 'end', subType: 'end',
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
extrusionId: expect.any(String), sweepId: expect.any(String),
pathIds: [], pathIds: [],
}, },
{ {
type: 'extrusion', type: 'sweep',
subType: 'extrusion',
pathId: expect.any(String), pathId: expect.any(String),
surfaceIds: expect.any(Array), surfaceIds: expect.any(Array),
edgeIds: expect.any(Array), edgeIds: expect.any(Array),

View File

@ -25,7 +25,7 @@ export interface PathArtifact {
type: 'path' type: 'path'
planeId: ArtifactId planeId: ArtifactId
segIds: Array<ArtifactId> segIds: Array<ArtifactId>
extrusionId: ArtifactId sweepId: ArtifactId
solid2dId?: ArtifactId solid2dId?: ArtifactId
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
@ -38,11 +38,11 @@ export interface PathArtifactRich {
type: 'path' type: 'path'
plane: PlaneArtifact | WallArtifact plane: PlaneArtifact | WallArtifact
segments: Array<SegmentArtifact> segments: Array<SegmentArtifact>
extrusion: ExtrusionArtifact sweep: SweepArtifact
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface SegmentArtifact { export interface SegmentArtifact {
type: 'segment' type: 'segment'
pathId: ArtifactId pathId: ArtifactId
surfaceId: ArtifactId surfaceId: ArtifactId
@ -54,23 +54,26 @@ interface SegmentArtifactRich {
type: 'segment' type: 'segment'
path: PathArtifact path: PathArtifact
surf: WallArtifact surf: WallArtifact
edges: Array<ExtrudeEdge> edges: Array<SweepEdge>
edgeCut?: EdgeCut edgeCut?: EdgeCut
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface ExtrusionArtifact { /** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
type: 'extrusion' interface SweepArtifact {
pathId: ArtifactId type: 'sweep'
surfaceIds: Array<ArtifactId> subType: 'extrusion' | 'revolve'
edgeIds: Array<ArtifactId> pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
interface ExtrusionArtifactRich { interface SweepArtifactRich {
type: 'extrusion' type: 'sweep'
subType: 'extrusion' | 'revolve'
path: PathArtifact path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact> surfaces: Array<WallArtifact | CapArtifact>
edges: Array<ExtrudeEdge> edges: Array<SweepEdge>
codeRef: CommonCommandProperties codeRef: CommonCommandProperties
} }
@ -78,21 +81,21 @@ interface WallArtifact {
type: 'wall' type: 'wall'
segId: ArtifactId segId: ArtifactId
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
extrusionId: ArtifactId sweepId: ArtifactId
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
} }
interface CapArtifact { interface CapArtifact {
type: 'cap' type: 'cap'
subType: 'start' | 'end' subType: 'start' | 'end'
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
extrusionId: ArtifactId sweepId: ArtifactId
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
} }
interface ExtrudeEdge { interface SweepEdge {
type: 'extrudeEdge' type: 'sweepEdge'
segId: ArtifactId segId: ArtifactId
extrusionId: ArtifactId sweepId: ArtifactId
subType: 'opposite' | 'adjacent' subType: 'opposite' | 'adjacent'
} }
@ -116,10 +119,10 @@ export type Artifact =
| PlaneArtifact | PlaneArtifact
| PathArtifact | PathArtifact
| SegmentArtifact | SegmentArtifact
| ExtrusionArtifact | SweepArtifact
| WallArtifact | WallArtifact
| CapArtifact | CapArtifact
| ExtrudeEdge | SweepEdge
| EdgeCut | EdgeCut
| EdgeCutEdge | EdgeCutEdge
| solid2D | solid2D
@ -257,7 +260,7 @@ export function getArtifactsToUpdate({
type: 'wall', type: 'wall',
segId: existingPlane.segId, segId: existingPlane.segId,
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
extrusionId: existingPlane.extrusionId, sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds, pathIds: existingPlane.pathIds,
}, },
}, },
@ -274,7 +277,7 @@ export function getArtifactsToUpdate({
type: 'path', type: 'path',
segIds: [], segIds: [],
planeId: currentPlaneId, planeId: currentPlaneId,
extrusionId: '', sweepId: '',
codeRef: { range, pathToNode }, codeRef: { range, pathToNode },
}, },
}) })
@ -294,7 +297,7 @@ export function getArtifactsToUpdate({
type: 'wall', type: 'wall',
segId: plane.segId, segId: plane.segId,
edgeCutEdgeIds: plane.edgeCutEdgeIds, edgeCutEdgeIds: plane.edgeCutEdgeIds,
extrusionId: plane.extrusionId, sweepId: plane.sweepId,
pathIds: [id], pathIds: [id],
}, },
}) })
@ -337,11 +340,13 @@ export function getArtifactsToUpdate({
}) })
} }
return returnArr return returnArr
} else if (cmd.type === 'extrude') { } else if (cmd.type === 'extrude' || cmd.type === 'revolve') {
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
returnArr.push({ returnArr.push({
id, id,
artifact: { artifact: {
type: 'extrusion', type: 'sweep',
subType: subType,
pathId: cmd.target, pathId: cmd.target,
surfaceIds: [], surfaceIds: [],
edgeIds: [], edgeIds: [],
@ -352,7 +357,7 @@ export function getArtifactsToUpdate({
if (path?.type === 'path') if (path?.type === 'path')
returnArr.push({ returnArr.push({
id: cmd.target, id: cmd.target,
artifact: { ...path, extrusionId: id }, artifact: { ...path, sweepId: id },
}) })
return returnArr return returnArr
} else if ( } else if (
@ -375,7 +380,7 @@ export function getArtifactsToUpdate({
type: 'wall', type: 'wall',
segId: curve_id, segId: curve_id,
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
extrusionId: path.extrusionId, sweepId: path.sweepId,
pathIds: [], pathIds: [],
}, },
}) })
@ -383,12 +388,12 @@ export function getArtifactsToUpdate({
id: curve_id, id: curve_id,
artifact: { ...seg, surfaceId: face_id }, artifact: { ...seg, surfaceId: face_id },
}) })
const extrusion = getArtifact(path.extrusionId) const sweep = getArtifact(path.sweepId)
if (extrusion?.type === 'extrusion') { if (sweep?.type === 'sweep') {
returnArr.push({ returnArr.push({
id: path.extrusionId, id: path.sweepId,
artifact: { artifact: {
...extrusion, ...sweep,
surfaceIds: [face_id], surfaceIds: [face_id],
}, },
}) })
@ -407,16 +412,16 @@ export function getArtifactsToUpdate({
type: 'cap', type: 'cap',
subType: cap === 'bottom' ? 'start' : 'end', subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
extrusionId: path.extrusionId, sweepId: path.sweepId,
pathIds: [], pathIds: [],
}, },
}) })
const extrusion = getArtifact(path.extrusionId) const sweep = getArtifact(path.sweepId)
if (extrusion?.type !== 'extrusion') return if (sweep?.type !== 'sweep') return
returnArr.push({ returnArr.push({
id: path.extrusionId, id: path.sweepId,
artifact: { artifact: {
...extrusion, ...sweep,
surfaceIds: [face_id], surfaceIds: [face_id],
}, },
}) })
@ -431,17 +436,17 @@ export function getArtifactsToUpdate({
response.data.modeling_response.type === 'solid3d_get_opposite_edge' && response.data.modeling_response.type === 'solid3d_get_opposite_edge' &&
response.data.modeling_response.data.edge) || response.data.modeling_response.data.edge) ||
// or is adjacent edge // or is adjacent edge
(cmd.type === 'solid3d_get_prev_adjacent_edge' && (cmd.type === 'solid3d_get_next_adjacent_edge' &&
response.type === 'modeling' && response.type === 'modeling' &&
response.data.modeling_response.type === response.data.modeling_response.type ===
'solid3d_get_prev_adjacent_edge' && 'solid3d_get_next_adjacent_edge' &&
response.data.modeling_response.data.edge) response.data.modeling_response.data.edge)
) { ) {
const wall = getArtifact(cmd.face_id) const wall = getArtifact(cmd.face_id)
if (wall?.type !== 'wall') return returnArr if (wall?.type !== 'wall') return returnArr
const extrusion = getArtifact(wall.extrusionId) const sweep = getArtifact(wall.sweepId)
if (extrusion?.type !== 'extrusion') return returnArr if (sweep?.type !== 'sweep') return returnArr
const path = getArtifact(extrusion.pathId) const path = getArtifact(sweep.pathId)
if (path?.type !== 'path') return returnArr if (path?.type !== 'path') return returnArr
const segment = getArtifact(cmd.edge_id) const segment = getArtifact(cmd.edge_id)
if (segment?.type !== 'segment') return returnArr if (segment?.type !== 'segment') return returnArr
@ -450,13 +455,13 @@ export function getArtifactsToUpdate({
{ {
id: response.data.modeling_response.data.edge, id: response.data.modeling_response.data.edge,
artifact: { artifact: {
type: 'extrudeEdge', type: 'sweepEdge',
subType: subType:
cmd.type === 'solid3d_get_prev_adjacent_edge' cmd.type === 'solid3d_get_next_adjacent_edge'
? 'adjacent' ? 'adjacent'
: 'opposite', : 'opposite',
segId: cmd.edge_id, segId: cmd.edge_id,
extrusionId: path.extrusionId, sweepId: path.sweepId,
}, },
}, },
{ {
@ -467,9 +472,9 @@ export function getArtifactsToUpdate({
}, },
}, },
{ {
id: path.extrusionId, id: path.sweepId,
artifact: { artifact: {
...extrusion, ...sweep,
edgeIds: [response.data.modeling_response.data.edge], edgeIds: [response.data.modeling_response.data.edge],
}, },
}, },
@ -582,10 +587,10 @@ export function expandPath(
{ keys: path.segIds, types: ['segment'] }, { keys: path.segIds, types: ['segment'] },
artifactGraph artifactGraph
) )
const extrusion = getArtifactOfTypes( const sweep = getArtifactOfTypes(
{ {
key: path.extrusionId, key: path.sweepId,
types: ['extrusion'], types: ['sweep'],
}, },
artifactGraph artifactGraph
) )
@ -593,40 +598,41 @@ export function expandPath(
{ key: path.planeId, types: ['plane', 'wall'] }, { key: path.planeId, types: ['plane', 'wall'] },
artifactGraph artifactGraph
) )
if (err(extrusion)) return extrusion if (err(sweep)) return sweep
if (err(plane)) return plane if (err(plane)) return plane
return { return {
type: 'path', type: 'path',
segments: Array.from(segs.values()), segments: Array.from(segs.values()),
extrusion, sweep,
plane, plane,
codeRef: path.codeRef, codeRef: path.codeRef,
} }
} }
export function expandExtrusion( export function expandSweep(
extrusion: ExtrusionArtifact, sweep: SweepArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): ExtrusionArtifactRich | Error { ): SweepArtifactRich | Error {
const surfs = getArtifactsOfTypes( const surfs = getArtifactsOfTypes(
{ keys: extrusion.surfaceIds, types: ['wall', 'cap'] }, { keys: sweep.surfaceIds, types: ['wall', 'cap'] },
artifactGraph artifactGraph
) )
const edges = getArtifactsOfTypes( const edges = getArtifactsOfTypes(
{ keys: extrusion.edgeIds, types: ['extrudeEdge'] }, { keys: sweep.edgeIds, types: ['sweepEdge'] },
artifactGraph artifactGraph
) )
const path = getArtifactOfTypes( const path = getArtifactOfTypes(
{ key: extrusion.pathId, types: ['path'] }, { key: sweep.pathId, types: ['path'] },
artifactGraph artifactGraph
) )
if (err(path)) return path if (err(path)) return path
return { return {
type: 'extrusion', type: 'sweep',
subType: 'extrusion',
surfaces: Array.from(surfs.values()), surfaces: Array.from(surfs.values()),
edges: Array.from(edges.values()), edges: Array.from(edges.values()),
path, path,
codeRef: extrusion.codeRef, codeRef: sweep.codeRef,
} }
} }
@ -643,7 +649,7 @@ export function expandSegment(
artifactGraph artifactGraph
) )
const edges = getArtifactsOfTypes( const edges = getArtifactsOfTypes(
{ keys: segment.edgeIds, types: ['extrudeEdge'] }, { keys: segment.edgeIds, types: ['sweepEdge'] },
artifactGraph artifactGraph
) )
const edgeCut = segment.edgeCutId const edgeCut = segment.edgeCutId
@ -670,13 +676,13 @@ export function getCapCodeRef(
cap: CapArtifact, cap: CapArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CommonCommandProperties | Error { ): CommonCommandProperties | Error {
const extrusion = getArtifactOfTypes( const sweep = getArtifactOfTypes(
{ key: cap.extrusionId, types: ['extrusion'] }, { key: cap.sweepId, types: ['sweep'] },
artifactGraph artifactGraph
) )
if (err(extrusion)) return extrusion if (err(sweep)) return sweep
const path = getArtifactOfTypes( const path = getArtifactOfTypes(
{ key: extrusion.pathId, types: ['path'] }, { key: sweep.pathId, types: ['path'] },
artifactGraph artifactGraph
) )
if (err(path)) return path if (err(path)) return path
@ -707,8 +713,8 @@ export function getWallCodeRef(
return seg.codeRef return seg.codeRef
} }
export function getExtrudeEdgeCodeRef( export function getSweepEdgeCodeRef(
edge: ExtrudeEdge, edge: SweepEdge,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CommonCommandProperties | Error { ): CommonCommandProperties | Error {
const seg = getArtifactOfTypes( const seg = getArtifactOfTypes(
@ -718,30 +724,64 @@ export function getExtrudeEdgeCodeRef(
if (err(seg)) return seg if (err(seg)) return seg
return seg.codeRef return seg.codeRef
} }
export function getEdgeCuteConsumedCodeRef(
edge: EdgeCut,
artifactGraph: ArtifactGraph
): CommonCommandProperties | Error {
const seg = getArtifactOfTypes(
{ key: edge.consumedEdgeId, types: ['segment', 'sweepEdge'] },
artifactGraph
)
if (err(seg)) return seg
if (seg.type === 'segment') return seg.codeRef
return getSweepEdgeCodeRef(seg, artifactGraph)
}
export function getExtrusionFromSuspectedExtrudeSurface( export function getSweepFromSuspectedSweepSurface(
id: ArtifactId, id: ArtifactId,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error { ): SweepArtifact | Error {
const artifact = getArtifactOfTypes( const artifact = getArtifactOfTypes(
{ key: id, types: ['wall', 'cap'] }, { key: id, types: ['wall', 'cap', 'edgeCut'] },
artifactGraph artifactGraph
) )
if (err(artifact)) return artifact if (err(artifact)) return artifact
if (artifact.type === 'wall' || artifact.type === 'cap') {
return getArtifactOfTypes(
{ key: artifact.sweepId, types: ['sweep'] },
artifactGraph
)
}
const segOrEdge = getArtifactOfTypes(
{ key: artifact.consumedEdgeId, types: ['segment', 'sweepEdge'] },
artifactGraph
)
if (err(segOrEdge)) return segOrEdge
if (segOrEdge.type === 'segment') {
const path = getArtifactOfTypes(
{ key: segOrEdge.pathId, types: ['path'] },
artifactGraph
)
if (err(path)) return path
return getArtifactOfTypes(
{ key: path.sweepId, types: ['sweep'] },
artifactGraph
)
}
return getArtifactOfTypes( return getArtifactOfTypes(
{ key: artifact.extrusionId, types: ['extrusion'] }, { key: segOrEdge.sweepId, types: ['sweep'] },
artifactGraph artifactGraph
) )
} }
export function getExtrusionFromSuspectedPath( export function getSweepFromSuspectedPath(
id: ArtifactId, id: ArtifactId,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error { ): SweepArtifact | Error {
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph) const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
if (err(path)) return path if (err(path)) return path
return getArtifactOfTypes( return getArtifactOfTypes(
{ key: path.extrusionId, types: ['extrusion'] }, { key: path.sweepId, types: ['sweep'] },
artifactGraph artifactGraph
) )
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 KiB

After

Width:  |  Height:  |  Size: 613 KiB

View File

@ -16,7 +16,11 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { exportMake } from 'lib/exportMake' import { exportMake } from 'lib/exportMake'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { SettingsViaQueryString } from 'lib/settings/settingsTypes' import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants' import {
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE,
EXPORT_TOAST_MESSAGES,
MAKE_TOAST_MESSAGES,
} from 'lib/constants'
import { KclManager } from 'lang/KclSingleton' import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
@ -959,7 +963,9 @@ class EngineConnection extends EventTarget {
) { ) {
// Reject the promise with the error. // Reject the promise with the error.
this.engineCommandManager.pendingExport.reject(errorsString) this.engineCommandManager.pendingExport.reject(errorsString)
toast.error(errorsString) toast.error(errorsString, {
id: this.engineCommandManager.pendingExport.toastId,
})
this.engineCommandManager.pendingExport = undefined this.engineCommandManager.pendingExport = undefined
} }
} else { } else {
@ -1327,8 +1333,13 @@ export class EngineCommandManager extends EventTarget {
defaultPlanes: DefaultPlanes | null = null defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = [] commandLogs: CommandLog[] = []
pendingExport?: { pendingExport?: {
/** The id of the shared loading/success/error toast for export */
toastId: string
/** An on-success callback */
resolve: (a: null) => void resolve: (a: null) => void
/** An on-error callback */
reject: (reason: string) => void reject: (reason: string) => void
/** The engine command uuid */
commandId: string commandId: string
} }
settings: SettingsViaQueryString settings: SettingsViaQueryString
@ -1590,7 +1601,7 @@ export class EngineCommandManager extends EventTarget {
// because in all other cases we send JSON strings. But in the case of // because in all other cases we send JSON strings. But in the case of
// export we send a binary blob. // export we send a binary blob.
// Pass this to our export function. // Pass this to our export function.
if (this.exportIntent === null) { if (this.exportIntent === null || this.pendingExport === undefined) {
toast.error( toast.error(
'Export intent was not set, but export data was received' 'Export intent was not set, but export data was received'
) )
@ -1602,19 +1613,22 @@ export class EngineCommandManager extends EventTarget {
switch (this.exportIntent) { switch (this.exportIntent) {
case ExportIntent.Save: { case ExportIntent.Save: {
exportSave(event.data).then(() => { exportSave(event.data, this.pendingExport.toastId).then(() => {
this.pendingExport?.resolve(null) this.pendingExport?.resolve(null)
}, this.pendingExport?.reject) }, this.pendingExport?.reject)
break break
} }
case ExportIntent.Make: { case ExportIntent.Make: {
exportMake(event.data).then((result) => { exportMake(event.data, this.pendingExport.toastId).then(
if (result) { (result) => {
this.pendingExport?.resolve(null) if (result) {
} else { this.pendingExport?.resolve(null)
this.pendingExport?.reject('Failed to make export') } else {
} this.pendingExport?.reject('Failed to make export')
}, this.pendingExport?.reject) }
},
this.pendingExport?.reject
)
break break
} }
} }
@ -1929,7 +1943,20 @@ export class EngineCommandManager extends EventTarget {
return Promise.resolve(null) return Promise.resolve(null)
} else if (cmd.type === 'export') { } else if (cmd.type === 'export') {
const promise = new Promise<null>((resolve, reject) => { const promise = new Promise<null>((resolve, reject) => {
if (this.exportIntent === null) {
if (this.exportIntent === null) {
toast.error('Export intent was not set, but export is being sent')
console.error('Export intent was not set, but export is being sent')
return
}
}
const toastId = toast.loading(
this.exportIntent === ExportIntent.Save
? EXPORT_TOAST_MESSAGES.START
: MAKE_TOAST_MESSAGES.START
)
this.pendingExport = { this.pendingExport = {
toastId,
resolve: (passThrough) => { resolve: (passThrough) => {
this.addCommandLog({ this.addCommandLog({
type: 'export-done', type: 'export-done',

View File

@ -231,7 +231,8 @@ describe('testing addTagForSketchOnFace', () => {
pathToNode, pathToNode,
node: ast, node: ast,
}, },
'lineTo' 'lineTo',
null
) )
if (err(sketchOnFaceRetVal)) return sketchOnFaceRetVal if (err(sketchOnFaceRetVal)) return sketchOnFaceRetVal
@ -239,6 +240,62 @@ describe('testing addTagForSketchOnFace', () => {
const expectedCode = genCode('lineTo([-1.59, -1.54], %, $seg01)') const expectedCode = genCode('lineTo([-1.59, -1.54], %, $seg01)')
expect(recast(modifiedAst)).toBe(expectedCode) expect(recast(modifiedAst)).toBe(expectedCode)
}) })
it('can break up chamfers in order to add tags', async () => {
const originalChamfer = `|> chamfer({
length: 30,
tags: [seg01, getOppositeEdge(seg01)]
}, %)`
const genCode = (
insertCode: string
) => `const sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
const extrude001 = extrude(100, sketch001)
${insertCode}
`
const code = genCode(originalChamfer)
const ast = parse(code)
await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalChamfer)
const sourceRange: [number, number] = [
sourceStart + 3,
sourceStart + originalChamfer.length - 3,
]
if (err(ast)) throw ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
console.log('pathToNode', pathToNode)
const sketchOnFaceRetVal = addTagForSketchOnFace(
{
pathToNode,
node: ast,
},
'chamfer',
{
type: 'edgeCut',
subType: 'opposite',
tagName: 'seg01',
}
)
if (err(sketchOnFaceRetVal)) throw sketchOnFaceRetVal
expect(recast(sketchOnFaceRetVal.modifiedAst)).toBe(
genCode(`|> chamfer({
length: 30,
tags: [getOppositeEdge(seg01)]
}, %, $seg03)
|> chamfer({ length: 30, tags: [seg01] }, %)`)
)
})
}) })
describe('testing getConstraintInfo', () => { describe('testing getConstraintInfo', () => {

View File

@ -53,6 +53,7 @@ import { roundOff, getLength, getAngle } from 'lib/utils'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { perpendicularDistance } from 'sketch-helpers' import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { EdgeCutInfo } from 'machines/modelingMachine'
const STRAIGHT_SEGMENT_ERR = new Error( const STRAIGHT_SEGMENT_ERR = new Error(
'Invalid input, expected "straight-segment"' 'Invalid input, expected "straight-segment"'
@ -1895,13 +1896,131 @@ export function replaceSketchLine({
return { modifiedAst, valueUsedInTransform, pathToNode } return { modifiedAst, valueUsedInTransform, pathToNode }
} }
/** Ostensibly should be used to add a chamfer tag to a chamfer call expression
*
* However things get complicated in situations like:
* ```ts
* |> chamfer({
* length: 1,
* tags: [tag1, tagOfInterest]
* }, %)
* ```
* Because tag declarator is not allowed on a chamfer with more than one tag,
* They must be pulled apart into separate chamfer calls:
* ```ts
* |> chamfer({
* length: 1,
* tags: [tag1]
* }, %)
* |> chamfer({
* length: 1,
* tags: [tagOfInterest]
* }, %, $newTagDeclarator)
* ```
*/
function addTagToChamfer(
tagInfo: AddTagInfo,
edgeCutMeta: EdgeCutInfo | null
):
| {
modifiedAst: Program
tag: string
}
| Error {
const _node = structuredClone(tagInfo.node)
let pipeIndex = 0
for (let i = 0; i < tagInfo.pathToNode.length; i++) {
if (tagInfo.pathToNode[i][1] === 'PipeExpression') {
pipeIndex = Number(tagInfo.pathToNode[i + 1][0])
break
}
}
const pipeExpr = getNodeFromPath<PipeExpression>(
_node,
tagInfo.pathToNode,
'PipeExpression'
)
if (err(pipeExpr)) return pipeExpr
const callExpr = pipeExpr.node.body[pipeIndex]
if (callExpr.type !== 'CallExpression')
return new Error('no chamfer call Expr')
const obj = callExpr.arguments[0]
if (obj.type !== 'ObjectExpression')
return new Error('first argument should be an object expression')
const tags = obj.properties.find((a) => {
return a.key.name === 'tags'
})
if (!tags) return new Error('no tags property')
if (tags.value.type !== 'ArrayExpression')
return new Error('tags should be an array expression')
if (tags.value.elements.length < 2) {
return addTag(2)(tagInfo)
}
const tagIndexToPullOut = tags.value.elements.findIndex((element) => {
if (
edgeCutMeta?.subType === 'base' &&
element.type === 'Identifier' &&
element.name === edgeCutMeta.tagName
)
return true
if (
edgeCutMeta?.subType === 'opposite' &&
element.type === 'CallExpression' &&
element.callee.name === 'getOppositeEdge' &&
element.arguments[0].type === 'Identifier' &&
element.arguments[0].name === edgeCutMeta.tagName
)
return true
if (
edgeCutMeta?.subType === 'adjacent' &&
element.type === 'CallExpression' &&
(element.callee.name === 'getNextAdjacentEdge' ||
element.callee.name === 'getPrevAdjacentEdge') &&
element.arguments[0].type === 'Identifier' &&
element.arguments[0].name === edgeCutMeta.tagName
)
return true
return false
})
if (tagIndexToPullOut === -1) return new Error('tag not found')
const tagToPullOut = tags.value.elements[tagIndexToPullOut]
tags.value.elements.splice(tagIndexToPullOut, 1)
const chamferLength = obj.properties.find(
(a) => a.key.name === 'length'
)?.value
if (!chamferLength) return new Error('no chamfer length')
const tagDec = createTagDeclarator(findUniqueName(_node, 'seg', 2))
const newExpressionToInsert = createCallExpression('chamfer', [
createObjectExpression({
length: chamferLength,
tags: createArrayExpression([tagToPullOut]),
}),
createPipeSubstitution(),
tagDec,
])
pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert)
return {
modifiedAst: _node,
tag: tagDec.value,
}
}
export function addTagForSketchOnFace( export function addTagForSketchOnFace(
tagInfo: AddTagInfo, tagInfo: AddTagInfo,
expressionName: string expressionName: string,
) { edgeCutMeta: EdgeCutInfo | null
):
| {
modifiedAst: Program
tag: string
}
| Error {
if (expressionName === 'close') { if (expressionName === 'close') {
return addTag(1)(tagInfo) return addTag(1)(tagInfo)
} }
if (expressionName === 'chamfer') {
return addTagToChamfer(tagInfo, edgeCutMeta)
}
if (expressionName in sketchLineHelperMap) { if (expressionName in sketchLineHelperMap) {
const { addTag } = sketchLineHelperMap[expressionName] const { addTag } = sketchLineHelperMap[expressionName]
return addTag(tagInfo) return addTag(tagInfo)

View File

@ -18,12 +18,24 @@ export function pathMapToSelections(
const nodeMeta = getNodeFromPath<any>(ast, path) const nodeMeta = getNodeFromPath<any>(ast, path)
if (err(nodeMeta)) return if (err(nodeMeta)) return
const node = nodeMeta.node as any const node = nodeMeta.node as any
const type = prevSelections.codeBasedSelections[Number(index)].type const selection = prevSelections.codeBasedSelections[Number(index)]
if (node) { if (node) {
newSelections.codeBasedSelections.push({ if (
range: [node.start, node.end], selection.type === 'base-edgeCut' ||
type: type || 'default', selection.type === 'adjacent-edgeCut' ||
}) selection.type === 'opposite-edgeCut'
) {
newSelections.codeBasedSelections.push({
range: [node.start, node.end],
type: selection.type,
secondaryRange: selection.secondaryRange,
})
} else {
newSelections.codeBasedSelections.push({
range: [node.start, node.end],
type: selection.type,
})
}
} }
}) })
return newSelections return newSelections

View File

@ -1,8 +1,16 @@
/// The method below uses the File System Access API when it's supported and /// The method below uses the File System Access API when it's supported and
// else falls back to the classic approach. In both cases the function saves // else falls back to the classic approach. In both cases the function saves
// the file, but in case of where the File System Access API is supported, the // the file, but in case of where the File System Access API is supported, the
import toast from 'react-hot-toast'
import { EXPORT_TOAST_MESSAGES } from './constants'
// user will get a file save dialog where they can choose where the file should be saved. // user will get a file save dialog where they can choose where the file should be saved.
export const browserSaveFile = async (blob: Blob, suggestedName: string) => { export const browserSaveFile = async (
blob: Blob,
suggestedName: string,
toastId: string
) => {
// Feature detection. The API needs to be supported // Feature detection. The API needs to be supported
// and the app not run in an iframe. // and the app not run in an iframe.
const supportsFileSystemAccess = const supportsFileSystemAccess =
@ -29,11 +37,15 @@ export const browserSaveFile = async (blob: Blob, suggestedName: string) => {
const writable = await handle.createWritable() const writable = await handle.createWritable()
await writable.write(blob) await writable.write(blob)
await writable.close() await writable.close()
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS, { id: toastId })
return return
} catch (err: any) { } catch (err: any) {
// Fail silently if the user has simply canceled the dialog. // Fail silently if the user has simply canceled the dialog.
if (err.name !== 'AbortError') { if (err.name === 'AbortError') {
toast.dismiss(toastId)
} else {
console.error(err.name, err.message) console.error(err.name, err.message)
toast.error(EXPORT_TOAST_MESSAGES.FAILED, { id: toastId })
} }
return return
} }
@ -54,4 +66,5 @@ export const browserSaveFile = async (blob: Blob, suggestedName: string) => {
URL.revokeObjectURL(blobURL) URL.revokeObjectURL(blobURL)
a.remove() a.remove()
}, 1000) }, 1000)
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS, { id: toastId })
} }

View File

@ -6,7 +6,7 @@ const META =
PLATFORM === 'macos' ? 'Cmd' : PLATFORM === 'windows' ? 'Win' : 'Super' PLATFORM === 'macos' ? 'Cmd' : PLATFORM === 'windows' ? 'Win' : 'Super'
const ALT = PLATFORM === 'macos' ? 'Option' : 'Alt' const ALT = PLATFORM === 'macos' ? 'Option' : 'Alt'
const noModifiersPressed = (e: React.MouseEvent) => const noModifiersPressed = (e: MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
export type CameraSystem = export type CameraSystem =
@ -53,14 +53,14 @@ export function mouseControlsToCameraSystem(
interface MouseGuardHandler { interface MouseGuardHandler {
description: string description: string
callback: (e: React.MouseEvent) => boolean callback: (e: MouseEvent) => boolean
lenientDragStartButton?: number lenientDragStartButton?: number
} }
interface MouseGuardZoomHandler { interface MouseGuardZoomHandler {
description: string description: string
dragCallback: (e: React.MouseEvent) => boolean dragCallback: (e: MouseEvent) => boolean
scrollCallback: (e: React.MouseEvent) => boolean scrollCallback: (e: WheelEvent) => boolean
lenientDragStartButton?: number lenientDragStartButton?: number
} }
@ -70,7 +70,7 @@ export interface MouseGuard {
rotate: MouseGuardHandler rotate: MouseGuardHandler
} }
export const btnName = (e: React.MouseEvent) => ({ export const btnName = (e: MouseEvent) => ({
middle: !!(e.buttons & 4) || e.button === 1, middle: !!(e.buttons & 4) || e.button === 1,
right: !!(e.buttons & 2) || e.button === 2, right: !!(e.buttons & 2) || e.button === 2,
left: !!(e.buttons & 1) || e.button === 0, left: !!(e.buttons & 1) || e.button === 0,

View File

@ -1,6 +1,6 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api' import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { machineManager } from 'lib/machineManager' import { machineManager } from 'lib/machineManager'
@ -32,6 +32,10 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: KclCommandValue
} }
Revolve: {
selection: Selections
angle: KclCommandValue
}
Fillet: { Fillet: {
// todo // todo
selection: Selections selection: Selections
@ -209,6 +213,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
args: { args: {
selection: { selection: {
inputType: 'selection', inputType: 'selection',
// TODO: These are products of an extrude
selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'], selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'],
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
@ -232,6 +237,26 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.',
icon: 'revolve',
needsReview: true,
args: {
selection: {
inputType: 'selection',
selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'],
multiple: false, // TODO: multiple selection
required: true,
skip: true,
},
angle: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_DEGREE,
required: true,
},
},
},
Fillet: { Fillet: {
// todo // todo
description: 'Fillet edge', description: 'Fillet edge',

View File

@ -53,9 +53,14 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch', SKETCH: 'sketch',
EXTRUDE: 'extrude', EXTRUDE: 'extrude',
SEGMENT: 'seg', SEGMENT: 'seg',
REVOLVE: 'revolve',
} as const } as const
/** The default KCL length expression */ /** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5` export const KCL_DEFAULT_LENGTH = `5`
/** The default KCL degree expression */
export const KCL_DEFAULT_DEGREE = `360`
/** localStorage key for the playwright test-specific app settings file */ /** localStorage key for the playwright test-specific app settings file */
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings' export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
@ -72,3 +77,21 @@ export const PLAYWRIGHT_KEY = 'playwright'
* allows us to match if the execution of executeAst was interrupted */ * allows us to match if the execution of executeAst was interrupted */
export const EXECUTE_AST_INTERRUPT_ERROR_MESSAGE = export const EXECUTE_AST_INTERRUPT_ERROR_MESSAGE =
'Force interrupt, executionIsStale, new AST requested' 'Force interrupt, executionIsStale, new AST requested'
/** The messages that appear for exporting toasts */
export const EXPORT_TOAST_MESSAGES = {
START: 'Exporting...',
SUCCESS: 'Exported successfully',
FAILED: 'Export failed',
}
/** The messages that appear for "make" command toasts */
export const MAKE_TOAST_MESSAGES = {
START: 'Starting print...',
NO_MACHINES: 'No machines available',
NO_MACHINE_API_IP: 'No machine api ip available',
NO_CURRENT_MACHINE: 'No current machine available',
NO_MACHINE_ID: 'No machine id available',
ERROR_STARTING_PRINT: 'Error while starting print',
SUCCESS: 'Started print successfully',
}

View File

@ -46,6 +46,7 @@ export function createMachineCommand<
| Command<T, typeof type, S[typeof type]>[] | Command<T, typeof type, S[typeof type]>[]
| null { | null {
const commandConfig = commandBarConfig && commandBarConfig[type] const commandConfig = commandBarConfig && commandBarConfig[type]
// There may be no command config for this event type, // There may be no command config for this event type,
// or there may be multiple commands to create. // or there may be multiple commands to create.
if (!commandConfig) { if (!commandConfig) {

View File

@ -1,7 +1,6 @@
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { type Models } from '@kittycad/lib' import { type Models } from '@kittycad/lib'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
// Isolating a function to call the engine to export the current scene. // Isolating a function to call the engine to export the current scene.
// Because it has given us trouble in automated testing environments. // Because it has given us trouble in automated testing environments.
@ -23,11 +22,5 @@ export async function exportFromEngine({
cmd_id: uuidv4(), cmd_id: uuidv4(),
}) })
// If we are in playwright slow down the export.
const inPlaywright = window.localStorage.getItem(IS_PLAYWRIGHT_KEY)
if (inPlaywright === 'true') {
await new Promise((resolve) => setTimeout(resolve, 2000))
}
return exportPromise return exportPromise
} }

View File

@ -3,33 +3,37 @@ import { machineManager } from './machineManager'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { components } from './machine-api' import { components } from './machine-api'
import ModelingAppFile from './modelingAppFile' import ModelingAppFile from './modelingAppFile'
import { MAKE_TOAST_MESSAGES } from './constants'
// Make files locally from an export call. // Make files locally from an export call.
export async function exportMake(data: ArrayBuffer): Promise<Response | null> { export async function exportMake(
data: ArrayBuffer,
toastId: string
): Promise<Response | null> {
if (machineManager.machineCount() === 0) { if (machineManager.machineCount() === 0) {
console.error('No machines available') console.error(MAKE_TOAST_MESSAGES.NO_MACHINES)
toast.error('No machines available') toast.error(MAKE_TOAST_MESSAGES.NO_MACHINES, { id: toastId })
return null return null
} }
const machineApiIp = machineManager.machineApiIp const machineApiIp = machineManager.machineApiIp
if (!machineApiIp) { if (!machineApiIp) {
console.error('No machine api ip available') console.error(MAKE_TOAST_MESSAGES.NO_MACHINE_API_IP)
toast.error('No machine api ip available') toast.error(MAKE_TOAST_MESSAGES.NO_MACHINE_API_IP, { id: toastId })
return null return null
} }
const currentMachine = machineManager.currentMachine const currentMachine = machineManager.currentMachine
if (!currentMachine) { if (!currentMachine) {
console.error('No current machine available') console.error(MAKE_TOAST_MESSAGES.NO_CURRENT_MACHINE)
toast.error('No current machine available') toast.error(MAKE_TOAST_MESSAGES.NO_CURRENT_MACHINE, { id: toastId })
return null return null
} }
let machineId = currentMachine?.id let machineId = currentMachine?.id
if (!machineId) { if (!machineId) {
console.error('No machine id available', currentMachine) console.error(MAKE_TOAST_MESSAGES.NO_MACHINE_ID, currentMachine)
toast.error('No machine id available') toast.error(MAKE_TOAST_MESSAGES.NO_MACHINE_ID, { id: toastId })
return null return null
} }
@ -58,16 +62,22 @@ export async function exportMake(data: ArrayBuffer): Promise<Response | null> {
console.log('response', response) console.log('response', response)
if (!response.ok) { if (!response.ok) {
console.error('Error exporting', response) console.error(MAKE_TOAST_MESSAGES.ERROR_STARTING_PRINT, response)
const text = await response.text() const text = await response.text()
toast.error('Error exporting: ' + response.statusText + ' ' + text) toast.error(
'Error while starting print: ' + response.statusText + ' ' + text,
{
id: toastId,
}
)
return null return null
} }
toast.success(MAKE_TOAST_MESSAGES.SUCCESS, { id: toastId })
return response return response
} catch (error) { } catch (error) {
console.error('Error exporting', error) console.error(MAKE_TOAST_MESSAGES.ERROR_STARTING_PRINT, error)
toast.error('Error exporting') toast.error(MAKE_TOAST_MESSAGES.ERROR_STARTING_PRINT, { id: toastId })
return null return null
} }
} }

View File

@ -4,8 +4,10 @@ import { browserSaveFile } from './browserSaveFile'
import JSZip from 'jszip' import JSZip from 'jszip'
import ModelingAppFile from './modelingAppFile' import ModelingAppFile from './modelingAppFile'
import toast from 'react-hot-toast'
import { EXPORT_TOAST_MESSAGES } from './constants'
const save_ = async (file: ModelingAppFile) => { const save_ = async (file: ModelingAppFile, toastId: string) => {
try { try {
if (isDesktop()) { if (isDesktop()) {
const extension = file.name.split('.').pop() || null const extension = file.name.split('.').pop() || null
@ -20,6 +22,7 @@ const save_ = async (file: ModelingAppFile) => {
file.name, file.name,
new Uint8Array(file.contents) new Uint8Array(file.contents)
) )
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS, { id: toastId })
return return
} }
@ -36,13 +39,17 @@ const save_ = async (file: ModelingAppFile) => {
// The user canceled the save. // The user canceled the save.
// Return early. // Return early.
if (filePathMeta.canceled) return if (filePathMeta.canceled) {
toast.dismiss(toastId)
return
}
// Write the file. // Write the file.
await window.electron.writeFile( await window.electron.writeFile(
filePathMeta.filePath, filePathMeta.filePath,
new Uint8Array(file.contents) new Uint8Array(file.contents)
) )
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS, { id: toastId })
} else { } else {
// Download the file to the user's computer. // Download the file to the user's computer.
// Now we need to download the files to the user's downloads folder. // Now we need to download the files to the user's downloads folder.
@ -51,16 +58,17 @@ const save_ = async (file: ModelingAppFile) => {
// Create a new blob. // Create a new blob.
const blob = new Blob([new Uint8Array(file.contents)]) const blob = new Blob([new Uint8Array(file.contents)])
// Save the file. // Save the file.
await browserSaveFile(blob, file.name) await browserSaveFile(blob, file.name, toastId)
} }
} catch (e) { } catch (e) {
// TODO: do something real with the error. // TODO: do something real with the error.
console.error('export error', e) console.error('export error', e)
toast.error(EXPORT_TOAST_MESSAGES.FAILED, { id: toastId })
} }
} }
// Saves files locally from an export call. // Saves files locally from an export call.
export async function exportSave(data: ArrayBuffer) { export async function exportSave(data: ArrayBuffer, toastId: string) {
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>. // This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
let uintArray = new Uint8Array(data) let uintArray = new Uint8Array(data)
@ -72,9 +80,9 @@ export async function exportSave(data: ArrayBuffer) {
zip.file(file.name, new Uint8Array(file.contents), { binary: true }) zip.file(file.name, new Uint8Array(file.contents), { binary: true })
} }
return zip.generateAsync({ type: 'array' }).then((contents) => { return zip.generateAsync({ type: 'array' }).then((contents) => {
return save_({ name: 'output.zip', contents }) return save_({ name: 'output.zip', contents }, toastId)
}) })
} else { } else {
return save_(files[0]) return save_(files[0], toastId)
} }
} }

View File

@ -31,7 +31,7 @@ import {
getArtifactOfTypes, getArtifactOfTypes,
getArtifactsOfTypes, getArtifactsOfTypes,
getCapCodeRef, getCapCodeRef,
getExtrudeEdgeCodeRef, getSweepEdgeCodeRef,
getSolid2dCodeRef, getSolid2dCodeRef,
getWallCodeRef, getWallCodeRef,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
@ -41,22 +41,30 @@ export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
export type Axis = 'y-axis' | 'x-axis' | 'z-axis' export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
export type Selection = { export type Selection =
type: | {
| 'default' type:
| 'line-end' | 'default'
| 'line-mid' | 'line-end'
| 'extrude-wall' | 'line-mid'
| 'solid2D' | 'extrude-wall'
| 'start-cap' | 'solid2D'
| 'end-cap' | 'start-cap'
| 'point' | 'end-cap'
| 'edge' | 'point'
| 'line' | 'edge'
| 'arc' | 'adjacent-edge'
| 'all' | 'line'
range: SourceRange | 'arc'
} | 'all'
range: SourceRange
}
| {
type: 'opposite-edgeCut' | 'adjacent-edgeCut' | 'base-edgeCut'
range: SourceRange
// TODO this is a temporary measure that well be made redundant with: https://github.com/KittyCAD/modeling-app/pull/3836
secondaryRange: SourceRange
}
export type Selections = { export type Selections = {
otherSelections: Axis[] otherSelections: Axis[]
codeBasedSelections: Selection[] codeBasedSelections: Selection[]
@ -140,12 +148,21 @@ export async function getEventForSelectWithPoint({
}, },
} }
} }
if (_artifact.type === 'extrudeEdge') { if (_artifact.type === 'sweepEdge') {
const codeRef = getExtrudeEdgeCodeRef( const codeRef = getSweepEdgeCodeRef(
_artifact, _artifact,
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (err(codeRef)) return null if (err(codeRef)) return null
if (_artifact?.subType === 'adjacent') {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'adjacent-edge' },
},
}
}
return { return {
type: 'Set selection', type: 'Set selection',
data: { data: {
@ -154,6 +171,52 @@ export async function getEventForSelectWithPoint({
}, },
} }
} }
if (_artifact.type === 'edgeCut') {
const consumedEdge = getArtifactOfTypes(
{ key: _artifact.consumedEdgeId, types: ['segment', 'sweepEdge'] },
engineCommandManager.artifactGraph
)
if (err(consumedEdge))
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: _artifact.codeRef.range, type: 'default' },
},
}
if (consumedEdge.type === 'segment') {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: _artifact.codeRef.range,
type: 'base-edgeCut',
secondaryRange: consumedEdge.codeRef.range,
},
},
}
}
const segment = getArtifactOfTypes(
{ key: consumedEdge.segId, types: ['segment'] },
engineCommandManager.artifactGraph
)
if (err(segment)) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: _artifact.codeRef.range,
type:
consumedEdge.subType === 'adjacent'
? 'adjacent-edgeCut'
: 'opposite-edgeCut',
secondaryRange: segment.codeRef.range,
},
},
}
}
return null return null
} }
@ -385,10 +448,16 @@ function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) {
} }
function nodeHasExtrude(node: CommonASTNode) { function nodeHasExtrude(node: CommonASTNode) {
return doesPipeHaveCallExp({ return (
calleeName: 'extrude', doesPipeHaveCallExp({
...node, calleeName: 'extrude',
}) ...node,
}) ||
doesPipeHaveCallExp({
calleeName: 'revolve',
...node,
})
)
} }
function nodeHasClose(node: CommonASTNode) { function nodeHasClose(node: CommonASTNode) {
@ -398,7 +467,7 @@ function nodeHasClose(node: CommonASTNode) {
}) })
} }
export function canExtrudeSelection(selection: Selections) { export function canSweepSelection(selection: Selections) {
const commonNodes = selection.codeBasedSelections.map((_, i) => const commonNodes = selection.codeBasedSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i) buildCommonNodeFromSelection(selection, i)
) )
@ -422,10 +491,14 @@ export function canFilletSelection(selection: Selections) {
} }
function canExtrudeSelectionItem(selection: Selections, i: number) { function canExtrudeSelectionItem(selection: Selections, i: number) {
const isolatedSelection = {
...selection,
codeBasedSelections: [selection.codeBasedSelections[i]],
}
const commonNode = buildCommonNodeFromSelection(selection, i) const commonNode = buildCommonNodeFromSelection(selection, i)
return ( return (
!!isSketchPipe(selection) && !!isSketchPipe(isolatedSelection) &&
nodeHasClose(commonNode) && nodeHasClose(commonNode) &&
!nodeHasExtrude(commonNode) !nodeHasExtrude(commonNode)
) )
@ -444,25 +517,17 @@ export type ResolvedSelectionType = [Selection['type'] | 'other', number]
export function getSelectionType( export function getSelectionType(
selection: Selections selection: Selections
): ResolvedSelectionType[] { ): ResolvedSelectionType[] {
return selection.codeBasedSelections const extrudableCount = selection.codeBasedSelections.filter((_, i) => {
.map((s, i) => { const singleSelection = {
if (canExtrudeSelectionItem(selection, i)) { ...selection,
return ['extrude-wall', 1] as ResolvedSelectionType // This is implicitly determining what a face is, which is bad codeBasedSelections: [selection.codeBasedSelections[i]],
} else { }
return ['other', 1] as ResolvedSelectionType return canExtrudeSelectionItem(singleSelection, 0)
} }).length
})
.reduce((acc, [type, count]) => {
const foundIndex = acc.findIndex((item) => item && item[0] === type)
if (foundIndex === -1) { return extrudableCount === selection.codeBasedSelections.length
return [...acc, [type, count]] ? [['extrude-wall', extrudableCount]]
} else { : [['other', selection.codeBasedSelections.length]]
const temp = [...acc]
temp[foundIndex][1] += count
return temp
}
}, [] as ResolvedSelectionType[])
} }
export function getSelectionTypeDisplayText( export function getSelectionTypeDisplayText(
@ -557,14 +622,43 @@ function codeToIdSelections(
} }
return return
} }
if (type === 'edge' && entry.artifact.type === 'segment') {
const edges = getArtifactsOfTypes(
{ keys: entry.artifact.edgeIds, types: ['sweepEdge'] },
engineCommandManager.artifactGraph
)
const edge = [...edges].find(([_, edge]) => edge.type === 'sweepEdge')
if (!edge) return
bestCandidate = {
artifact: edge[1],
selection: { type, range, ...rest },
id: edge[0],
}
}
if (type === 'adjacent-edge' && entry.artifact.type === 'segment') {
const edges = getArtifactsOfTypes(
{ keys: entry.artifact.edgeIds, types: ['sweepEdge'] },
engineCommandManager.artifactGraph
)
const edge = [...edges].find(
([_, edge]) =>
edge.type === 'sweepEdge' && edge.subType === 'adjacent'
)
if (!edge) return
bestCandidate = {
artifact: edge[1],
selection: { type, range, ...rest },
id: edge[0],
}
}
if ( if (
(type === 'end-cap' || type === 'start-cap') && (type === 'end-cap' || type === 'start-cap') &&
entry.artifact.type === 'path' entry.artifact.type === 'path'
) { ) {
const extrusion = getArtifactOfTypes( const extrusion = getArtifactOfTypes(
{ {
key: entry.artifact.extrusionId, key: entry.artifact.sweepId,
types: ['extrusion'], types: ['sweep'],
}, },
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
@ -584,6 +678,54 @@ function codeToIdSelections(
} }
return return
} }
if (entry.artifact.type === 'edgeCut') {
const consumedEdge = getArtifactOfTypes(
{
key: entry.artifact.consumedEdgeId,
types: ['segment', 'sweepEdge'],
},
engineCommandManager.artifactGraph
)
if (err(consumedEdge)) return
if (
consumedEdge.type === 'segment' &&
type === 'base-edgeCut' &&
isOverlap(
consumedEdge.codeRef.range,
entry.selection?.secondaryRange || [0, 0]
)
) {
bestCandidate = {
artifact: entry.artifact,
selection: { type, range, ...rest },
id: entry.id,
}
} else if (
consumedEdge.type === 'sweepEdge' &&
((type === 'adjacent-edgeCut' &&
consumedEdge.subType === 'adjacent') ||
(type === 'opposite-edgeCut' &&
consumedEdge.subType === 'opposite'))
) {
const seg = getArtifactOfTypes(
{ key: consumedEdge.segId, types: ['segment'] },
engineCommandManager.artifactGraph
)
if (err(seg)) return
if (
isOverlap(
seg.codeRef.range,
entry.selection?.secondaryRange || [0, 0]
)
) {
bestCandidate = {
artifact: entry.artifact,
selection: { type, range, ...rest },
id: entry.id,
}
}
}
}
}) })
if (bestCandidate) { if (bestCandidate) {
@ -645,9 +787,20 @@ export function updateSelections(
const nodeMeta = getNodeFromPath<Expr>(ast, pathToNode) const nodeMeta = getNodeFromPath<Expr>(ast, pathToNode)
if (err(nodeMeta)) return undefined if (err(nodeMeta)) return undefined
const node = nodeMeta.node const node = nodeMeta.node
const selection = prevSelectionRanges.codeBasedSelections[Number(index)]
if (
selection?.type === 'base-edgeCut' ||
selection?.type === 'adjacent-edgeCut' ||
selection?.type === 'opposite-edgeCut'
)
return {
range: [node.start, node.end],
type: selection?.type,
secondaryRange: selection?.secondaryRange,
}
return { return {
range: [node.start, node.end], range: [node.start, node.end],
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type, type: selection?.type,
} }
}) })
.filter((x?: Selection) => x !== undefined) as Selection[] .filter((x?: Selection) => x !== undefined) as Selection[]

View File

@ -94,9 +94,16 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}, },
{ {
id: 'revolve', id: 'revolve',
onClick: () => console.error('Revolve not yet implemented'), onClick: ({ commandBarSend }) =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Revolve', groupId: 'modeling' },
}),
// TODO: disabled
// Who's state is this?
disabled: (state) => !state.can({ type: 'Revolve' }),
icon: 'revolve', icon: 'revolve',
status: 'kcl-only', status: DEV ? 'available' : 'kcl-only',
title: 'Revolve', title: 'Revolve',
hotkey: 'R', hotkey: 'R',
description: description:

File diff suppressed because one or more lines are too long

279
src/wasm-lib/Cargo.lock generated
View File

@ -78,20 +78,26 @@ checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.88" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
dependencies = [ dependencies = [
"backtrace", "backtrace",
] ]
[[package]]
name = "approx"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08abcc3b4e9339e33a3d0a5ed15d84a687350c05689d825e0f6655eef9e76a94"
[[package]] [[package]]
name = "approx" name = "approx"
version = "0.5.1" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [ dependencies = [
"num-traits", "num-traits 0.2.18",
] ]
[[package]] [[package]]
@ -201,7 +207,7 @@ dependencies = [
"libm", "libm",
"num-bigint", "num-bigint",
"num-integer", "num-integer",
"num-traits", "num-traits 0.2.18",
"serde", "serde",
] ]
@ -249,19 +255,19 @@ dependencies = [
[[package]] [[package]]
name = "bson" name = "bson"
version = "2.12.0" version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80cf6f7806607bd58ad490bab34bf60e25455ea4aaf995f897a13324d41ea580" checksum = "068208f2b6fcfa27a7f1ee37488d2bb8ba2640f68f5475d08e1d9130696aba59"
dependencies = [ dependencies = [
"ahash", "ahash",
"base64 0.13.1", "base64 0.13.1",
"bitvec", "bitvec",
"chrono", "chrono",
"hex", "hex",
"indexmap 2.2.5", "indexmap 2.5.0",
"js-sys", "js-sys",
"once_cell", "once_cell",
"rand", "rand 0.8.5",
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json", "serde_json",
@ -326,6 +332,18 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cgmath"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64a4b57c8f4e3a2e9ac07e0f6abc9c24b6fc9e1b54c3478cfb598f3d0023e51c"
dependencies = [
"approx 0.1.1",
"mint",
"num-traits 0.1.43",
"rand 0.4.6",
]
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.38" version = "0.4.38"
@ -335,7 +353,7 @@ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits 0.2.18",
"serde", "serde",
"wasm-bindgen", "wasm-bindgen",
"windows-targets 0.52.4", "windows-targets 0.52.4",
@ -496,7 +514,7 @@ dependencies = [
"futures", "futures",
"is-terminal", "is-terminal",
"itertools 0.10.5", "itertools 0.10.5",
"num-traits", "num-traits 0.2.18",
"once_cell", "once_cell",
"oorandom", "oorandom",
"plotters", "plotters",
@ -672,7 +690,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.26" version = "0.1.27"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -754,6 +772,26 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "enum-iterator"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635"
dependencies = [
"enum-iterator-derive",
]
[[package]]
name = "enum-iterator-derive"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -770,6 +808,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "euler"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f19d11568a4a46aee488bdab3a2963e5e2c3cfd6091aa0abceaddcea82c0bc1"
dependencies = [
"approx 0.1.1",
"cgmath",
]
[[package]] [[package]]
name = "expectorate" name = "expectorate"
version = "1.1.0" version = "1.1.0"
@ -834,6 +882,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]] [[package]]
name = "funty" name = "funty"
version = "2.0.0" version = "2.0.0"
@ -1013,7 +1067,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http 0.2.12", "http 0.2.12",
"indexmap 2.2.5", "indexmap 2.5.0",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -1211,7 +1265,7 @@ checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
"num-traits", "num-traits 0.2.18",
"png", "png",
] ]
@ -1239,12 +1293,13 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.2.5" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.3", "hashbrown 0.14.3",
"serde",
] ]
[[package]] [[package]]
@ -1345,10 +1400,10 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.14" version = "0.2.17"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx 0.5.1",
"async-recursion", "async-recursion",
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
@ -1368,10 +1423,12 @@ dependencies = [
"http 0.2.12", "http 0.2.12",
"iai", "iai",
"image", "image",
"indexmap 2.5.0",
"insta", "insta",
"itertools 0.13.0", "itertools 0.13.0",
"js-sys", "js-sys",
"kittycad", "kittycad",
"kittycad-modeling-cmds",
"lazy_static", "lazy_static",
"measurements", "measurements",
"mime_guess", "mime_guess",
@ -1417,7 +1474,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.10" version = "0.1.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper", "hyper",
@ -1449,7 +1506,7 @@ dependencies = [
"mime_guess", "mime_guess",
"parse-display", "parse-display",
"phonenumber", "phonenumber",
"rand", "rand 0.8.5",
"reqwest", "reqwest",
"reqwest-conditional-middleware", "reqwest-conditional-middleware",
"reqwest-middleware", "reqwest-middleware",
@ -1467,6 +1524,54 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5128ba683e849388cac4214b65911c343842d559bec10827575c840a47733786"
dependencies = [
"anyhow",
"chrono",
"data-encoding",
"enum-iterator",
"enum-iterator-derive",
"euler",
"http 0.2.12",
"kittycad-modeling-cmds-macros",
"kittycad-unit-conversion-derive",
"measurements",
"parse-display",
"parse-display-derive",
"schemars",
"serde",
"serde_bytes",
"serde_json",
"uuid",
]
[[package]]
name = "kittycad-modeling-cmds-macros"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0cdc505a33bfffb87c317435ec41ced8f73474217cf30db685e479bf289757e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "kittycad-unit-conversion-derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7001c46a92c1edce6722a3900539b198230980799035f02d92b4e7df3fc08738"
dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -1600,6 +1705,12 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "mint"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.1" version = "1.0.1"
@ -1639,7 +1750,7 @@ checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"num-integer", "num-integer",
"num-traits", "num-traits 0.2.18",
] ]
[[package]] [[package]]
@ -1654,7 +1765,16 @@ version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [ dependencies = [
"num-traits", "num-traits 0.2.18",
]
[[package]]
name = "num-traits"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
dependencies = [
"num-traits 0.2.18",
] ]
[[package]] [[package]]
@ -1677,9 +1797,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.19.0" version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe"
[[package]] [[package]]
name = "oncemutex" name = "oncemutex"
@ -1714,7 +1834,7 @@ dependencies = [
"lazy_static", "lazy_static",
"percent-encoding", "percent-encoding",
"pin-project", "pin-project",
"rand", "rand 0.8.5",
"thiserror", "thiserror",
] ]
@ -1873,7 +1993,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
dependencies = [ dependencies = [
"num-traits", "num-traits 0.2.18",
"plotters-backend", "plotters-backend",
"plotters-svg", "plotters-svg",
"wasm-bindgen", "wasm-bindgen",
@ -1928,9 +2048,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
version = "1.4.0" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [ dependencies = [
"diff", "diff",
"yansi", "yansi",
@ -1971,9 +2091,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.22.2" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831e8e819a138c36e212f3af3fd9eeffed6bf1510a805af35b0edee5ffa59433" checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"indoc", "indoc",
@ -1989,9 +2109,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.22.2" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8730e591b14492a8945cdff32f089250b05f5accecf74aeddf9e8272ce1fa8" checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"target-lexicon", "target-lexicon",
@ -1999,9 +2119,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.22.2" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e97e919d2df92eb88ca80a037969f44e5e70356559654962cbb3316d00300c6" checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@ -2009,9 +2129,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.22.2" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb57983022ad41f9e683a599f2fd13c3664d7063a3ac5714cae4b7bee7d3f206" checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@ -2021,9 +2141,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.22.2" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec480c0c51ddec81019531705acac51bcdbeae563557c982aa8263bb96880372" checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -2056,6 +2176,19 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -2064,7 +2197,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha",
"rand_core", "rand_core 0.6.4",
] ]
[[package]] [[package]]
@ -2074,9 +2207,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.6.4",
] ]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.6.4" version = "0.6.4"
@ -2106,6 +2254,15 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.16" version = "0.2.16"
@ -2291,7 +2448,7 @@ checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"rand", "rand 0.8.5",
] ]
[[package]] [[package]]
@ -2378,9 +2535,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.7.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pemfile 2.1.1", "rustls-pemfile 2.1.1",
@ -2410,9 +2567,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.4.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868e20fada228fefaf6b652e00cc73623d54f8171e7352c18bb281571f2d92da" checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
@ -2550,9 +2707,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_bytes" name = "serde_bytes"
version = "0.11.14" version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -2585,7 +2742,7 @@ version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [ dependencies = [
"indexmap 2.2.5", "indexmap 2.5.0",
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
@ -3058,9 +3215,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.23.1" version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@ -3113,7 +3270,7 @@ version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [ dependencies = [
"indexmap 2.2.5", "indexmap 2.5.0",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
@ -3270,11 +3427,12 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "ts-rs" name = "ts-rs"
version = "9.0.1" version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c" checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9"
dependencies = [ dependencies = [
"chrono", "chrono",
"lazy_static",
"serde_json", "serde_json",
"thiserror", "thiserror",
"ts-rs-macros", "ts-rs-macros",
@ -3284,9 +3442,9 @@ dependencies = [
[[package]] [[package]]
name = "ts-rs-macros" name = "ts-rs-macros"
version = "9.0.1" version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130" checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3296,9 +3454,9 @@ dependencies = [
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.23.0" version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
@ -3306,7 +3464,7 @@ dependencies = [
"http 1.1.0", "http 1.1.0",
"httparse", "httparse",
"log", "log",
"rand", "rand 0.8.5",
"rustls 0.23.7", "rustls 0.23.7",
"rustls-pki-types", "rustls-pki-types",
"sha1", "sha1",
@ -3570,6 +3728,7 @@ dependencies = [
"js-sys", "js-sys",
"kcl-lib", "kcl-lib",
"kittycad", "kittycad",
"kittycad-modeling-cmds",
"pretty_assertions", "pretty_assertions",
"reqwest", "reqwest",
"serde_json", "serde_json",
@ -3839,9 +3998,9 @@ dependencies = [
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "0.5.1" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
@ -3879,6 +4038,6 @@ dependencies = [
"crc32fast", "crc32fast",
"crossbeam-utils", "crossbeam-utils",
"displaydoc", "displaydoc",
"indexmap 2.2.5", "indexmap 2.5.0",
"thiserror", "thiserror",
] ]

View File

@ -10,7 +10,7 @@ rust-version = "1.73"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
bson = { version = "2.12.0", features = ["uuid-1", "chrono"] } bson = { version = "2.13.0", features = ["uuid-1", "chrono"] }
data-encoding = "2.6.0" data-encoding = "2.6.0"
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
@ -27,7 +27,8 @@ anyhow = "1"
hyper = { version = "0.14.29", features = ["server", "http1"] } hyper = { version = "0.14.29", features = ["server", "http1"] }
image = { version = "0.25.1", default-features = false, features = ["png"] } image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, default-features = true } kittycad = { workspace = true, default-features = true }
pretty_assertions = "1.4.0" kittycad-modeling-cmds = { workspace = true }
pretty_assertions = "1.4.1"
reqwest = { version = "0.11.26", default-features = false } reqwest = { version = "0.11.26", default-features = false }
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] } tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8" twenty-twenty = "0.8"
@ -72,6 +73,7 @@ members = [
http = "0.2.12" http = "0.2.12"
kittycad = { version = "0.3.20", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.3.20", default-features = false, features = ["js", "requests"] }
kittycad-modeling-session = "0.1.4" kittycad-modeling-session = "0.1.4"
kittycad-modeling-cmds = { version = "0.2.61", features = ["websocket"] }
[[test]] [[test]]
name = "executor" name = "executor"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "derive-docs" name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros" description = "A tool for generating documentation from Rust derive macros"
version = "0.1.26" version = "0.1.27"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -14,7 +14,7 @@ proc-macro = true
[dependencies] [dependencies]
Inflector = "0.11.4" Inflector = "0.11.4"
convert_case = "0.6.0" convert_case = "0.6.0"
once_cell = "1.19.0" once_cell = "1.20.0"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
regex = "1.10" regex = "1.10"
@ -23,7 +23,7 @@ serde_tokenstream = "0.2"
syn = { version = "2.0.77", features = ["full"] } syn = { version = "2.0.77", features = ["full"] }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.88" anyhow = "1.0.89"
expectorate = "1.1.0" expectorate = "1.1.0"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.1"
rustfmt-wrapper = "0.2.1" rustfmt-wrapper = "0.2.1"

View File

@ -269,7 +269,7 @@ fn do_stdlib_inner(
let ty_string = rust_type_to_openapi_type(&ty_string); let ty_string = rust_type_to_openapi_type(&ty_string);
let required = !ty_ident.to_string().starts_with("Option <"); let required = !ty_ident.to_string().starts_with("Option <");
if ty_string != "Args" { if ty_string != "ExecState" && ty_string != "Args" {
let schema = if ty_ident.to_string().starts_with("Vec < ") let schema = if ty_ident.to_string().starts_with("Vec < ")
|| ty_ident.to_string().starts_with("Option <") || ty_ident.to_string().starts_with("Option <")
|| ty_ident.to_string().starts_with('[') || ty_ident.to_string().starts_with('[')
@ -387,11 +387,12 @@ fn do_stdlib_inner(
#const_struct #const_struct
fn #boxed_fn_name_ident( fn #boxed_fn_name_ident(
exec_state: &mut crate::executor::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>> + Send>, Box<dyn std::future::Future<Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>> + Send + '_>,
> { > {
Box::pin(#fn_name_ident(args)) Box::pin(#fn_name_ident(exec_state, args))
} }
impl #docs_crate::StdLibFn for #name_ident impl #docs_crate::StdLibFn for #name_ident
@ -662,6 +663,9 @@ fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
.replace("mut", "") .replace("mut", "")
.replace("< 'a >", "") .replace("< 'a >", "")
.replace(' ', ""); .replace(' ', "");
if ty_string.starts_with("ExecState") {
ty_string = "ExecState".to_string();
}
if ty_string.starts_with("Args") { if ty_string.starts_with("Args") {
ty_string = "Args".to_string(); ty_string = "Args".to_string();
} }

View File

@ -85,6 +85,32 @@ fn test_args_with_lifetime() {
expectorate::assert_contents("tests/args_with_lifetime.gen", &get_text_fmt(&item).unwrap()); expectorate::assert_contents("tests/args_with_lifetime.gen", &get_text_fmt(&item).unwrap());
} }
#[test]
fn test_args_with_exec_state() {
let (item, mut errors) = do_stdlib(
quote! {
name = "someFunction",
},
quote! {
/// Docs
/// ```
/// someFunction()
/// ```
fn inner_some_function<'a>(
exec_state: &mut ExecState,
args: &Args,
) -> i32 {
3
}
},
)
.unwrap();
if let Some(e) = errors.pop() {
panic!("{e}");
}
expectorate::assert_contents("tests/test_args_with_exec_state.gen", &get_text_fmt(&item).unwrap());
}
#[test] #[test]
fn test_stdlib_line_to() { fn test_stdlib_line_to() {
let (item, errors) = do_stdlib( let (item, errors) = do_stdlib(

View File

@ -44,15 +44,17 @@ pub(crate) struct SomeFn {}
#[doc = "Std lib function: someFn\nDocs"] #[doc = "Std lib function: someFn\nDocs"]
pub(crate) const SomeFn: SomeFn = SomeFn {}; pub(crate) const SomeFn: SomeFn = SomeFn {};
fn boxed_someFn( fn boxed_someFn(
exec_state: &mut crate::executor::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>,
> + Send, > + Send
+ '_,
>, >,
> { > {
Box::pin(someFn(args)) Box::pin(someFn(exec_state, args))
} }
impl crate::docs::StdLibFn for SomeFn { impl crate::docs::StdLibFn for SomeFn {

View File

@ -44,15 +44,17 @@ pub(crate) struct SomeFn {}
#[doc = "Std lib function: someFn\nDocs"] #[doc = "Std lib function: someFn\nDocs"]
pub(crate) const SomeFn: SomeFn = SomeFn {}; pub(crate) const SomeFn: SomeFn = SomeFn {};
fn boxed_someFn( fn boxed_someFn(
exec_state: &mut crate::executor::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>,
> + Send, > + Send
+ '_,
>, >,
> { > {
Box::pin(someFn(args)) Box::pin(someFn(exec_state, args))
} }
impl crate::docs::StdLibFn for SomeFn { impl crate::docs::StdLibFn for SomeFn {

View File

@ -77,15 +77,17 @@ pub(crate) struct Show {}
#[doc = "Std lib function: show\nThis is some function.\nIt does shit."] #[doc = "Std lib function: show\nThis is some function.\nIt does shit."]
pub(crate) const Show: Show = Show {}; pub(crate) const Show: Show = Show {};
fn boxed_show( fn boxed_show(
exec_state: &mut crate::executor::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>,
> + Send, > + Send
+ '_,
>, >,
> { > {
Box::pin(show(args)) Box::pin(show(exec_state, args))
} }
impl crate::docs::StdLibFn for Show { impl crate::docs::StdLibFn for Show {

View File

@ -44,15 +44,17 @@ pub(crate) struct Show {}
#[doc = "Std lib function: show\nThis is some function.\nIt does shit."] #[doc = "Std lib function: show\nThis is some function.\nIt does shit."]
pub(crate) const Show: Show = Show {}; pub(crate) const Show: Show = Show {};
fn boxed_show( fn boxed_show(
exec_state: &mut crate::executor::ExecState,
args: crate::std::Args, args: crate::std::Args,
) -> std::pin::Pin< ) -> std::pin::Pin<
Box< Box<
dyn std::future::Future< dyn std::future::Future<
Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>, Output = anyhow::Result<crate::executor::KclValue, crate::errors::KclError>,
> + Send, > + Send
+ '_,
>, >,
> { > {
Box::pin(show(args)) Box::pin(show(exec_state, args))
} }
impl crate::docs::StdLibFn for Show { impl crate::docs::StdLibFn for Show {

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