Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
0add26cf61 | |||
b54fc534c2 | |||
c66f851a3f | |||
13b8ab71d8 | |||
bdeab4f87d | |||
05ccf5e2f4 | |||
7ab015d783 | |||
3d6cfa980f | |||
9f5f1eb8c3 | |||
50fcdff879 | |||
efaae2b193 | |||
7e4ebacb72 | |||
72482506c3 | |||
a51b5b09a3 | |||
53ccc1ed6c | |||
8106749ccf | |||
081e34a600 |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
|
||||
|
5
.github/workflows/cargo-clippy.yml
vendored
@ -54,3 +54,8 @@ jobs:
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo clippy --all --tests --benches -- -D warnings
|
||||
# If this fails, run "cargo check" to update Cargo.lock,
|
||||
# then add Cargo.lock to the PR.
|
||||
- name: Check Cargo.lock doesn't need updating
|
||||
run: |
|
||||
cargo check --locked || echo "Pls run cargo check and commit the changed Cargo.lock"
|
||||
|
323
docs/kcl/chamfer.md
Normal file
@ -23,6 +23,7 @@ layout: manual
|
||||
* [`atan`](kcl/atan)
|
||||
* [`bezierCurve`](kcl/bezierCurve)
|
||||
* [`ceil`](kcl/ceil)
|
||||
* [`chamfer`](kcl/chamfer)
|
||||
* [`circle`](kcl/circle)
|
||||
* [`close`](kcl/close)
|
||||
* [`cos`](kcl/cos)
|
||||
@ -64,6 +65,7 @@ layout: manual
|
||||
* [`segEndX`](kcl/segEndX)
|
||||
* [`segEndY`](kcl/segEndY)
|
||||
* [`segLen`](kcl/segLen)
|
||||
* [`shell`](kcl/shell)
|
||||
* [`sin`](kcl/sin)
|
||||
* [`sqrt`](kcl/sqrt)
|
||||
* [`startProfileAt`](kcl/startProfileAt)
|
||||
|
@ -9,7 +9,7 @@ A circular pattern on a 2D sketch.
|
||||
|
||||
|
||||
```js
|
||||
patternCircular2d(data: CircularPattern2dData, sketch_group: SketchGroup) -> [SketchGroup]
|
||||
patternCircular2d(data: CircularPattern2dData, sketch_group_set: SketchGroupSet) -> [SketchGroup]
|
||||
```
|
||||
|
||||
### Examples
|
||||
@ -48,7 +48,7 @@ const example = extrude(1, exampleSketch)
|
||||
rotateDuplicates: string,
|
||||
}
|
||||
```
|
||||
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED)
|
||||
* `sketch_group_set`: `SketchGroupSet` - A sketch group or a group of sketch groups. (REQUIRED)
|
||||
```js
|
||||
{
|
||||
// The plane id or face id of the sketch group.
|
||||
@ -129,6 +129,7 @@ const example = extrude(1, exampleSketch)
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
},
|
||||
type: "sketchGroup",
|
||||
// The paths in the sketch group.
|
||||
value: [{
|
||||
// The from point.
|
||||
@ -212,6 +213,9 @@ const example = extrude(1, exampleSketch)
|
||||
y: number,
|
||||
z: number,
|
||||
},
|
||||
} |
|
||||
{
|
||||
type: "sketchGroups",
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -9,7 +9,7 @@ A circular pattern on a 3D model.
|
||||
|
||||
|
||||
```js
|
||||
patternCircular3d(data: CircularPattern3dData, extrude_group: ExtrudeGroup) -> [ExtrudeGroup]
|
||||
patternCircular3d(data: CircularPattern3dData, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]
|
||||
```
|
||||
|
||||
### Examples
|
||||
@ -47,7 +47,7 @@ const example = extrude(-5, exampleSketch)
|
||||
rotateDuplicates: string,
|
||||
}
|
||||
```
|
||||
* `extrude_group`: `ExtrudeGroup` - An extrude group is a collection of extrude surfaces. (REQUIRED)
|
||||
* `extrude_group_set`: `ExtrudeGroupSet` - A extrude group or a group of extrude groups. (REQUIRED)
|
||||
```js
|
||||
{
|
||||
// The id of the extrusion end cap
|
||||
@ -127,6 +127,7 @@ const example = extrude(-5, exampleSketch)
|
||||
}],
|
||||
// The id of the extrusion start cap
|
||||
startCapId: uuid,
|
||||
type: "extrudeGroup",
|
||||
// The extrude surfaces.
|
||||
value: [{
|
||||
// The face id for the extrude plane.
|
||||
@ -176,6 +177,9 @@ const example = extrude(-5, exampleSketch)
|
||||
y: number,
|
||||
z: number,
|
||||
},
|
||||
} |
|
||||
{
|
||||
type: "extrudeGroups",
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -9,7 +9,7 @@ A linear pattern on a 3D model.
|
||||
|
||||
|
||||
```js
|
||||
patternLinear3d(data: LinearPattern3dData, extrude_group: ExtrudeGroup) -> [ExtrudeGroup]
|
||||
patternLinear3d(data: LinearPattern3dData, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]
|
||||
```
|
||||
|
||||
### Examples
|
||||
@ -45,7 +45,7 @@ const example = extrude(1, exampleSketch)
|
||||
repetitions: number,
|
||||
}
|
||||
```
|
||||
* `extrude_group`: `ExtrudeGroup` - An extrude group is a collection of extrude surfaces. (REQUIRED)
|
||||
* `extrude_group_set`: `ExtrudeGroupSet` - A extrude group or a group of extrude groups. (REQUIRED)
|
||||
```js
|
||||
{
|
||||
// The id of the extrusion end cap
|
||||
@ -125,6 +125,7 @@ const example = extrude(1, exampleSketch)
|
||||
}],
|
||||
// The id of the extrusion start cap
|
||||
startCapId: uuid,
|
||||
type: "extrudeGroup",
|
||||
// The extrude surfaces.
|
||||
value: [{
|
||||
// The face id for the extrude plane.
|
||||
@ -174,6 +175,9 @@ const example = extrude(1, exampleSketch)
|
||||
y: number,
|
||||
z: number,
|
||||
},
|
||||
} |
|
||||
{
|
||||
type: "extrudeGroups",
|
||||
}
|
||||
```
|
||||
|
||||
|
311
docs/kcl/shell.md
Normal file
10081
docs/kcl/std.json
@ -411,6 +411,47 @@ test('ensure the Zoo logo is not a link in browser app', async ({ page }) => {
|
||||
await expect(zooLogo).not.toHaveAttribute('href')
|
||||
})
|
||||
|
||||
test('if you write kcl with lint errors you get lints', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// check no error to begin with
|
||||
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type('const my_snake_case_var = 5')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const myCamelCaseVar = 5')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// press arrows to clear autocomplete
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await page.keyboard.press('ArrowRight')
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-info')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-info')
|
||||
await expect(
|
||||
page.getByText('Identifiers must be lowerCamelCase')
|
||||
).toBeVisible()
|
||||
|
||||
// select the line that's causing the error and delete it
|
||||
await page.getByText('const my_snake_case_var = 5').click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.press('Home')
|
||||
await page.keyboard.up('Shift')
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// wait for .cm-lint-marker-info not to be visible
|
||||
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
@ -421,8 +462,8 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
// check no error to begin with
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
/* add the following code to the editor (# error is not a valid line)
|
||||
# error
|
||||
/* add the following code to the editor ($ error is not a valid line)
|
||||
$ error
|
||||
const topAng = 30
|
||||
const bottomAng = 25
|
||||
*/
|
||||
@ -463,6 +504,8 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
await page.keyboard.type("// Let's define the same thing twice")
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const topAng = 42')
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await page.keyboard.press('ArrowRight')
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
await expect(page.locator('.cm-lintRange.cm-lintRange-error')).toBeVisible()
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.22.1",
|
||||
"version": "0.22.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
@ -10,7 +10,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.64",
|
||||
"@kittycad/lib": "^0.0.67",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
|
1666
src-tauri/Cargo.lock
generated
@ -16,11 +16,11 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
|
||||
kittycad = "0.3.0"
|
||||
kittycad = "0.3.5"
|
||||
log = "0.4.21"
|
||||
oauth2 = "4.4.2"
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
||||
tauri = { version = "2.0.0-beta.22", features = [ "devtools", "unstable"] }
|
||||
tauri-plugin-cli = { version = "2.0.0-beta.3" }
|
||||
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
|
||||
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
||||
|
@ -63,16 +63,17 @@
|
||||
"subcommands": {}
|
||||
},
|
||||
"deep-link": {
|
||||
"domains": [
|
||||
{
|
||||
"host": "app.zoo.dev"
|
||||
}
|
||||
]
|
||||
"mobile": [],
|
||||
"desktop": {
|
||||
"schemes": [
|
||||
"app.zoo.dev"
|
||||
]
|
||||
}
|
||||
},
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.22.1"
|
||||
"version": "0.22.2"
|
||||
}
|
||||
|
@ -71,6 +71,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
bug: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.8209 5.99884C10.6403 5.73962 10.3399 5.57001 10 5.57001C9.65984 5.57001 9.35936 5.73984 9.17871 5.99935C9.43724 5.95129 9.71142 5.92578 10.0012 5.92578C10.29 5.92578 10.5633 5.95111 10.8209 5.99884ZM10 4.57001C8.9459 4.57001 8.08227 5.38548 8.00554 6.41997C7.58916 6.65398 7.23724 6.95989 6.95014 7.31304L5.85355 6.21645L5.14645 6.92356L6.40931 8.18642C6.20774 8.62503 6.08043 9.09624 6.0278 9.57001H5V10.57H6.01946C6.06396 11.1581 6.1867 11.8173 6.4071 12.4558L5.14645 13.7165L5.85355 14.4236L6.8408 13.4363C7.46354 14.555 8.47307 15.4258 10.0012 15.4258C11.529 15.4258 12.5378 14.5554 13.16 13.4371L14.1464 14.4236L14.8536 13.7165L13.5934 12.4563C13.8136 11.8177 13.9362 11.1583 13.9806 10.57H15V9.57001H13.9722C13.9197 9.0961 13.7925 8.62474 13.5911 8.18602L14.8536 6.92356L14.1464 6.21645L13.0505 7.31239C12.7633 6.95894 12.4112 6.65285 11.9944 6.41883C11.9171 5.38488 11.0537 4.57001 10 4.57001ZM10.5 14.3801V8.57001H9.5V14.3796C8.72105 14.2298 8.15885 13.7245 7.7428 12.9999C7.22316 12.095 7 10.937 7 10.07C7 8.46381 8.04281 6.92578 10.0012 6.92578C11.9589 6.92578 13 8.4629 13 10.07C13 10.9373 12.7773 12.0954 12.2582 13.0003C11.8422 13.7254 11.2799 14.2309 10.5 14.3801Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
checkmark: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
@ -33,7 +33,7 @@ export function LowerRightControls(props: React.PropsWithChildren) {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<CustomIcon
|
||||
name="exclamationMark"
|
||||
name="bug"
|
||||
className={`w-5 h-5 ${linkOverrideClassName}`}
|
||||
/>
|
||||
<Tooltip position="top">Report a bug</Tooltip>
|
||||
|
@ -24,9 +24,9 @@ export function RefreshButton() {
|
||||
return (
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-10 dark:border-chalkboard-100"
|
||||
className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
|
||||
>
|
||||
<CustomIcon name="arrowRotateRight" className="w-5 h-5" />
|
||||
<CustomIcon name="exclamationMark" className="w-5 h-5" />
|
||||
<Tooltip position="bottom-right">
|
||||
<span>Refresh and report</span>
|
||||
<br />
|
||||
|
@ -171,7 +171,9 @@ export const SettingsAuthProviderBase = ({
|
||||
})
|
||||
},
|
||||
'Execute AST': () => kclManager.executeCode(true, true),
|
||||
persistSettings: (context) =>
|
||||
},
|
||||
services: {
|
||||
'Persist settings': (context) =>
|
||||
saveSettings(context, loadedProject?.project?.path),
|
||||
},
|
||||
}
|
||||
|
@ -11,30 +11,8 @@
|
||||
--_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2));
|
||||
--_p-block: 4px;
|
||||
--_bg: var(--chalkboard-10);
|
||||
--_shadow-alpha: 5%;
|
||||
--_shadow-alpha: 8%;
|
||||
--_theme-alpha: 0.15;
|
||||
--_theme-outline: drop-shadow(
|
||||
0 1px 0
|
||||
oklch(
|
||||
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
|
||||
var(--_theme-alpha)
|
||||
)
|
||||
)
|
||||
drop-shadow(
|
||||
0 -1px 0 oklch(var(--primary-lightness) var(--primary-chroma)
|
||||
var(--primary-hue) / var(--_theme-alpha))
|
||||
)
|
||||
drop-shadow(
|
||||
1px 0 0
|
||||
oklch(
|
||||
var(--primary-lightness) var(--primary-chroma) var(--primary-hue) /
|
||||
var(--_theme-alpha)
|
||||
)
|
||||
)
|
||||
drop-shadow(
|
||||
-1px 0 0 oklch(var(--primary-lightness) var(--primary-chroma)
|
||||
var(--primary-hue) / var(--_theme-alpha))
|
||||
);
|
||||
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
@ -61,16 +39,15 @@
|
||||
background: var(--_bg);
|
||||
@apply text-chalkboard-110;
|
||||
will-change: filter;
|
||||
filter: drop-shadow(0 1px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
drop-shadow(0 4px 8px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
var(--_theme-outline);
|
||||
filter: drop-shadow(0 1px 2px hsl(0 0% 0% / var(--_shadow-alpha)))
|
||||
drop-shadow(0 4px 6px hsl(0 0% 0% / calc(var(--_shadow-alpha) / 2)));
|
||||
}
|
||||
|
||||
:global(.dark) .tooltip {
|
||||
--_bg: var(--chalkboard-110);
|
||||
--_bg: var(--chalkboard-90);
|
||||
--_theme-alpha: 40%;
|
||||
--_shadow-alpha: 16%;
|
||||
@apply text-chalkboard-10;
|
||||
filter: var(--_theme-outline);
|
||||
}
|
||||
|
||||
.tooltip:dir(rtl) {
|
||||
@ -109,7 +86,7 @@
|
||||
}
|
||||
|
||||
/* Sometimes there's no visible label,
|
||||
* so we'll use the tooltip as the label
|
||||
* so we'll use the tooltip as the label
|
||||
*/
|
||||
.tooltip:only-child::before {
|
||||
content: 'Tooltip:';
|
||||
|
@ -7,7 +7,11 @@ import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
import { addLineHighlight } from './highlightextension'
|
||||
import { setDiagnostics, Diagnostic } from '@codemirror/lint'
|
||||
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
|
||||
|
||||
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
|
||||
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
|
||||
}
|
||||
|
||||
export default class EditorManager {
|
||||
private _editorView: EditorView | null = null
|
||||
@ -91,11 +95,38 @@ export default class EditorManager {
|
||||
}
|
||||
}
|
||||
|
||||
clearDiagnostics(): void {
|
||||
if (!this.editorView) return
|
||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
|
||||
}
|
||||
|
||||
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this.editorView) return
|
||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
|
||||
}
|
||||
|
||||
addDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this.editorView) return
|
||||
|
||||
forEachDiagnostic(this.editorView.state, function (diag) {
|
||||
diagnostics.push(diag)
|
||||
})
|
||||
|
||||
const uniqueDiagnostics = new Set<Diagnostic>()
|
||||
diagnostics.forEach((diagnostic) => {
|
||||
for (const knownDiagnostic of uniqueDiagnostics.values()) {
|
||||
if (diagnosticIsEqual(diagnostic, knownDiagnostic)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
uniqueDiagnostics.add(diagnostic)
|
||||
})
|
||||
|
||||
this.editorView.dispatch(
|
||||
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
|
||||
)
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this._editorView) {
|
||||
undo(this._editorView)
|
||||
|
@ -382,9 +382,14 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
try {
|
||||
switch (notification.method) {
|
||||
case 'textDocument/publishDiagnostics':
|
||||
//const params = notification.params as PublishDiagnosticsParams
|
||||
console.log(
|
||||
'[lsp] [window/publishDiagnostics]',
|
||||
this.client.getName(),
|
||||
notification.params
|
||||
)
|
||||
const params = notification.params as PublishDiagnosticsParams
|
||||
// this is sometimes slower than our actual typing.
|
||||
//this.processDiagnostics(params)
|
||||
this.processDiagnostics(params)
|
||||
break
|
||||
case 'window/logMessage':
|
||||
console.log(
|
||||
|
@ -89,9 +89,10 @@ export class KclManager {
|
||||
return this._kclErrors
|
||||
}
|
||||
set kclErrors(kclErrors) {
|
||||
console.log('[lsp] not lsp, actually typescript: ', kclErrors)
|
||||
this._kclErrors = kclErrors
|
||||
let diagnostics = kclErrorsToDiagnostics(kclErrors)
|
||||
editorManager.setDiagnostics(diagnostics)
|
||||
editorManager.addDiagnostics(diagnostics)
|
||||
this._kclErrorsCallBack(kclErrors)
|
||||
}
|
||||
|
||||
@ -185,6 +186,11 @@ export class KclManager {
|
||||
const currentExecutionId = executionId || Date.now()
|
||||
this._cancelTokens.set(currentExecutionId, false)
|
||||
|
||||
// here we're going to clear diagnostics since we're the first
|
||||
// one in. We're the only location where diagnostics are cleared;
|
||||
// everything from here on out should be *appending*.
|
||||
editorManager.clearDiagnostics()
|
||||
|
||||
this.isExecuting = true
|
||||
await this.ensureWasmInit()
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
@ -234,6 +240,7 @@ export class KclManager {
|
||||
} = { updates: 'none' }
|
||||
) {
|
||||
await this.ensureWasmInit()
|
||||
|
||||
const newCode = recast(ast)
|
||||
const newAst = this.safeParse(newCode)
|
||||
if (!newAst) return
|
||||
@ -243,6 +250,11 @@ export class KclManager {
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
this._ast = { ...newAst }
|
||||
|
||||
// here we're going to clear diagnostics since we're the first
|
||||
// one in. We're the only location where diagnostics are cleared;
|
||||
// everything from here on out should be *appending*.
|
||||
editorManager.clearDiagnostics()
|
||||
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast: newAst,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
|
@ -17,15 +17,17 @@ const prependRoutes =
|
||||
)
|
||||
}
|
||||
|
||||
type OnboardingPaths = {
|
||||
[K in keyof typeof onboardingPaths]: `/onboarding${(typeof onboardingPaths)[K]}`
|
||||
}
|
||||
|
||||
export const paths = {
|
||||
INDEX: '/',
|
||||
HOME: '/home',
|
||||
FILE: '/file',
|
||||
SETTINGS: '/settings',
|
||||
SIGN_IN: '/signin',
|
||||
ONBOARDING: prependRoutes(onboardingPaths)(
|
||||
'/onboarding'
|
||||
) as typeof onboardingPaths,
|
||||
ONBOARDING: prependRoutes(onboardingPaths)('/onboarding') as OnboardingPaths,
|
||||
} as const
|
||||
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
|
||||
|
||||
|
@ -11,98 +11,98 @@ import {
|
||||
|
||||
export const settingsMachine = createMachine(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IAbAFZN+AOwAWAIwAOYwE4AzGYBM+-ZosAaEAE9Eh62LP51ls+v0LMWt1awMAX3DnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BA9vQ0N1CwCxdVbdY1DnNwQzPp8zTTFje1D1QwtjSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmotNY3w7YysHRuNDTXV1bvdG7w-IKTcbaazWCzTEAxOZ4QiLJIrFL4dJZMAXUpXSrVDTGazPMQPR4GXRBAx-XoGfDWIadMx4n6EqEwuLwxLLVbrTbbNKYKBpLb8tAAUWgcAxMjk11uoHumn0+DEw2sJkMulCgWsFL6YnwfX0ELsYg61jMumZs1ZCXIACU4OgAATLWGiSSXKXYu4aLz4UwWBr6DqBYYUzSePXqUlBLxmyZmC2xeZs8gxB3UYjEJ2W+YSsoem5VL0IKy6z6EsJifQ2Czq0MTHzq8Yjfz6MQmBMu5NkABUuaxBZxCDD+DD5lJgUjxssFP0xl0I+0pm06uMmg8kSiIGwXPgpRZ83dFQHRYAtA0LCO2tZjGIHJNdB5fq5EK3dc1OsNGkrA+oO1bFoe0qFrKiAnlol7BDed5zo+FK+Doc7qhCNaVv4UwbkAA */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmsGxfCMPM08PQaDNU0cXRG1tLwedTaKxif7+UJTKIgGJzPCERZJFYpfDpLJgC6lK6VaqIEx6fBmCw2Do2IJ6MxdRDvTT4MRDdRGEzWbQ6ELTGGzOIIxLLVbrTbbNKYKBpLaitAAUWgcExMjk11uoBqVgM3jMYhsAIMrVs6ipPWChOeYhC9KMFhGHNh3IS5AASnB0AACZZw0SSS4KnF3PFafADTV1YZ2IxiH7dNpGfCaIzAgE+IzWMzBa1c+Y88gxZ3UYjEV3pvBysrem5VX0IFq0y3aTXWOp6JmU34IKMxuz0joGEYWsxp2IZu1kABUxexZdxtRG+EmQMZmne3dNBs0jKewLBsbCwI81n77vwtDAHHksDhBYHeDIEGYYEI2AAbowANZ3o8nzBnm3zMelpWqRAAFp62sJ4jEsZ4AT0UJGwjPFzH6cwNW0AwWXpbRImhbABXgUpvzwL0KgnCtgJMMCII8KCYLsA11EGOkXneDxmXMCk92hfCFlIQjFXLZUgLjddWhaFkgRCaxOhbEYzBnXwXkmOjAjjfduXfU9zzdOIeJ9fiEEA6ckwMClQ2BFpmJXMF9DjYI6hZfxmMw8IgA */
|
||||
id: 'Settings',
|
||||
predictableActionArguments: true,
|
||||
context: {} as ReturnType<typeof createSettings>,
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
entry: ['setThemeClass', 'setClientSideSceneUnits', 'persistSettings'],
|
||||
entry: ['setThemeClass', 'setClientSideSceneUnits'],
|
||||
|
||||
on: {
|
||||
'*': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: ['setSettingAtLevel', 'toastSuccess', 'persistSettings'],
|
||||
target: 'persisting settings',
|
||||
actions: ['setSettingAtLevel', 'toastSuccess'],
|
||||
},
|
||||
|
||||
'set.app.onboardingStatus': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
|
||||
target: 'persisting settings',
|
||||
|
||||
// No toast
|
||||
actions: ['setSettingAtLevel'],
|
||||
},
|
||||
'set.app.themeColor': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
|
||||
target: 'persisting settings',
|
||||
|
||||
// No toast
|
||||
actions: ['setSettingAtLevel'],
|
||||
},
|
||||
|
||||
'set.modeling.defaultUnit': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: [
|
||||
'setSettingAtLevel',
|
||||
'toastSuccess',
|
||||
'setClientSideSceneUnits',
|
||||
'Execute AST',
|
||||
'persistSettings',
|
||||
],
|
||||
},
|
||||
|
||||
'set.app.theme': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: [
|
||||
'setSettingAtLevel',
|
||||
'toastSuccess',
|
||||
'setThemeClass',
|
||||
'setEngineTheme',
|
||||
'persistSettings',
|
||||
'setClientTheme',
|
||||
],
|
||||
},
|
||||
|
||||
'set.modeling.highlightEdges': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'setSettingAtLevel',
|
||||
'toastSuccess',
|
||||
'setEngineEdges',
|
||||
'persistSettings',
|
||||
],
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: ['setSettingAtLevel', 'toastSuccess', 'setEngineEdges'],
|
||||
},
|
||||
|
||||
'Reset settings': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: [
|
||||
'resetSettings',
|
||||
'setThemeClass',
|
||||
'setEngineTheme',
|
||||
'setClientSideSceneUnits',
|
||||
'Execute AST',
|
||||
'persistSettings',
|
||||
'setClientTheme',
|
||||
],
|
||||
},
|
||||
|
||||
'Set all settings': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
target: 'persisting settings',
|
||||
|
||||
actions: [
|
||||
'setAllSettings',
|
||||
'setThemeClass',
|
||||
'setEngineTheme',
|
||||
'setClientSideSceneUnits',
|
||||
'Execute AST',
|
||||
'persistSettings',
|
||||
'setClientTheme',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'persisting settings': {
|
||||
invoke: {
|
||||
src: 'Persist settings',
|
||||
id: 'persistSettings',
|
||||
onDone: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
||||
schema: {
|
||||
|
@ -3,7 +3,7 @@ import { Outlet, useNavigate } from 'react-router-dom'
|
||||
import Introduction from './Introduction'
|
||||
import Camera from './Camera'
|
||||
import Sketching from './Sketching'
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import Streaming from './Streaming'
|
||||
@ -94,17 +94,31 @@ export function useNextClick(newStatus: string) {
|
||||
export function useDismiss() {
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const {
|
||||
settings: { send },
|
||||
settings: { state, send },
|
||||
} = useSettingsAuthContext()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return useCallback(() => {
|
||||
const settingsCallback = useCallback(() => {
|
||||
send({
|
||||
type: 'set.app.onboardingStatus',
|
||||
data: { level: 'user', value: 'dismissed' },
|
||||
})
|
||||
navigate(filePath)
|
||||
}, [send, navigate, filePath])
|
||||
}, [send])
|
||||
|
||||
/**
|
||||
* A "listener" for the XState to return to "idle" state
|
||||
* when the user dismisses the onboarding, using the callback above
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
state.context.app.onboardingStatus.user === 'dismissed' &&
|
||||
state.matches('idle')
|
||||
) {
|
||||
navigate(filePath)
|
||||
}
|
||||
}, [filePath, navigate, state])
|
||||
|
||||
return settingsCallback
|
||||
}
|
||||
|
||||
// Get the 1-indexed step number of the current onboarding step
|
||||
|
48
src/wasm-lib/Cargo.lock
generated
@ -297,9 +297,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bson"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d43b38e074cc0de2957f10947e376a1d88b9c4dbab340b590800cc1b2e066b2"
|
||||
checksum = "d8a88e82b9106923b5c4d6edfca9e7db958d4e98a478ec115022e81b9b38e2c8"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.13.1",
|
||||
@ -406,9 +406,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -416,9 +416,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.2"
|
||||
version = "4.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -430,9 +430,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.4"
|
||||
version = "4.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
|
||||
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@ -670,9 +670,9 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
|
||||
[[package]]
|
||||
name = "databake"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a"
|
||||
checksum = "6a04fbfbecca8f0679c8c06fef907594adcc3e2052e11163a6d30535a1a5604d"
|
||||
dependencies = [
|
||||
"databake-derive",
|
||||
"proc-macro2",
|
||||
@ -681,9 +681,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "databake-derive"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
|
||||
checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1369,7 +1369,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.58"
|
||||
version = "0.1.60"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -1436,9 +1436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.3"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0cbef813153197e60c0e96f59eea0b75f8418380f414b20250ee81b60e522c3"
|
||||
checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1451,7 +1451,7 @@ dependencies = [
|
||||
"format_serde_error",
|
||||
"futures",
|
||||
"http 0.2.12",
|
||||
"itertools 0.12.1",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"parse-display",
|
||||
@ -2037,9 +2037,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.4"
|
||||
version = "1.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -2378,9 +2378,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.20"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0218ceea14babe24a4a5836f86ade86c1effbc198164e619194cb5069187e29"
|
||||
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
|
||||
dependencies = [
|
||||
"bigdecimal",
|
||||
"bytes",
|
||||
@ -2395,9 +2395,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.20"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed5a1ccce8ff962e31a165d41f6e2a2dd1245099dc4d594f5574a86cd90f4d3"
|
||||
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2945,9 +2945,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.23.0"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0"
|
||||
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
|
@ -10,11 +10,11 @@ rust-version = "1.73"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
|
||||
clap = "4.5.4"
|
||||
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
|
||||
clap = "4.5.7"
|
||||
gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
kittycad = { workspace = true }
|
||||
kittycad.workspace = true
|
||||
serde_json = "1.0.116"
|
||||
tokio = { version = "1.38.0", features = ["sync"] }
|
||||
toml = "0.8.14"
|
||||
@ -68,7 +68,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
kittycad = { version = "0.3.3", default-features = false, features = ["js", "requests"] }
|
||||
kittycad = { version = "0.3.5", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-modeling-session = "0.1.4"
|
||||
|
||||
[[test]]
|
||||
|
@ -11,7 +11,7 @@ repository = "https://github.com/KittyCAD/modeling-app"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
databake = "0.1.7"
|
||||
databake = "0.1.8"
|
||||
kcl-lib = { path = "../kcl" }
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.1.58"
|
||||
version = "0.1.60"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -16,9 +16,9 @@ async-recursion = "1.1.1"
|
||||
async-trait = "0.1.80"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.5.4", default-features = false, optional = true }
|
||||
clap = { version = "4.5.7", default-features = false, optional = true }
|
||||
dashmap = "5.5.3"
|
||||
databake = { version = "0.1.7", features = ["derive"] }
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.18", path = "../derive-docs" }
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = { version = "0.3.30" }
|
||||
@ -54,9 +54,9 @@ web-sys = { version = "0.3.69", features = ["console"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
approx = "0.5"
|
||||
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
|
||||
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.23.0", features = ["rustls-tls-native-roots"] }
|
||||
tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] }
|
||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
|
||||
[features]
|
||||
|
@ -63,9 +63,10 @@ impl StdLibFnArg {
|
||||
|
||||
pub fn get_autocomplete_snippet(&self, index: usize) -> Result<Option<(usize, String)>> {
|
||||
if self.type_ == "SketchGroup"
|
||||
|| self.type_ == "ExtrudeGroup"
|
||||
|| self.type_ == "SketchSurface"
|
||||
|| self.type_ == "SketchGroupSet"
|
||||
|| self.type_ == "ExtrudeGroup"
|
||||
|| self.type_ == "ExtrudeGroupSet"
|
||||
|| self.type_ == "SketchSurface"
|
||||
{
|
||||
return Ok(Some((index, format!("${{{}:{}}}", index, "%"))));
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
use crate::executor::SourceRange;
|
||||
use crate::{executor::SourceRange, lsp::IntoDiagnostic};
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
|
||||
#[ts(export)]
|
||||
@ -42,19 +42,9 @@ pub struct KclErrorDetails {
|
||||
}
|
||||
|
||||
impl KclError {
|
||||
/// Get the error message, line and column from the error and input code.
|
||||
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) {
|
||||
// Calculate the line and column of the error from the source range.
|
||||
let (line, column) = if let Some(range) = self.source_ranges().first() {
|
||||
let line = input[..range.0[0]].lines().count();
|
||||
let column = input[..range.0[0]].lines().last().map(|l| l.len()).unwrap_or_default();
|
||||
|
||||
(Some(line), Some(column))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
(format!("{}: {}", self.error_type(), self.message()), line, column)
|
||||
/// Get the error message.
|
||||
pub fn get_message(&self) -> String {
|
||||
format!("{}: {}", self.error_type(), self.message())
|
||||
}
|
||||
|
||||
pub fn error_type(&self) -> &'static str {
|
||||
@ -106,24 +96,6 @@ impl KclError {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let (message, _, _) = self.get_message_line_column(code);
|
||||
let source_ranges = self.source_ranges();
|
||||
|
||||
Diagnostic {
|
||||
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("kcl".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
||||
let mut new = self.clone();
|
||||
match &mut new {
|
||||
@ -163,6 +135,26 @@ impl KclError {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for KclError {
|
||||
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let message = self.get_message();
|
||||
let source_ranges = self.source_ranges();
|
||||
|
||||
Diagnostic {
|
||||
range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("kcl".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is different than to_string() in that it will serialize the Error
|
||||
/// the struct as JSON so we can deserialize it on the js side.
|
||||
impl From<KclError> for String {
|
||||
|
@ -11,6 +11,7 @@ pub mod engine;
|
||||
pub mod errors;
|
||||
pub mod executor;
|
||||
pub mod fs;
|
||||
pub mod lint;
|
||||
pub mod lsp;
|
||||
pub mod parser;
|
||||
pub mod settings;
|
||||
|
64
src/wasm-lib/kcl/src/lint/ast_node.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use crate::ast::types;
|
||||
|
||||
/// The "Node" type wraps all the AST elements we're able to find in a KCL
|
||||
/// file. Tokens we walk through will be one of these.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Node<'a> {
|
||||
Program(&'a types::Program),
|
||||
|
||||
ExpressionStatement(&'a types::ExpressionStatement),
|
||||
VariableDeclaration(&'a types::VariableDeclaration),
|
||||
ReturnStatement(&'a types::ReturnStatement),
|
||||
|
||||
VariableDeclarator(&'a types::VariableDeclarator),
|
||||
|
||||
Literal(&'a types::Literal),
|
||||
Identifier(&'a types::Identifier),
|
||||
BinaryExpression(&'a types::BinaryExpression),
|
||||
FunctionExpression(&'a types::FunctionExpression),
|
||||
CallExpression(&'a types::CallExpression),
|
||||
PipeExpression(&'a types::PipeExpression),
|
||||
PipeSubstitution(&'a types::PipeSubstitution),
|
||||
ArrayExpression(&'a types::ArrayExpression),
|
||||
ObjectExpression(&'a types::ObjectExpression),
|
||||
MemberExpression(&'a types::MemberExpression),
|
||||
UnaryExpression(&'a types::UnaryExpression),
|
||||
|
||||
Parameter(&'a types::Parameter),
|
||||
|
||||
ObjectProperty(&'a types::ObjectProperty),
|
||||
|
||||
MemberObject(&'a types::MemberObject),
|
||||
LiteralIdentifier(&'a types::LiteralIdentifier),
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($node:ident, $t: ident) => {
|
||||
impl<'a> From<&'a types::$t> for Node<'a> {
|
||||
fn from(v: &'a types::$t) -> Self {
|
||||
Node::$t(v)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from!(Node, Program);
|
||||
impl_from!(Node, ExpressionStatement);
|
||||
impl_from!(Node, VariableDeclaration);
|
||||
impl_from!(Node, ReturnStatement);
|
||||
impl_from!(Node, VariableDeclarator);
|
||||
impl_from!(Node, Literal);
|
||||
impl_from!(Node, Identifier);
|
||||
impl_from!(Node, BinaryExpression);
|
||||
impl_from!(Node, FunctionExpression);
|
||||
impl_from!(Node, CallExpression);
|
||||
impl_from!(Node, PipeExpression);
|
||||
impl_from!(Node, PipeSubstitution);
|
||||
impl_from!(Node, ArrayExpression);
|
||||
impl_from!(Node, ObjectExpression);
|
||||
impl_from!(Node, MemberExpression);
|
||||
impl_from!(Node, UnaryExpression);
|
||||
impl_from!(Node, Parameter);
|
||||
impl_from!(Node, ObjectProperty);
|
||||
impl_from!(Node, MemberObject);
|
||||
impl_from!(Node, LiteralIdentifier);
|
233
src/wasm-lib/kcl/src/lint/ast_walk.rs
Normal file
@ -0,0 +1,233 @@
|
||||
use super::Node;
|
||||
use crate::ast::types::{
|
||||
BinaryPart, BodyItem, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, ObjectProperty,
|
||||
Parameter, Program, UnaryExpression, Value, VariableDeclarator,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
/// Walker is implemented by things that are able to walk an AST tree to
|
||||
/// produce lints. This trait is implemented automatically for a few of the
|
||||
/// common types, but can be manually implemented too.
|
||||
pub trait Walker<'a> {
|
||||
/// Walk will visit every element of the AST.
|
||||
fn walk(&self, n: Node<'a>) -> Result<bool>;
|
||||
}
|
||||
|
||||
impl<'a, FnT> Walker<'a> for FnT
|
||||
where
|
||||
FnT: Fn(Node<'a>) -> Result<bool>,
|
||||
{
|
||||
fn walk(&self, n: Node<'a>) -> Result<bool> {
|
||||
self(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the Walker against all [Node]s in a [Program].
|
||||
pub fn walk<'a, WalkT>(prog: &'a Program, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(prog.into())?;
|
||||
|
||||
for bi in &prog.body {
|
||||
walk_body_item(bi, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_variable_declarator<'a, WalkT>(node: &'a VariableDeclarator, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
f.walk((&node.id).into())?;
|
||||
walk_value(&node.init, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_parameter<'a, WalkT>(node: &'a Parameter, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
f.walk((&node.identifier).into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_member_object<'a, WalkT>(node: &'a MemberObject, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_literal_identifier<'a, WalkT>(node: &'a LiteralIdentifier, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_member_expression<'a, WalkT>(node: &'a MemberExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
|
||||
walk_member_object(&node.object, f)?;
|
||||
walk_literal_identifier(&node.property, f)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_binary_part<'a, WalkT>(node: &'a BinaryPart, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
match node {
|
||||
BinaryPart::Literal(lit) => f.walk(lit.as_ref().into())?,
|
||||
BinaryPart::Identifier(id) => f.walk(id.as_ref().into())?,
|
||||
BinaryPart::BinaryExpression(be) => f.walk(be.as_ref().into())?,
|
||||
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into())?,
|
||||
BinaryPart::UnaryExpression(ue) => {
|
||||
walk_unary_expression(ue, f)?;
|
||||
true
|
||||
}
|
||||
BinaryPart::MemberExpression(me) => {
|
||||
walk_member_expression(me, f)?;
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn walk_value<'a, WalkT>(node: &'a Value, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
match node {
|
||||
Value::Literal(lit) => {
|
||||
f.walk(lit.as_ref().into())?;
|
||||
}
|
||||
|
||||
Value::Identifier(id) => {
|
||||
// sometimes there's a bare Identifier without a Value::Identifier.
|
||||
f.walk(id.as_ref().into())?;
|
||||
}
|
||||
|
||||
Value::BinaryExpression(be) => {
|
||||
f.walk(be.as_ref().into())?;
|
||||
|
||||
walk_binary_part(&be.left, f)?;
|
||||
walk_binary_part(&be.right, f)?;
|
||||
}
|
||||
Value::FunctionExpression(fe) => {
|
||||
f.walk(fe.as_ref().into())?;
|
||||
|
||||
for arg in &fe.params {
|
||||
walk_parameter(arg, f)?;
|
||||
}
|
||||
walk(&fe.body, f)?;
|
||||
}
|
||||
Value::CallExpression(ce) => {
|
||||
f.walk(ce.as_ref().into())?;
|
||||
f.walk((&ce.callee).into())?;
|
||||
for e in &ce.arguments {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::PipeExpression(pe) => {
|
||||
f.walk(pe.as_ref().into())?;
|
||||
|
||||
for e in &pe.body {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::PipeSubstitution(ps) => {
|
||||
f.walk(ps.as_ref().into())?;
|
||||
}
|
||||
Value::ArrayExpression(ae) => {
|
||||
f.walk(ae.as_ref().into())?;
|
||||
for e in &ae.elements {
|
||||
walk_value::<WalkT>(e, f)?;
|
||||
}
|
||||
}
|
||||
Value::ObjectExpression(oe) => {
|
||||
walk_object_expression(oe, f)?;
|
||||
}
|
||||
Value::MemberExpression(me) => {
|
||||
walk_member_expression(me, f)?;
|
||||
}
|
||||
Value::UnaryExpression(ue) => {
|
||||
walk_unary_expression(ue, f)?;
|
||||
}
|
||||
_ => {
|
||||
println!("{:?}", node);
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk through an [ObjectProperty].
|
||||
fn walk_object_property<'a, WalkT>(node: &'a ObjectProperty, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
walk_value(&node.value, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk through an [ObjectExpression].
|
||||
fn walk_object_expression<'a, WalkT>(node: &'a ObjectExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
for prop in &node.properties {
|
||||
walk_object_property(prop, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// walk through an [UnaryExpression].
|
||||
fn walk_unary_expression<'a, WalkT>(node: &'a UnaryExpression, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
f.walk(node.into())?;
|
||||
walk_binary_part(&node.argument, f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// walk through a [BodyItem].
|
||||
fn walk_body_item<'a, WalkT>(node: &'a BodyItem, f: &WalkT) -> Result<()>
|
||||
where
|
||||
WalkT: Walker<'a>,
|
||||
{
|
||||
// We don't walk a BodyItem since it's an enum itself.
|
||||
|
||||
match node {
|
||||
BodyItem::ExpressionStatement(xs) => {
|
||||
f.walk(xs.into())?;
|
||||
walk_value(&xs.expression, f)?;
|
||||
}
|
||||
BodyItem::VariableDeclaration(vd) => {
|
||||
f.walk(vd.into())?;
|
||||
for dec in &vd.declarations {
|
||||
walk_variable_declarator(dec, f)?;
|
||||
}
|
||||
}
|
||||
BodyItem::ReturnStatement(rs) => {
|
||||
f.walk(rs.into())?;
|
||||
walk_value(&rs.argument, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
131
src/wasm-lib/kcl/src/lint/checks/camel_case.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use crate::{
|
||||
ast::types::VariableDeclarator,
|
||||
executor::SourceRange,
|
||||
lint::{
|
||||
rule::{def_finding, Discovered, Finding},
|
||||
Node,
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
def_finding!(
|
||||
Z0001,
|
||||
"Identifiers must be lowerCamelCase",
|
||||
"\
|
||||
By convention, variable names are lowerCamelCase, not snake_case, kebab-case,
|
||||
nor CammelCase. 🐪
|
||||
|
||||
For instance, a good identifier for the variable representing 'box height'
|
||||
would be 'boxHeight', not 'BOX_HEIGHT', 'box_height' nor 'BoxHeight'. For
|
||||
more information there's a pretty good Wikipedia page at
|
||||
|
||||
https://en.wikipedia.org/wiki/Camel_case
|
||||
"
|
||||
);
|
||||
|
||||
fn lint_lower_camel_case(decl: &VariableDeclarator) -> Result<Vec<Discovered>> {
|
||||
let mut findings = vec![];
|
||||
let ident = &decl.id;
|
||||
let name = &ident.name;
|
||||
|
||||
if !name.chars().next().unwrap().is_lowercase() {
|
||||
findings.push(Z0001.at(format!("found '{}'", name), SourceRange::new(ident.start, ident.end)));
|
||||
return Ok(findings);
|
||||
}
|
||||
|
||||
if name.contains('-') || name.contains('_') {
|
||||
findings.push(Z0001.at(format!("found '{}'", name), SourceRange::new(ident.start, ident.end)));
|
||||
return Ok(findings);
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
}
|
||||
|
||||
pub fn lint_variables(decl: Node) -> Result<Vec<Discovered>> {
|
||||
let Node::VariableDeclaration(decl) = decl else {
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
Ok(decl
|
||||
.declarations
|
||||
.iter()
|
||||
.flat_map(|v| lint_lower_camel_case(v).unwrap_or_default())
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{lint_variables, Z0001};
|
||||
use crate::lint::rule::{assert_finding, test_finding, test_no_finding};
|
||||
|
||||
#[test]
|
||||
fn z0001_const() {
|
||||
assert_finding!(lint_variables, Z0001, "const Thickness = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const THICKNESS = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const THICC_NES = 0.5");
|
||||
assert_finding!(lint_variables, Z0001, "const thicc_nes = 0.5");
|
||||
}
|
||||
|
||||
test_finding!(z0001_full_bad, lint_variables, Z0001, "\
|
||||
// Define constants
|
||||
const pipeLength = 40
|
||||
const pipeSmallDia = 10
|
||||
const pipeLargeDia = 20
|
||||
const thickness = 0.5
|
||||
|
||||
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
|
||||
const Part001 = startSketchOn('XY')
|
||||
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|
||||
|> line([thickness, 0], %)
|
||||
|> line([0, -1], %)
|
||||
|> angledLineToX({
|
||||
angle: 60,
|
||||
to: pipeSmallDia + thickness
|
||||
}, %)
|
||||
|> line([0, -pipeLength], %)
|
||||
|> angledLineToX({
|
||||
angle: -60,
|
||||
to: pipeLargeDia + thickness
|
||||
}, %)
|
||||
|> line([0, -1], %)
|
||||
|> line([-thickness, 0], %)
|
||||
|> line([0, 1], %)
|
||||
|> angledLineToX({ angle: 120, to: pipeSmallDia }, %)
|
||||
|> line([0, pipeLength], %)
|
||||
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: 'y' }, %)
|
||||
");
|
||||
|
||||
test_no_finding!(z0001_full_good, lint_variables, Z0001, "\
|
||||
// Define constants
|
||||
const pipeLength = 40
|
||||
const pipeSmallDia = 10
|
||||
const pipeLargeDia = 20
|
||||
const thickness = 0.5
|
||||
|
||||
// Create the sketch to be revolved around the y-axis. Use the small diameter, large diameter, length, and thickness to define the sketch.
|
||||
const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([pipeLargeDia - (thickness / 2), 38], %)
|
||||
|> line([thickness, 0], %)
|
||||
|> line([0, -1], %)
|
||||
|> angledLineToX({
|
||||
angle: 60,
|
||||
to: pipeSmallDia + thickness
|
||||
}, %)
|
||||
|> line([0, -pipeLength], %)
|
||||
|> angledLineToX({
|
||||
angle: -60,
|
||||
to: pipeLargeDia + thickness
|
||||
}, %)
|
||||
|> line([0, -1], %)
|
||||
|> line([-thickness, 0], %)
|
||||
|> line([0, 1], %)
|
||||
|> angledLineToX({ angle: 120, to: pipeSmallDia }, %)
|
||||
|> line([0, pipeLength], %)
|
||||
|> angledLineToX({ angle: 60, to: pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: 'y' }, %)
|
||||
");
|
||||
}
|
4
src/wasm-lib/kcl/src/lint/checks/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod camel_case;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use camel_case::{lint_variables, Z0001};
|
9
src/wasm-lib/kcl/src/lint/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
mod ast_node;
|
||||
mod ast_walk;
|
||||
pub mod checks;
|
||||
mod rule;
|
||||
|
||||
pub use ast_node::Node;
|
||||
pub use ast_walk::walk;
|
||||
// pub(crate) use rule::{def_finding, finding};
|
||||
pub use rule::{lint, Discovered, Finding};
|
180
src/wasm-lib/kcl/src/lint/rule.rs
Normal file
@ -0,0 +1,180 @@
|
||||
use super::{walk, Node};
|
||||
use crate::{ast::types::Program, executor::SourceRange, lsp::IntoDiagnostic};
|
||||
use anyhow::Result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
/// Check the provided AST for any found rule violations.
|
||||
///
|
||||
/// The Rule trait is automatically implemented for a few other types,
|
||||
/// but it can also be manually implemented as required.
|
||||
pub trait Rule<'a> {
|
||||
/// Check the AST at this specific node for any Finding(s).
|
||||
fn check(&self, node: Node<'a>) -> Result<Vec<Discovered>>;
|
||||
}
|
||||
|
||||
impl<'a, FnT> Rule<'a> for FnT
|
||||
where
|
||||
FnT: Fn(Node<'a>) -> Result<Vec<Discovered>>,
|
||||
{
|
||||
fn check(&self, n: Node<'a>) -> Result<Vec<Discovered>> {
|
||||
self(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Specific discovered lint rule Violation of a particular Finding.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Discovered {
|
||||
/// Zoo Lint Finding information.
|
||||
pub finding: Finding,
|
||||
|
||||
/// Further information about the specific finding.
|
||||
pub description: String,
|
||||
|
||||
/// Source code location.
|
||||
pub pos: SourceRange,
|
||||
|
||||
/// Is this discovered issue overridden by the programmer?
|
||||
pub overridden: bool,
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for Discovered {
|
||||
fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let message = self.finding.title.to_owned();
|
||||
let source_range = self.pos;
|
||||
|
||||
Diagnostic {
|
||||
range: source_range.to_lsp_range(code),
|
||||
severity: Some(DiagnosticSeverity::INFORMATION),
|
||||
code: None,
|
||||
// TODO: this is neat we can pass a URL to a help page here for this specific error.
|
||||
code_description: None,
|
||||
source: Some("lint".to_string()),
|
||||
message,
|
||||
related_information: None,
|
||||
tags: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract lint problem type.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Finding {
|
||||
/// Unique identifier for this particular issue.
|
||||
pub code: &'static str,
|
||||
|
||||
/// Short one-line description of this issue.
|
||||
pub title: &'static str,
|
||||
|
||||
/// Long human-readable description of this issue.
|
||||
pub description: &'static str,
|
||||
|
||||
/// Is this discovered issue experimental?
|
||||
pub experimental: bool,
|
||||
}
|
||||
|
||||
impl Finding {
|
||||
/// Create a new Discovered finding at the specific Position.
|
||||
pub fn at(&self, description: String, pos: SourceRange) -> Discovered {
|
||||
Discovered {
|
||||
description,
|
||||
finding: self.clone(),
|
||||
pos,
|
||||
overridden: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! def_finding {
|
||||
( $code:ident, $title:expr, $description:expr ) => {
|
||||
/// Generated Finding
|
||||
pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
|
||||
};
|
||||
}
|
||||
pub(crate) use def_finding;
|
||||
|
||||
macro_rules! finding {
|
||||
( $code:ident, $title:expr, $description:expr ) => {
|
||||
$crate::lint::rule::Finding {
|
||||
code: stringify!($code),
|
||||
title: $title,
|
||||
description: $description,
|
||||
experimental: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use finding;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
|
||||
|
||||
/// Check the provided Program for any Findings.
|
||||
pub fn lint<'a, RuleT>(prog: &'a Program, rule: RuleT) -> Result<Vec<Discovered>>
|
||||
where
|
||||
RuleT: Rule<'a>,
|
||||
{
|
||||
let v = Arc::new(Mutex::new(vec![]));
|
||||
walk(prog, &|node: Node<'a>| {
|
||||
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
|
||||
findings.append(&mut rule.check(node)?);
|
||||
Ok(true)
|
||||
})?;
|
||||
let x = v.lock().unwrap();
|
||||
Ok(x.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
macro_rules! assert_no_finding {
|
||||
( $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
let tokens = $crate::token::lexer($kcl).unwrap();
|
||||
let parser = $crate::parser::Parser::new(tokens);
|
||||
let prog = parser.ast().unwrap();
|
||||
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
|
||||
if discovered_finding.finding == $finding {
|
||||
assert!(false, "Finding {:?} was emitted", $finding.code);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_finding {
|
||||
( $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
let tokens = $crate::token::lexer($kcl).unwrap();
|
||||
let parser = $crate::parser::Parser::new(tokens);
|
||||
let prog = parser.ast().unwrap();
|
||||
|
||||
for discovered_finding in $crate::lint::lint(&prog, $check).unwrap() {
|
||||
if discovered_finding.finding == $finding {
|
||||
return;
|
||||
}
|
||||
}
|
||||
assert!(false, "Finding {:?} was not emitted", $finding.code);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_finding {
|
||||
( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
$crate::lint::rule::assert_finding!($check, $finding, $kcl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_no_finding {
|
||||
( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
$crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use assert_finding;
|
||||
pub(crate) use assert_no_finding;
|
||||
pub(crate) use test_finding;
|
||||
pub(crate) use test_no_finding;
|
||||
}
|
@ -36,9 +36,9 @@ use tower_lsp::{
|
||||
use super::backend::{InnerHandle, UpdateHandle};
|
||||
use crate::{
|
||||
ast::types::VariableKind,
|
||||
errors::KclError,
|
||||
executor::SourceRange,
|
||||
lsp::{backend::Backend as _, safemap::SafeMap},
|
||||
lint::{checks, lint},
|
||||
lsp::{backend::Backend as _, safemap::SafeMap, util::IntoDiagnostic},
|
||||
parser::PIPE_OPERATOR,
|
||||
};
|
||||
|
||||
@ -166,6 +166,7 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
}
|
||||
|
||||
async fn inner_on_change(&self, params: TextDocumentItem, force: bool) {
|
||||
self.clear_diagnostics_map(¶ms.uri).await;
|
||||
// We already updated the code map in the shared backend.
|
||||
|
||||
// Lets update the tokens.
|
||||
@ -251,14 +252,14 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
// Execute the code if we have an executor context.
|
||||
// This function automatically executes if we should & updates the diagnostics if we got
|
||||
// errors.
|
||||
let result = self.execute(¶ms, ast).await;
|
||||
if result.is_err() {
|
||||
// We return early because we got errors, and we don't want to clear the diagnostics.
|
||||
if self.execute(¶ms, ast.clone()).await.is_err() {
|
||||
// if there was an issue, let's bail and avoid trying to lint.
|
||||
return;
|
||||
}
|
||||
|
||||
// Lets update the diagnostics, since we got no errors.
|
||||
self.clear_diagnostics(¶ms.uri).await;
|
||||
for discovered_finding in lint(&ast, checks::lint_variables).into_iter().flatten() {
|
||||
self.add_to_diagnostics(¶ms, discovered_finding).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,30 +357,7 @@ impl Backend {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn add_to_diagnostics(&self, params: &TextDocumentItem, err: KclError) {
|
||||
let diagnostic = err.to_lsp_diagnostic(¶ms.text);
|
||||
// We got errors, update the diagnostics.
|
||||
self.diagnostics_map
|
||||
.insert(
|
||||
params.uri.to_string(),
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: vec![diagnostic.clone()],
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Publish the diagnostic.
|
||||
// If the client supports it.
|
||||
self.client
|
||||
.publish_diagnostics(params.uri.clone(), vec![diagnostic], None)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn clear_diagnostics(&self, uri: &url::Url) {
|
||||
async fn clear_diagnostics_map(&self, uri: &url::Url) {
|
||||
self.diagnostics_map
|
||||
.insert(
|
||||
uri.to_string(),
|
||||
@ -392,10 +370,43 @@ impl Backend {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Publish the diagnostic, we reset it here so the client knows the code compiles now.
|
||||
// If the client supports it.
|
||||
self.client.publish_diagnostics(uri.clone(), vec![], None).await;
|
||||
async fn add_to_diagnostics<DiagT: IntoDiagnostic + std::fmt::Debug>(
|
||||
&self,
|
||||
params: &TextDocumentItem,
|
||||
diagnostic: DiagT,
|
||||
) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostic))
|
||||
.await;
|
||||
|
||||
let diagnostic = diagnostic.to_lsp_diagnostic(¶ms.text);
|
||||
|
||||
let DocumentDiagnosticReport::Full(mut report) = self
|
||||
.diagnostics_map
|
||||
.get(params.uri.clone().as_str())
|
||||
.await
|
||||
.unwrap_or(DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: vec![],
|
||||
},
|
||||
}))
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
report.full_document_diagnostic_report.items.push(diagnostic);
|
||||
|
||||
self.diagnostics_map
|
||||
.insert(params.uri.to_string(), DocumentDiagnosticReport::Full(report.clone()))
|
||||
.await;
|
||||
|
||||
self.client
|
||||
.publish_diagnostics(params.uri.clone(), report.full_document_diagnostic_report.items, None)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn execute(&self, params: &TextDocumentItem, ast: crate::ast::types::Program) -> Result<()> {
|
||||
|
@ -7,3 +7,5 @@ mod safemap;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod util;
|
||||
|
||||
pub use util::IntoDiagnostic;
|
||||
|
@ -1498,6 +1498,53 @@ async fn test_kcl_lsp_diagnostic_has_errors() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_kcl_lsp_diagnostic_has_lints() {
|
||||
let server = kcl_lsp_server(false).await.unwrap();
|
||||
|
||||
// Send open file.
|
||||
server
|
||||
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentItem {
|
||||
uri: "file:///testlint.kcl".try_into().unwrap(),
|
||||
language_id: "kcl".to_string(),
|
||||
version: 1,
|
||||
text: r#"let THING = 10"#.to_string(),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
server.wait_on_handle().await;
|
||||
|
||||
// Send diagnostics request.
|
||||
let diagnostics = server
|
||||
.diagnostic(tower_lsp::lsp_types::DocumentDiagnosticParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
||||
uri: "file:///testlint.kcl".try_into().unwrap(),
|
||||
},
|
||||
partial_result_params: Default::default(),
|
||||
work_done_progress_params: Default::default(),
|
||||
identifier: None,
|
||||
previous_result_id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check the diagnostics.
|
||||
if let tower_lsp::lsp_types::DocumentDiagnosticReportResult::Report(diagnostics) = diagnostics {
|
||||
if let tower_lsp::lsp_types::DocumentDiagnosticReport::Full(diagnostics) = diagnostics {
|
||||
assert_eq!(diagnostics.full_document_diagnostic_report.items.len(), 1);
|
||||
assert_eq!(
|
||||
diagnostics.full_document_diagnostic_report.items[0].message,
|
||||
"Identifiers must be lowerCamelCase"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected full diagnostics");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected diagnostics");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_copilot_lsp_set_editor_info() {
|
||||
let server = copilot_lsp_server().await.unwrap();
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Utility functions for working with ropes and positions.
|
||||
|
||||
use ropey::Rope;
|
||||
use tower_lsp::lsp_types::Position;
|
||||
use tower_lsp::lsp_types::{Diagnostic, Position};
|
||||
|
||||
pub fn position_to_offset(position: Position, rope: &Rope) -> Option<usize> {
|
||||
Some(rope.try_line_to_char(position.line as usize).ok()? + position.character as usize)
|
||||
@ -31,3 +31,10 @@ pub fn get_line_before(pos: Position, rope: &Rope) -> Option<String> {
|
||||
let line_start = offset - char_offset;
|
||||
Some(rope.slice(line_start..offset).to_string())
|
||||
}
|
||||
|
||||
/// Convert an object into a [lsp_types::Diagnostic] given the
|
||||
/// [TextDocumentItem]'s `.text` field.
|
||||
pub trait IntoDiagnostic {
|
||||
/// Convert the traited object to a [lsp_types::Diagnostic].
|
||||
fn to_lsp_diagnostic(&self, text: &str) -> Diagnostic;
|
||||
}
|
||||
|
126
src/wasm-lib/kcl/src/std/chamfer.rs
Normal file
@ -0,0 +1,126 @@
|
||||
//! Standard library chamfers.
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::ModelingCmd;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExtrudeGroup, MemoryItem},
|
||||
std::Args,
|
||||
};
|
||||
|
||||
pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
|
||||
|
||||
/// Data for chamfers.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChamferData {
|
||||
/// The radius of the chamfer.
|
||||
pub radius: f64,
|
||||
/// The tags of the paths you want to chamfer.
|
||||
pub tags: Vec<EdgeReference>,
|
||||
}
|
||||
|
||||
/// A string or a uuid.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Ord, PartialOrd, Eq, Hash)]
|
||||
#[ts(export)]
|
||||
#[serde(untagged)]
|
||||
pub enum EdgeReference {
|
||||
/// A uuid of an edge.
|
||||
Uuid(uuid::Uuid),
|
||||
/// A tag name of an edge.
|
||||
Tag(String),
|
||||
}
|
||||
|
||||
/// Create chamfers on tagged paths.
|
||||
pub async fn chamfer(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, extrude_group): (ChamferData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
|
||||
|
||||
let extrude_group = inner_chamfer(data, extrude_group, args).await?;
|
||||
Ok(MemoryItem::ExtrudeGroup(extrude_group))
|
||||
}
|
||||
|
||||
/// Create chamfers on tagged paths.
|
||||
///
|
||||
/// ```no_run
|
||||
/// const width = 20
|
||||
/// const length = 10
|
||||
/// const thickness = 1
|
||||
/// const chamferRadius = 2
|
||||
///
|
||||
/// const mountingPlateSketch = startSketchOn("XY")
|
||||
/// |> startProfileAt([-width/2, -length/2], %)
|
||||
/// |> lineTo([width/2, -length/2], %, 'edge1')
|
||||
/// |> lineTo([width/2, length/2], %, 'edge2')
|
||||
/// |> lineTo([-width/2, length/2], %, 'edge3')
|
||||
/// |> close(%, 'edge4')
|
||||
///
|
||||
/// const mountingPlate = extrude(thickness, mountingPlateSketch)
|
||||
/// |> chamfer({
|
||||
/// radius: chamferRadius,
|
||||
/// tags: [
|
||||
/// getNextAdjacentEdge('edge1', %),
|
||||
/// getNextAdjacentEdge('edge2', %),
|
||||
/// getNextAdjacentEdge('edge3', %),
|
||||
/// getNextAdjacentEdge('edge4', %)
|
||||
/// ],
|
||||
/// }, %)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "chamfer",
|
||||
}]
|
||||
async fn inner_chamfer(
|
||||
data: ChamferData,
|
||||
extrude_group: Box<ExtrudeGroup>,
|
||||
args: Args,
|
||||
) -> Result<Box<ExtrudeGroup>, KclError> {
|
||||
// Check if tags contains any duplicate values.
|
||||
let mut tags = data.tags.clone();
|
||||
tags.sort();
|
||||
tags.dedup();
|
||||
if tags.len() != data.tags.len() {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Duplicate tags are not allowed.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
for tag in data.tags {
|
||||
let edge_id = match tag {
|
||||
EdgeReference::Uuid(uuid) => uuid,
|
||||
EdgeReference::Tag(tag) => {
|
||||
extrude_group
|
||||
.sketch_group_values
|
||||
.iter()
|
||||
.find(|p| p.get_name() == tag)
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("No edge found with tag: `{}`", tag),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
.get_base()
|
||||
.geo_meta
|
||||
.id
|
||||
}
|
||||
};
|
||||
|
||||
args.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid3DFilletEdge {
|
||||
edge_id,
|
||||
object_id: extrude_group.id,
|
||||
radius: data.radius,
|
||||
tolerance: DEFAULT_TOLERANCE, // We can let the user set this in the future.
|
||||
cut_type: Some(kittycad::types::CutType::Chamfer),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(extrude_group)
|
||||
}
|
@ -117,6 +117,7 @@ async fn inner_fillet(
|
||||
object_id: extrude_group.id,
|
||||
radius: data.radius,
|
||||
tolerance: DEFAULT_TOLERANCE, // We can let the user set this in the future.
|
||||
cut_type: Some(kittycad::types::CutType::Fillet),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
@ -275,7 +275,7 @@ pub async fn min(args: Args) -> Result<MemoryItem, KclError> {
|
||||
tags = ["math"],
|
||||
}]
|
||||
fn inner_min(args: Vec<f64>) -> f64 {
|
||||
let mut min = std::f64::MAX;
|
||||
let mut min = f64::MAX;
|
||||
for arg in args.iter() {
|
||||
if *arg < min {
|
||||
min = *arg;
|
||||
@ -312,7 +312,7 @@ pub async fn max(args: Args) -> Result<MemoryItem, KclError> {
|
||||
tags = ["math"],
|
||||
}]
|
||||
fn inner_max(args: Vec<f64>) -> f64 {
|
||||
let mut max = std::f64::MIN;
|
||||
let mut max = f64::MIN;
|
||||
for arg in args.iter() {
|
||||
if *arg > max {
|
||||
max = *arg;
|
||||
|
@ -1,5 +1,6 @@
|
||||
//! Functions implemented for language execution.
|
||||
|
||||
pub mod chamfer;
|
||||
pub mod extrude;
|
||||
pub mod fillet;
|
||||
pub mod helix;
|
||||
@ -10,6 +11,7 @@ pub mod patterns;
|
||||
pub mod revolve;
|
||||
pub mod segment;
|
||||
pub mod shapes;
|
||||
pub mod shell;
|
||||
pub mod sketch;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
@ -29,7 +31,8 @@ use crate::{
|
||||
docs::StdLibFn,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{
|
||||
ExecutorContext, ExtrudeGroup, MemoryItem, Metadata, SketchGroup, SketchGroupSet, SketchSurface, SourceRange,
|
||||
ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryItem, Metadata, SketchGroup, SketchGroupSet,
|
||||
SketchSurface, SourceRange,
|
||||
},
|
||||
std::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag},
|
||||
};
|
||||
@ -81,11 +84,13 @@ lazy_static! {
|
||||
Box::new(crate::std::patterns::PatternLinear3D),
|
||||
Box::new(crate::std::patterns::PatternCircular2D),
|
||||
Box::new(crate::std::patterns::PatternCircular3D),
|
||||
Box::new(crate::std::chamfer::Chamfer),
|
||||
Box::new(crate::std::fillet::Fillet),
|
||||
Box::new(crate::std::fillet::GetOppositeEdge),
|
||||
Box::new(crate::std::fillet::GetNextAdjacentEdge),
|
||||
Box::new(crate::std::fillet::GetPreviousAdjacentEdge),
|
||||
Box::new(crate::std::helix::Helix),
|
||||
Box::new(crate::std::shell::Shell),
|
||||
Box::new(crate::std::revolve::Revolve),
|
||||
Box::new(crate::std::revolve::GetEdge),
|
||||
Box::new(crate::std::import::Import),
|
||||
@ -769,6 +774,52 @@ impl Args {
|
||||
Ok((data, sketch_surface, tag))
|
||||
}
|
||||
|
||||
fn get_data_and_extrude_group_set<T: serde::de::DeserializeOwned>(&self) -> Result<(T, ExtrudeGroupSet), KclError> {
|
||||
let first_value = self
|
||||
.args
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?
|
||||
.get_json_value()?;
|
||||
|
||||
let data: T = serde_json::from_value(first_value).map_err(|e| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Failed to deserialize struct from JSON: {}", e),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let second_value = self.args.get(1).ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected an ExtrudeGroup as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
})
|
||||
})?;
|
||||
|
||||
let extrude_set = if let MemoryItem::ExtrudeGroup(eg) = second_value {
|
||||
ExtrudeGroupSet::ExtrudeGroup(eg.clone())
|
||||
} else if let MemoryItem::ExtrudeGroups { value } = second_value {
|
||||
ExtrudeGroupSet::ExtrudeGroups(value.clone())
|
||||
} else {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: format!(
|
||||
"Expected a ExtrudeGroup or Vector of ExtrudeGroups as the second argument, found `{:?}`",
|
||||
self.args
|
||||
),
|
||||
source_ranges: vec![self.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok((data, extrude_set))
|
||||
}
|
||||
|
||||
fn get_data_and_extrude_group<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Box<ExtrudeGroup>), KclError> {
|
||||
let first_value = self
|
||||
.args
|
||||
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExtrudeGroup, Geometries, Geometry, MemoryItem, SketchGroup, SketchGroupSet},
|
||||
executor::{ExtrudeGroup, ExtrudeGroupSet, Geometries, Geometry, MemoryItem, SketchGroup, SketchGroupSet},
|
||||
std::{types::Uint, Args},
|
||||
};
|
||||
|
||||
@ -141,7 +141,7 @@ async fn inner_pattern_linear_2d(
|
||||
|
||||
/// A linear pattern on a 3D model.
|
||||
pub async fn pattern_linear_3d(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, extrude_group): (LinearPattern3dData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
|
||||
let (data, extrude_group_set): (LinearPattern3dData, ExtrudeGroupSet) = args.get_data_and_extrude_group_set()?;
|
||||
|
||||
if data.axis == [0.0, 0.0, 0.0] {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
@ -152,7 +152,7 @@ pub async fn pattern_linear_3d(args: Args) -> Result<MemoryItem, KclError> {
|
||||
}));
|
||||
}
|
||||
|
||||
let extrude_groups = inner_pattern_linear_3d(data, extrude_group, args).await?;
|
||||
let extrude_groups = inner_pattern_linear_3d(data, extrude_group_set, args).await?;
|
||||
Ok(MemoryItem::ExtrudeGroups { value: extrude_groups })
|
||||
}
|
||||
|
||||
@ -178,26 +178,36 @@ pub async fn pattern_linear_3d(args: Args) -> Result<MemoryItem, KclError> {
|
||||
}]
|
||||
async fn inner_pattern_linear_3d(
|
||||
data: LinearPattern3dData,
|
||||
extrude_group: Box<ExtrudeGroup>,
|
||||
extrude_group_set: ExtrudeGroupSet,
|
||||
args: Args,
|
||||
) -> Result<Vec<Box<ExtrudeGroup>>, KclError> {
|
||||
let starting_extrude_groups = match extrude_group_set {
|
||||
ExtrudeGroupSet::ExtrudeGroup(extrude_group) => vec![extrude_group],
|
||||
ExtrudeGroupSet::ExtrudeGroups(extrude_groups) => extrude_groups,
|
||||
};
|
||||
|
||||
if args.ctx.is_mock {
|
||||
return Ok(vec![extrude_group.clone()]);
|
||||
return Ok(starting_extrude_groups);
|
||||
}
|
||||
|
||||
let geometries = pattern_linear(
|
||||
LinearPattern::ThreeD(data),
|
||||
Geometry::ExtrudeGroup(extrude_group),
|
||||
args.clone(),
|
||||
)
|
||||
.await?;
|
||||
let mut extrude_groups = Vec::new();
|
||||
for extrude_group in starting_extrude_groups.iter() {
|
||||
let geometries = pattern_linear(
|
||||
LinearPattern::ThreeD(data.clone()),
|
||||
Geometry::ExtrudeGroup(extrude_group.clone()),
|
||||
args.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Geometries::ExtrudeGroups(extrude_groups) = geometries else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected a vec of extrude groups".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
};
|
||||
let Geometries::ExtrudeGroups(new_extrude_groups) = geometries else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected a vec of extrude groups".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
extrude_groups.extend(new_extrude_groups);
|
||||
}
|
||||
|
||||
Ok(extrude_groups)
|
||||
}
|
||||
@ -335,9 +345,9 @@ impl CircularPattern {
|
||||
|
||||
/// A circular pattern on a 2D sketch.
|
||||
pub async fn pattern_circular_2d(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, sketch_group): (CircularPattern2dData, Box<SketchGroup>) = args.get_data_and_sketch_group()?;
|
||||
let (data, sketch_group_set): (CircularPattern2dData, SketchGroupSet) = args.get_data_and_sketch_group_set()?;
|
||||
|
||||
let sketch_groups = inner_pattern_circular_2d(data, sketch_group, args).await?;
|
||||
let sketch_groups = inner_pattern_circular_2d(data, sketch_group_set, args).await?;
|
||||
Ok(MemoryItem::SketchGroups { value: sketch_groups })
|
||||
}
|
||||
|
||||
@ -364,35 +374,45 @@ pub async fn pattern_circular_2d(args: Args) -> Result<MemoryItem, KclError> {
|
||||
}]
|
||||
async fn inner_pattern_circular_2d(
|
||||
data: CircularPattern2dData,
|
||||
sketch_group: Box<SketchGroup>,
|
||||
sketch_group_set: SketchGroupSet,
|
||||
args: Args,
|
||||
) -> Result<Vec<Box<SketchGroup>>, KclError> {
|
||||
let starting_sketch_groups = match sketch_group_set {
|
||||
SketchGroupSet::SketchGroup(sketch_group) => vec![sketch_group],
|
||||
SketchGroupSet::SketchGroups(sketch_groups) => sketch_groups,
|
||||
};
|
||||
|
||||
if args.ctx.is_mock {
|
||||
return Ok(vec![sketch_group]);
|
||||
return Ok(starting_sketch_groups);
|
||||
}
|
||||
|
||||
let geometries = pattern_circular(
|
||||
CircularPattern::TwoD(data),
|
||||
Geometry::SketchGroup(sketch_group),
|
||||
args.clone(),
|
||||
)
|
||||
.await?;
|
||||
let mut sketch_groups = Vec::new();
|
||||
for sketch_group in starting_sketch_groups.iter() {
|
||||
let geometries = pattern_circular(
|
||||
CircularPattern::TwoD(data.clone()),
|
||||
Geometry::SketchGroup(sketch_group.clone()),
|
||||
args.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Geometries::SketchGroups(sketch_groups) = geometries else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected a vec of sketch groups".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
};
|
||||
let Geometries::SketchGroups(new_sketch_groups) = geometries else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected a vec of sketch groups".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
sketch_groups.extend(new_sketch_groups);
|
||||
}
|
||||
|
||||
Ok(sketch_groups)
|
||||
}
|
||||
|
||||
/// A circular pattern on a 3D model.
|
||||
pub async fn pattern_circular_3d(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, extrude_group): (CircularPattern3dData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
|
||||
let (data, extrude_group_set): (CircularPattern3dData, ExtrudeGroupSet) = args.get_data_and_extrude_group_set()?;
|
||||
|
||||
let extrude_groups = inner_pattern_circular_3d(data, extrude_group, args).await?;
|
||||
let extrude_groups = inner_pattern_circular_3d(data, extrude_group_set, args).await?;
|
||||
Ok(MemoryItem::ExtrudeGroups { value: extrude_groups })
|
||||
}
|
||||
|
||||
@ -416,26 +436,36 @@ pub async fn pattern_circular_3d(args: Args) -> Result<MemoryItem, KclError> {
|
||||
}]
|
||||
async fn inner_pattern_circular_3d(
|
||||
data: CircularPattern3dData,
|
||||
extrude_group: Box<ExtrudeGroup>,
|
||||
extrude_group_set: ExtrudeGroupSet,
|
||||
args: Args,
|
||||
) -> Result<Vec<Box<ExtrudeGroup>>, KclError> {
|
||||
let starting_extrude_groups = match extrude_group_set {
|
||||
ExtrudeGroupSet::ExtrudeGroup(extrude_group) => vec![extrude_group],
|
||||
ExtrudeGroupSet::ExtrudeGroups(extrude_groups) => extrude_groups,
|
||||
};
|
||||
|
||||
if args.ctx.is_mock {
|
||||
return Ok(vec![extrude_group]);
|
||||
return Ok(starting_extrude_groups);
|
||||
}
|
||||
|
||||
let geometries = pattern_circular(
|
||||
CircularPattern::ThreeD(data),
|
||||
Geometry::ExtrudeGroup(extrude_group),
|
||||
args.clone(),
|
||||
)
|
||||
.await?;
|
||||
let mut extrude_groups = Vec::new();
|
||||
for extrude_group in starting_extrude_groups.iter() {
|
||||
let geometries = pattern_circular(
|
||||
CircularPattern::ThreeD(data.clone()),
|
||||
Geometry::ExtrudeGroup(extrude_group.clone()),
|
||||
args.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Geometries::ExtrudeGroups(extrude_groups) = geometries else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected a vec of extrude groups".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
};
|
||||
let Geometries::ExtrudeGroups(new_extrude_groups) = geometries else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected a vec of extrude groups".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
};
|
||||
|
||||
extrude_groups.extend(new_extrude_groups);
|
||||
}
|
||||
|
||||
Ok(extrude_groups)
|
||||
}
|
||||
|
140
src/wasm-lib/kcl/src/std/shell.rs
Normal file
@ -0,0 +1,140 @@
|
||||
//! Standard library shells.
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::ModelingCmd;
|
||||
use parse_display::{Display, FromStr};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExtrudeGroup, ExtrudeSurface, MemoryItem},
|
||||
std::{sketch::StartOrEnd, Args},
|
||||
};
|
||||
|
||||
/// A tag for a face.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case", untagged)]
|
||||
#[display("{0}")]
|
||||
pub enum FaceTag {
|
||||
StartOrEnd(StartOrEnd),
|
||||
/// A string tag for the face you want to sketch on.
|
||||
String(String),
|
||||
}
|
||||
|
||||
/// Data for shells.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ShellData {
|
||||
/// The thickness of the shell.
|
||||
pub thickness: f64,
|
||||
/// The faces you want removed.
|
||||
pub faces: Vec<FaceTag>,
|
||||
}
|
||||
|
||||
/// Create a shell.
|
||||
pub async fn shell(args: Args) -> Result<MemoryItem, KclError> {
|
||||
let (data, extrude_group): (ShellData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;
|
||||
|
||||
let extrude_group = inner_shell(data, extrude_group, args).await?;
|
||||
Ok(MemoryItem::ExtrudeGroup(extrude_group))
|
||||
}
|
||||
|
||||
/// Shell a solid.
|
||||
///
|
||||
/// ```no_run
|
||||
/// const firstSketch = startSketchOn('XY')
|
||||
/// |> startProfileAt([-12, 12], %)
|
||||
/// |> line([24, 0], %)
|
||||
/// |> line([0, -24], %)
|
||||
/// |> line([-24, 0], %)
|
||||
/// |> close(%)
|
||||
/// |> extrude(6, %)
|
||||
///
|
||||
/// // Remove the end face for the extrusion.
|
||||
/// shell({
|
||||
/// faces: ['end'],
|
||||
/// thickness: 0.25,
|
||||
/// }, firstSketch)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "shell",
|
||||
}]
|
||||
async fn inner_shell(
|
||||
data: ShellData,
|
||||
extrude_group: Box<ExtrudeGroup>,
|
||||
args: Args,
|
||||
) -> Result<Box<ExtrudeGroup>, KclError> {
|
||||
if data.faces.is_empty() {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Expected at least one face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
let mut face_ids = Vec::new();
|
||||
for tag in data.faces {
|
||||
let extrude_plane_id = match tag {
|
||||
FaceTag::String(ref s) => {
|
||||
if s.is_empty() {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Expected a non-empty tag for the face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
extrude_group
|
||||
.value
|
||||
.iter()
|
||||
.find_map(|extrude_surface| match extrude_surface {
|
||||
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => {
|
||||
Some(extrude_plane.face_id)
|
||||
}
|
||||
ExtrudeSurface::ExtrudeArc(extrude_arc) if extrude_arc.name == *s => Some(extrude_arc.face_id),
|
||||
ExtrudeSurface::ExtrudePlane(_) | ExtrudeSurface::ExtrudeArc(_) => None,
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: format!("Expected a face with the tag `{}`", tag),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?
|
||||
}
|
||||
FaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: "Expected a start face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?,
|
||||
FaceTag::StartOrEnd(StartOrEnd::End) => extrude_group.end_cap_id.ok_or_else(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
message: "Expected an end face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
})
|
||||
})?,
|
||||
};
|
||||
|
||||
face_ids.push(extrude_plane_id);
|
||||
}
|
||||
|
||||
if face_ids.is_empty() {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Expected at least one valid face".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
args.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
ModelingCmd::Solid3DShellFace {
|
||||
face_ids,
|
||||
object_id: extrude_group.id,
|
||||
shell_thickness: data.thickness,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(extrude_group)
|
||||
}
|
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_chamfer0.png
Normal file
After Width: | Height: | Size: 98 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_shell0.png
Normal file
After Width: | Height: | Size: 157 KiB |
@ -1959,3 +1959,53 @@ async fn serial_test_neg_xz_plane() {
|
||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/neg_xz_plane.png", &result, 1.0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn serial_test_linear_pattern3d_a_pattern() {
|
||||
let code = r#"const exampleSketch = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, 2], %)
|
||||
|> line([3, 1], %)
|
||||
|> line([0, -4], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
|
||||
const pattn1 = patternLinear3d({
|
||||
axis: [1, 0, 0],
|
||||
repetitions: 6,
|
||||
distance: 6
|
||||
}, exampleSketch)
|
||||
|
||||
const pattn2 = patternLinear3d({
|
||||
axis: [0, 0, 1],
|
||||
distance: 1,
|
||||
repetitions: 6
|
||||
}, pattn1)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/linear_pattern3d_a_pattern.png", &result, 1.0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn serial_test_circular_pattern3d_a_pattern() {
|
||||
let code = r#"const exampleSketch = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, 2], %)
|
||||
|> line([3, 1], %)
|
||||
|> line([0, -4], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
|
||||
const pattn1 = patternLinear3d({
|
||||
axis: [1, 0, 0],
|
||||
repetitions: 6,
|
||||
distance: 6
|
||||
}, exampleSketch)
|
||||
|
||||
const pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], repetitions: 40, arcDegrees: 360, rotateDuplicates: false}, pattn1)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/circular_pattern3d_a_pattern.png", &result, 1.0);
|
||||
}
|
||||
|
After Width: | Height: | Size: 209 KiB |
After Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 102 KiB |
@ -1880,10 +1880,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||
|
||||
"@kittycad/lib@^0.0.64":
|
||||
version "0.0.64"
|
||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.64.tgz#0cea0788cd8af4a8964ddbf7152028affadcb17f"
|
||||
integrity sha512-qHyvNYKbhsfR5aXLFrdKrBQ4JI+0G0v096oROD3HatJ+AIzg5H0THmI+rMnQ9L4zx4U6n1A9gLi7ZQjSsZsleg==
|
||||
"@kittycad/lib@^0.0.67":
|
||||
version "0.0.67"
|
||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.67.tgz#b8edc66d83e41a79a7f238ba41bc27f0101935fb"
|
||||
integrity sha512-Uy2fve75bgpnlPiIgKrnKAqiko+1hlTCPSIPky6mv7Hrnwn7FhWAeeesdyc1Xws9Ae18kNyA2po8udK6PjZPkA==
|
||||
dependencies:
|
||||
node-fetch "3.3.2"
|
||||
openapi-types "^12.0.0"
|
||||
|