Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
30afa65ccf | |||
a2f9e70d18 | |||
986675fe89 | |||
d8ce5ad8bd | |||
1a9926be8a | |||
54b5774f9e | |||
66bbbf81e2 | |||
652519aeae | |||
f826afb32d | |||
f71fafdece | |||
16b7544d69 | |||
34f019305b | |||
79e06b3a00 | |||
24bc4fcd8c | |||
97b9529c81 | |||
8e5fc02941 | |||
909690f3c7 | |||
14ba66378d | |||
b2e895e508 | |||
05f4f34269 | |||
fbb7b08b62 | |||
16c7a2457a | |||
c429bc6ed7 | |||
a0493cb332 | |||
b798f7da03 | |||
c5d051855f | |||
0b24216dc5 | |||
2d3841bf61 | |||
51a71a180e | |||
1ec25dfe96 | |||
8de29dd461 | |||
b11040c23c | |||
2bc4f076cb | |||
9e1cf90c81 | |||
062fae1e54 | |||
d7660e221c | |||
938e27adac | |||
17b9af2416 |
4
.github/workflows/cargo-test.yml
vendored
@ -5,6 +5,8 @@ on:
|
||||
paths:
|
||||
- 'src/wasm-lib/**.rs'
|
||||
- 'src/wasm-lib/**.hbs'
|
||||
- 'src/wasm-lib/**.gen'
|
||||
- 'src/wasm-lib/**.snap'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
@ -15,6 +17,8 @@ on:
|
||||
paths:
|
||||
- 'src/wasm-lib/**.rs'
|
||||
- 'src/wasm-lib/**.hbs'
|
||||
- 'src/wasm-lib/**.gen'
|
||||
- 'src/wasm-lib/**.snap'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
|
2
Makefile
@ -19,7 +19,7 @@ $(XSTATE_TYPEGENS): $(TS_SRC)
|
||||
yarn xstate typegen 'src/**/*.ts?(x)'
|
||||
|
||||
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
|
||||
yarn build:wasm-dev
|
||||
yarn build:wasm
|
||||
|
||||
node_modules: package.json yarn.lock
|
||||
yarn install
|
||||
|
@ -110,7 +110,7 @@ Which commands from setup are one off vs need to be run every time?
|
||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||
```bash
|
||||
yarn install
|
||||
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
|
||||
yarn build:wasm
|
||||
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
||||
```
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "angleToMatchLengthX"
|
||||
excerpt: "Compute the angle (in degrees) in o"
|
||||
excerpt: "Returns the angle to match the given length for x."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Compute the angle (in degrees) in o
|
||||
Returns the angle to match the given length for x.
|
||||
|
||||
|
||||
|
||||
|
41
docs/kcl/arcTo.md
Normal file
@ -7,6 +7,7 @@ layout: manual
|
||||
## Table of Contents
|
||||
|
||||
* [Types](kcl/types)
|
||||
* [Modules](kcl/modules)
|
||||
* [Known Issues](kcl/KNOWN-ISSUES)
|
||||
* [`abs`](kcl/abs)
|
||||
* [`acos`](kcl/acos)
|
||||
@ -19,6 +20,7 @@ layout: manual
|
||||
* [`angledLineToX`](kcl/angledLineToX)
|
||||
* [`angledLineToY`](kcl/angledLineToY)
|
||||
* [`arc`](kcl/arc)
|
||||
* [`arcTo`](kcl/arcTo)
|
||||
* [`asin`](kcl/asin)
|
||||
* [`assert`](kcl/assert)
|
||||
* [`assertEqual`](kcl/assertEqual)
|
||||
|
@ -31,7 +31,7 @@ map(array: [KclValue], map_fn: FunctionParam) -> [KclValue]
|
||||
r = 10 // radius
|
||||
fn drawCircle = (id) => {
|
||||
return startSketchOn("XY")
|
||||
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
|
||||
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
|
||||
}
|
||||
|
||||
// Call `drawCircle`, passing in each element of the array.
|
||||
@ -47,7 +47,7 @@ r = 10 // radius
|
||||
// Call `map`, using an anonymous function instead of a named one.
|
||||
circles = map([1..3], (id) => {
|
||||
return startSketchOn("XY")
|
||||
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
|
||||
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
|
||||
})
|
||||
```
|
||||
|
||||
|
59
docs/kcl/modules.md
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "KCL Modules"
|
||||
excerpt: "Documentation of modules for the KCL language for the Zoo Modeling App."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
`KCL` allows splitting code up into multiple files. Each file is somewhat
|
||||
isolated from other files as a separate module.
|
||||
|
||||
When you define a function, you can use `export` before it to make it available
|
||||
to other modules.
|
||||
|
||||
```
|
||||
// util.kcl
|
||||
export fn increment = (x) => {
|
||||
return x + 1
|
||||
}
|
||||
```
|
||||
|
||||
Other files in the project can now import functions that have been exported.
|
||||
This makes them available to use in another file.
|
||||
|
||||
```
|
||||
// main.kcl
|
||||
import increment from "util.kcl"
|
||||
|
||||
answer = increment(41)
|
||||
```
|
||||
|
||||
Imported files _must_ be in the same project so that units are uniform across
|
||||
modules. This means that it must be in the same directory.
|
||||
|
||||
Import statements must be at the top-level of a file. It is not allowed to have
|
||||
an `import` statement inside a function or in the body of an if-else.
|
||||
|
||||
Multiple functions can be exported in a file.
|
||||
|
||||
```
|
||||
// util.kcl
|
||||
export fn increment = (x) => {
|
||||
return x + 1
|
||||
}
|
||||
|
||||
export fn decrement = (x) => {
|
||||
return x - 1
|
||||
}
|
||||
```
|
||||
|
||||
When importing, you can import multiple functions at once.
|
||||
|
||||
```
|
||||
import increment, decrement from "util.kcl"
|
||||
```
|
||||
|
||||
Imported symbols can be renamed for convenience or to avoid name collisions.
|
||||
|
||||
```
|
||||
import increment as inc, decrement as dec from "util.kcl"
|
||||
```
|
@ -96,24 +96,24 @@ fn cube = (length, center) => {
|
||||
p3 = [l + x, -l + y]
|
||||
|
||||
return startSketchAt(p0)
|
||||
|> lineTo(p1, %)
|
||||
|> lineTo(p2, %)
|
||||
|> lineTo(p3, %)
|
||||
|> lineTo(p0, %)
|
||||
|> close(%)
|
||||
|> extrude(length, %)
|
||||
|> lineTo(p1, %)
|
||||
|> lineTo(p2, %)
|
||||
|> lineTo(p3, %)
|
||||
|> lineTo(p0, %)
|
||||
|> close(%)
|
||||
|> extrude(length, %)
|
||||
}
|
||||
|
||||
width = 20
|
||||
fn transform = (i) => {
|
||||
return {
|
||||
// Move down each time.
|
||||
translate: [0, 0, -i * width],
|
||||
// Make the cube longer, wider and flatter each time.
|
||||
scale: [pow(1.1, i), pow(1.1, i), pow(0.9, i)],
|
||||
// Turn by 15 degrees each time.
|
||||
rotation: { angle: 15 * i, origin: "local" }
|
||||
}
|
||||
// Move down each time.
|
||||
translate: [0, 0, -i * width],
|
||||
// Make the cube longer, wider and flatter each time.
|
||||
scale: [pow(1.1, i), pow(1.1, i), pow(0.9, i)],
|
||||
// Turn by 15 degrees each time.
|
||||
rotation: { angle: 15 * i, origin: "local" }
|
||||
}
|
||||
}
|
||||
|
||||
myCubes = cube(width, [100, 0])
|
||||
@ -133,25 +133,25 @@ fn cube = (length, center) => {
|
||||
p3 = [l + x, -l + y]
|
||||
|
||||
return startSketchAt(p0)
|
||||
|> lineTo(p1, %)
|
||||
|> lineTo(p2, %)
|
||||
|> lineTo(p3, %)
|
||||
|> lineTo(p0, %)
|
||||
|> close(%)
|
||||
|> extrude(length, %)
|
||||
|> lineTo(p1, %)
|
||||
|> lineTo(p2, %)
|
||||
|> lineTo(p3, %)
|
||||
|> lineTo(p0, %)
|
||||
|> close(%)
|
||||
|> extrude(length, %)
|
||||
}
|
||||
|
||||
width = 20
|
||||
fn transform = (i) => {
|
||||
return {
|
||||
translate: [0, 0, -i * width],
|
||||
rotation: {
|
||||
angle: 90 * i,
|
||||
// Rotate around the overall scene's origin.
|
||||
origin: "global"
|
||||
translate: [0, 0, -i * width],
|
||||
rotation: {
|
||||
angle: 90 * i,
|
||||
// Rotate around the overall scene's origin.
|
||||
origin: "global"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
myCubes = cube(width, [100, 100])
|
||||
|> patternTransform(4, transform, %)
|
||||
```
|
||||
@ -168,16 +168,16 @@ t = 0.005 // taper factor [0-1)
|
||||
fn transform = (replicaId) => {
|
||||
scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8))
|
||||
return {
|
||||
translate: [0, 0, replicaId * 10],
|
||||
scale: [scale, scale, 0]
|
||||
}
|
||||
translate: [0, 0, replicaId * 10],
|
||||
scale: [scale, scale, 0]
|
||||
}
|
||||
}
|
||||
// Each layer is just a pretty thin cylinder.
|
||||
fn layer = () => {
|
||||
return startSketchOn("XY")
|
||||
// or some other plane idk
|
||||
|> circle({ center: [0, 0], radius: 1 }, %, $tag1)
|
||||
|> extrude(h, %)
|
||||
// or some other plane idk
|
||||
|> circle({ center: [0, 0], radius: 1 }, %, $tag1)
|
||||
|> extrude(h, %)
|
||||
}
|
||||
// The vase is 100 layers tall.
|
||||
// The 100 layers are replica of each other, with a slight transformation applied to each.
|
||||
|
@ -38,8 +38,8 @@ cube = startSketchAt([0, 0])
|
||||
|
||||
fn cylinder = (radius, tag) => {
|
||||
return startSketchAt([0, 0])
|
||||
|> circle({ radius: radius, center: segEnd(tag) }, %)
|
||||
|> extrude(radius, %)
|
||||
|> circle({ radius: radius, center: segEnd(tag) }, %)
|
||||
|> extrude(radius, %)
|
||||
}
|
||||
|
||||
cylinder(1, line1)
|
||||
|
@ -38,11 +38,11 @@ cube = startSketchAt([0, 0])
|
||||
|
||||
fn cylinder = (radius, tag) => {
|
||||
return startSketchAt([0, 0])
|
||||
|> circle({
|
||||
radius: radius,
|
||||
center: segStart(tag)
|
||||
}, %)
|
||||
|> extrude(radius, %)
|
||||
|> circle({
|
||||
radius: radius,
|
||||
center: segStart(tag)
|
||||
}, %)
|
||||
|> extrude(radius, %)
|
||||
}
|
||||
|
||||
cylinder(1, line1)
|
||||
|
9495
docs/kcl/std.json
22
docs/kcl/types/ArcToData.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
title: "ArcToData"
|
||||
excerpt: "Data to draw a three point arc (arcTo)."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data to draw a three point arc (arcTo).
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `end` |`[number, number]`| End point of the arc. A point in 3D space | No |
|
||||
| `interior` |`[number, number]`| Interior point of the arc. A point in 3D space | No |
|
||||
|
||||
|
@ -24,7 +24,7 @@ Autodesk Filmbox (FBX) format
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `fbx`| | No |
|
||||
| `format` |enum: `fbx`| | No |
|
||||
|
||||
|
||||
----
|
||||
@ -40,7 +40,7 @@ Binary glTF 2.0. We refer to this as glTF since that is how our customers refer
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `gltf`| | No |
|
||||
| `format` |enum: `gltf`| | No |
|
||||
|
||||
|
||||
----
|
||||
@ -56,7 +56,7 @@ Wavefront OBJ format.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `obj`| | No |
|
||||
| `format` |enum: `obj`| | No |
|
||||
| `coords` |[`System`](/docs/kcl/types/System)| Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system. | No |
|
||||
| `units` |[`UnitLength`](/docs/kcl/types/UnitLength)| The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters. | No |
|
||||
|
||||
@ -74,7 +74,7 @@ The PLY Polygon File Format.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `ply`| | No |
|
||||
| `format` |enum: `ply`| | No |
|
||||
| `coords` |[`System`](/docs/kcl/types/System)| Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system. | No |
|
||||
| `units` |[`UnitLength`](/docs/kcl/types/UnitLength)| The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters. | No |
|
||||
|
||||
@ -92,7 +92,7 @@ SolidWorks part (SLDPRT) format.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `sldprt`| | No |
|
||||
| `format` |enum: `sldprt`| | No |
|
||||
|
||||
|
||||
----
|
||||
@ -108,7 +108,7 @@ ISO 10303-21 (STEP) format.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `step`| | No |
|
||||
| `format` |enum: `step`| | No |
|
||||
|
||||
|
||||
----
|
||||
@ -124,7 +124,7 @@ ST**ereo**L**ithography format.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `stl`| | No |
|
||||
| `format` |enum: `stl`| | No |
|
||||
| `coords` |[`System`](/docs/kcl/types/System)| Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system. | No |
|
||||
| `units` |[`UnitLength`](/docs/kcl/types/UnitLength)| The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters. | No |
|
||||
|
||||
|
16
docs/kcl/types/KclNone.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
title: "KclNone"
|
||||
excerpt: "KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application)."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -23,8 +23,110 @@ Any KCL value.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `UserVal`| | No |
|
||||
| `value` |``| | No |
|
||||
| `type` |enum: `Uuid`| | No |
|
||||
| `value` |`string`| | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Bool`| | No |
|
||||
| `value` |`boolean`| | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Number`| | No |
|
||||
| `value` |`number`| | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Int`| | No |
|
||||
| `value` |`integer`| | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `String`| | No |
|
||||
| `value` |`string`| | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Array`| | No |
|
||||
| `value` |`[` [`KclValue`](/docs/kcl/types/KclValue) `]`| | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Object`| | No |
|
||||
| `value` |`object`| | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
@ -78,7 +180,7 @@ A plane.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Plane`| | No |
|
||||
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
|
||||
| `id` |`string`| The id of the plane. | No |
|
||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
||||
@ -111,6 +213,38 @@ A face.
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Sketch`](/docs/kcl/types/Sketch)| | No |
|
||||
| `value` |[`Sketch`](/docs/kcl/types/Sketch)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Sketches`| | No |
|
||||
| `value` |`[` [`Sketch`](/docs/kcl/types/Sketch) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
An solid is a collection of extrude surfaces.
|
||||
|
||||
@ -190,6 +324,23 @@ Data for an imported geometry.
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`KclNone`](/docs/kcl/types/KclNone)| | No |
|
||||
| `value` |[`KclNone`](/docs/kcl/types/KclNone)| Any KCL value. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
27
docs/kcl/types/Plane.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
title: "Plane"
|
||||
excerpt: "A plane."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A plane.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `id` |`string`| The id of the plane. | No |
|
||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A plane. | No |
|
||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "PlaneData"
|
||||
excerpt: "Data for a plane."
|
||||
excerpt: "Orientation data that can be used to construct a plane, not a plane in itself."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for a plane.
|
||||
Orientation data that can be used to construct a plane, not a plane in itself.
|
||||
|
||||
|
||||
|
||||
|
@ -22,6 +22,18 @@ Data for start sketch on. You can start a sketch on a plane or an solid.
|
||||
|
||||
|
||||
|
||||
----
|
||||
Data for start sketch on. You can start a sketch on a plane or an solid.
|
||||
|
||||
[`Plane`](/docs/kcl/types/Plane)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Data for start sketch on. You can start a sketch on a plane or an solid.
|
||||
|
||||
|
@ -67,15 +67,15 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|> xLine(${commonPoints.num1}, %)`)
|
||||
}
|
||||
await page.waitForTimeout(500)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|
||||
|> xLine(${commonPoints.num1}, %)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||
} else {
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
@ -84,9 +84,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||
|> lineTo([0, ${commonPoints.num3}], %)`)
|
||||
|> xLine(${commonPoints.num1}, %)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||
}
|
||||
|
||||
// deselect line tool
|
||||
@ -142,9 +142,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
await u.openKclCodePanel()
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %, $seg01)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||
|> angledLine([180, segLen(seg01)], %)`)
|
||||
|> xLine(${commonPoints.num1}, %, $seg01)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||
|> xLine(-segLen(seg01), %)`)
|
||||
}
|
||||
|
||||
test.describe('Basic sketch', () => {
|
||||
|
@ -62,6 +62,8 @@ test(
|
||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
||||
// The open file's name is `main.kcl`, so the export file name should be `main.gltf`
|
||||
const exportFileName = `main.gltf`
|
||||
|
||||
// Click the export button
|
||||
await exportButton.click()
|
||||
@ -96,7 +98,7 @@ test(
|
||||
.poll(
|
||||
async () => {
|
||||
try {
|
||||
const outputGltf = await fsp.readFile('output.gltf')
|
||||
const outputGltf = await fsp.readFile(exportFileName)
|
||||
return outputGltf.byteLength
|
||||
} catch (e) {
|
||||
return 0
|
||||
@ -106,8 +108,8 @@ test(
|
||||
)
|
||||
.toBeGreaterThan(300_000)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
// clean up exported file
|
||||
await fsp.rm(exportFileName)
|
||||
})
|
||||
})
|
||||
|
||||
@ -138,6 +140,8 @@ test(
|
||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
||||
// The open file's name is `other.kcl`, so the export file name should be `other.gltf`
|
||||
const exportFileName = `other.gltf`
|
||||
|
||||
// Click the export button
|
||||
await exportButton.click()
|
||||
@ -171,7 +175,7 @@ test(
|
||||
.poll(
|
||||
async () => {
|
||||
try {
|
||||
const outputGltf = await fsp.readFile('output.gltf')
|
||||
const outputGltf = await fsp.readFile(exportFileName)
|
||||
return outputGltf.byteLength
|
||||
} catch (e) {
|
||||
return 0
|
||||
@ -181,8 +185,8 @@ test(
|
||||
)
|
||||
.toBeGreaterThan(100_000)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
// clean up exported file
|
||||
await fsp.rm(exportFileName)
|
||||
})
|
||||
await electronApp.close()
|
||||
})
|
||||
|
@ -694,6 +694,9 @@ test.describe('Editor tests', () => {
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([3.14, 12], %)
|
||||
|> xLine(5, %) // lin`)
|
||||
|
||||
// expect there to be no KCL errors
|
||||
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('with tab to accept the completion', async ({ page }) => {
|
||||
|
@ -1135,3 +1135,189 @@ _test.describe('Deleting items from the file pane', () => {
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
_test.describe(
|
||||
'Undo and redo do not keep history when navigating between files',
|
||||
() => {
|
||||
_test(
|
||||
`open a file, change something, open a different file, hitting undo should do nothing`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { 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, 'other.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 otherFile = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'other.kcl' }) })
|
||||
|
||||
await _test.step(
|
||||
'Open project and make a change to the file',
|
||||
async () => {
|
||||
await projectCard.click()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// Get the text in the code locator.
|
||||
const originalText = await u.codeLocator.innerText()
|
||||
// Click in the editor and add some new lines.
|
||||
await u.codeLocator.click()
|
||||
|
||||
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|
||||
some other shit`)
|
||||
|
||||
// Ensure the content in the editor changed.
|
||||
const newContent = await u.codeLocator.innerText()
|
||||
|
||||
expect(originalText !== newContent)
|
||||
}
|
||||
)
|
||||
|
||||
await _test.step('navigate to other.kcl', async () => {
|
||||
await u.openFilePanel()
|
||||
|
||||
await otherFile.click()
|
||||
await u.waitForPageLoad()
|
||||
await u.openKclCodePanel()
|
||||
await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
|
||||
})
|
||||
|
||||
await _test.step('hit undo', async () => {
|
||||
// Get the original content of the file.
|
||||
const originalText = await u.codeLocator.innerText()
|
||||
// Now hit undo
|
||||
await page.keyboard.down('ControlOrMeta')
|
||||
await page.keyboard.press('KeyZ')
|
||||
await page.keyboard.up('ControlOrMeta')
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await expect(u.codeLocator).toContainText(originalText)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
_test(
|
||||
`open a file, change something, undo it, open a different file, hitting redo should do nothing`,
|
||||
{ tag: '@electron' },
|
||||
// Skip on windows i think the keybindings are different for redo.
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const { 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, 'other.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 otherFile = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'other.kcl' }) })
|
||||
|
||||
const badContent = 'this shit'
|
||||
await _test.step(
|
||||
'Open project and make a change to the file',
|
||||
async () => {
|
||||
await projectCard.click()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// Get the text in the code locator.
|
||||
const originalText = await u.codeLocator.innerText()
|
||||
// Click in the editor and add some new lines.
|
||||
await u.codeLocator.click()
|
||||
|
||||
await page.keyboard.type(badContent)
|
||||
|
||||
// Ensure the content in the editor changed.
|
||||
const newContent = await u.codeLocator.innerText()
|
||||
|
||||
expect(originalText !== newContent)
|
||||
|
||||
// Now hit undo
|
||||
await page.keyboard.down('ControlOrMeta')
|
||||
await page.keyboard.press('KeyZ')
|
||||
await page.keyboard.up('ControlOrMeta')
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await expect(u.codeLocator).toContainText(originalText)
|
||||
await expect(u.codeLocator).not.toContainText(badContent)
|
||||
|
||||
// Hit redo.
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.down('ControlOrMeta')
|
||||
await page.keyboard.press('KeyZ')
|
||||
await page.keyboard.up('ControlOrMeta')
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await expect(u.codeLocator).toContainText(originalText)
|
||||
await expect(u.codeLocator).toContainText(badContent)
|
||||
|
||||
// Now hit undo
|
||||
await page.keyboard.down('ControlOrMeta')
|
||||
await page.keyboard.press('KeyZ')
|
||||
await page.keyboard.up('ControlOrMeta')
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await expect(u.codeLocator).toContainText(originalText)
|
||||
await expect(u.codeLocator).not.toContainText(badContent)
|
||||
}
|
||||
)
|
||||
|
||||
await _test.step('navigate to other.kcl', async () => {
|
||||
await u.openFilePanel()
|
||||
|
||||
await otherFile.click()
|
||||
await u.waitForPageLoad()
|
||||
await u.openKclCodePanel()
|
||||
await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
|
||||
await expect(u.codeLocator).not.toContainText(badContent)
|
||||
})
|
||||
|
||||
await _test.step('hit redo', async () => {
|
||||
// Get the original content of the file.
|
||||
const originalText = await u.codeLocator.innerText()
|
||||
// Now hit redo
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.down('ControlOrMeta')
|
||||
await page.keyboard.press('KeyZ')
|
||||
await page.keyboard.up('ControlOrMeta')
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await expect(u.codeLocator).toContainText(originalText)
|
||||
await expect(u.codeLocator).not.toContainText(badContent)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -452,7 +452,7 @@ sketch002 = startSketchOn(extrude001, seg03)
|
||||
)
|
||||
})
|
||||
|
||||
test(`Verify axis and origin snapping`, async ({
|
||||
test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||
app,
|
||||
editor,
|
||||
toolbar,
|
||||
@ -505,7 +505,7 @@ test(`Verify axis and origin snapping`, async ({
|
||||
const expectedCodeSnippets = {
|
||||
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
|
||||
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
|
||||
segmentOnXAxis: `lineTo([${xAxisSloppy.kcl[0]}, ${xAxisSloppy.kcl[1]}], %)`,
|
||||
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
|
||||
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
|
||||
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
|
||||
}
|
||||
|
@ -247,7 +247,7 @@ test.describe('Can export from electron app', () => {
|
||||
.poll(
|
||||
async () => {
|
||||
try {
|
||||
const outputGltf = await fsp.readFile('output.gltf')
|
||||
const outputGltf = await fsp.readFile('main.gltf')
|
||||
return outputGltf.byteLength
|
||||
} catch (e) {
|
||||
return 0
|
||||
@ -257,8 +257,8 @@ test.describe('Can export from electron app', () => {
|
||||
)
|
||||
.toBeGreaterThan(300_000)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
// clean up exported file
|
||||
await fsp.rm('main.gltf')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
|
@ -115,7 +115,7 @@ test.describe('Sketch tests', () => {
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([4.61, -14.01], %)
|
||||
|> line([12.73, -0.09], %)
|
||||
|> xLine(12.73, %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)`
|
||||
)
|
||||
})
|
||||
@ -156,7 +156,7 @@ test.describe('Sketch tests', () => {
|
||||
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
||||
.toBe(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([12.34, -12.34], %)
|
||||
|> line([-12.34, 12.34], %)
|
||||
|> yLine(12.34, %)
|
||||
|
||||
`)
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
@ -645,7 +645,7 @@ test.describe('Sketch tests', () => {
|
||||
await u.openDebugPanel()
|
||||
|
||||
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
||||
const { toSU, click00r } = getMovementUtils({ center, page })
|
||||
const { toSU, toU, click00r } = getMovementUtils({ center, page })
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
@ -674,16 +674,15 @@ test.describe('Sketch tests', () => {
|
||||
|
||||
await click00r(50, 0)
|
||||
await page.waitForTimeout(100)
|
||||
codeStr += ` |> lineTo(${toSU([50, 0])}, %)`
|
||||
codeStr += ` |> xLine(${toU(50, 0)[0]}, %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(0, 50)
|
||||
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
||||
codeStr += ` |> yLine(${toU(0, 50)[1]}, %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
let clickCoords = await click00r(-50, 0)
|
||||
expect(clickCoords).not.toBeUndefined()
|
||||
codeStr += ` |> lineTo(${toSU(clickCoords!)}, %)`
|
||||
await click00r(-50, 0)
|
||||
codeStr += ` |> xLine(${toU(-50, 0)[0]}, %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
// exit the sketch, reset relative clicker
|
||||
@ -712,15 +711,15 @@ test.describe('Sketch tests', () => {
|
||||
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
||||
// it to be off by 0.01
|
||||
await click00r(30, 0)
|
||||
codeStr += ` |> lineTo([4.07, 0], %)`
|
||||
codeStr += ` |> xLine(2.04, %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(0, 30)
|
||||
codeStr += ` |> line([0, -2.03], %)`
|
||||
codeStr += ` |> yLine(-2.03, %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(-30, 0)
|
||||
codeStr += ` |> line([-2.04, 0], %)`
|
||||
codeStr += ` |> xLine(-2.04, %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(undefined, undefined)
|
||||
@ -744,8 +743,8 @@ test.describe('Sketch tests', () => {
|
||||
|
||||
const code = `sketch001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|
||||
|> line([${roundOff(scale * 139.19)}, 0], %)
|
||||
|> line([0, -${roundOff(scale * 139.2)}], %)
|
||||
|> xLine(${roundOff(scale * 139.19)}, %)
|
||||
|> yLine(-${roundOff(scale * 139.2)}, %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`
|
||||
|
||||
@ -1275,3 +1274,44 @@ test2.describe('Sketch mode should be toleratant to syntax errors', () => {
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test2.describe(`Sketching with offset planes`, () => {
|
||||
test2(
|
||||
`Can select an offset plane to sketch on`,
|
||||
async ({ app, scene, toolbar, editor }) => {
|
||||
// We seed the scene with a single offset plane
|
||||
await app.initialise(`offsetPlane001 = offsetPlane("XY", 10)`)
|
||||
|
||||
const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200)
|
||||
|
||||
await test2.step(`Start sketching on the offset plane`, async () => {
|
||||
await toolbar.startSketchPlaneSelection()
|
||||
|
||||
await test2.step(`Hovering should highlight code`, async () => {
|
||||
await planeHover()
|
||||
await editor.expectState({
|
||||
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
|
||||
diagnostics: [],
|
||||
highlightedCode: 'offsetPlane("XY", 10)',
|
||||
})
|
||||
})
|
||||
|
||||
await test2.step(
|
||||
`Clicking should select the plane and enter sketch mode`,
|
||||
async () => {
|
||||
await planeClick()
|
||||
// Have to wait for engine-side animation to finish
|
||||
await app.page.waitForTimeout(600)
|
||||
await expect2(toolbar.lineBtn).toBeEnabled()
|
||||
await editor.expectEditor.toContain('startSketchOn(offsetPlane001)')
|
||||
await editor.expectState({
|
||||
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
|
||||
diagnostics: [],
|
||||
highlightedCode: '',
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -283,7 +283,7 @@ part001 = startSketchOn('-XZ')
|
||||
const gltfFilename = filenames.filter((t: string) =>
|
||||
t.includes('.gltf')
|
||||
)[0]
|
||||
if (!gltfFilename) throw new Error('No output.gltf in this archive')
|
||||
if (!gltfFilename) throw new Error('No gLTF in this archive')
|
||||
cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${parentPath}/${gltfFilename} ${imagePath}`
|
||||
}
|
||||
|
||||
@ -462,7 +462,7 @@ test(
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
code += `
|
||||
|> line([7.25, 0], %)`
|
||||
|> xLine(7.25, %)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
|
||||
await page
|
||||
@ -647,7 +647,7 @@ test.describe(
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
code += `
|
||||
|> line([7.25, 0], %)`
|
||||
|> xLine(7.25, %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page
|
||||
@ -752,7 +752,7 @@ test.describe(
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
code += `
|
||||
|> line([184.3, 0], %)`
|
||||
|> xLine(184.3, %)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
|
||||
await page
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
@ -141,7 +141,7 @@ test.describe('Test network and connection issues', () => {
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|> xLine(${commonPoints.num1}, %)`)
|
||||
|
||||
// Expect the network to be up
|
||||
await expect(networkToggle).toContainText('Connected')
|
||||
@ -207,7 +207,7 @@ test.describe('Test network and connection issues', () => {
|
||||
await expect.poll(u.normalisedEditorCode)
|
||||
.toBe(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([12.34, -12.34], %)
|
||||
|> line([12.34, 0], %)
|
||||
|> xLine(12.34, %)
|
||||
|> line([-12.34, 12.34], %)
|
||||
|
||||
`)
|
||||
@ -217,9 +217,9 @@ test.describe('Test network and connection issues', () => {
|
||||
await expect.poll(u.normalisedEditorCode)
|
||||
.toBe(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([12.34, -12.34], %)
|
||||
|> line([12.34, 0], %)
|
||||
|> xLine(12.34, %)
|
||||
|> line([-12.34, 12.34], %)
|
||||
|> lineTo([0, -12.34], %)
|
||||
|> xLine(-12.34, %)
|
||||
|
||||
`)
|
||||
|
||||
|
@ -305,7 +305,7 @@ export const getMovementUtils = (opts: any) => {
|
||||
return [last.x, last.y]
|
||||
}
|
||||
|
||||
return { toSU, click00r }
|
||||
return { toSU, toU, click00r }
|
||||
}
|
||||
|
||||
async function waitForAuthAndLsp(page: Page) {
|
||||
|
@ -32,10 +32,17 @@ test.describe('Testing selections', () => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
const xAxisClick = () =>
|
||||
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
|
||||
const yAxisClick = () =>
|
||||
test.step('Click on Y axis', async () => {
|
||||
await page.mouse.move(600, 200, { steps: 5 })
|
||||
await page.mouse.click(600, 200)
|
||||
await page.waitForTimeout(100)
|
||||
})
|
||||
const xAxisClickAfterExitingSketch = () =>
|
||||
page.mouse.click(639, 278).then(() => page.waitForTimeout(100))
|
||||
test.step(`Click on X axis after exiting sketch, which shifts it at the moment`, async () => {
|
||||
await page.mouse.click(639, 278)
|
||||
await page.waitForTimeout(100)
|
||||
})
|
||||
const emptySpaceHover = () =>
|
||||
test.step('Hover over empty space', async () => {
|
||||
await page.mouse.move(700, 143, { steps: 5 })
|
||||
@ -80,23 +87,23 @@ test.describe('Testing selections', () => {
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|> xLine(${commonPoints.num1}, %)`)
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|
||||
|> xLine(${commonPoints.num1}, %)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
||||
|> lineTo([0, ${commonPoints.num3}], %)`)
|
||||
|> xLine(${commonPoints.num1}, %)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||
|
||||
// deselect line tool
|
||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
@ -121,53 +128,58 @@ test.describe('Testing selections', () => {
|
||||
// now check clicking works including axis
|
||||
|
||||
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
||||
await topHorzSegmentClick()
|
||||
await page.keyboard.down('Shift')
|
||||
const constrainButton = page.getByRole('button', {
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
const absYButton = page.getByRole('button', { name: 'Absolute Y' })
|
||||
await constrainButton.click()
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
await xAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await constrainButton.click()
|
||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
const absXButton = page.getByRole('button', { name: 'Absolute X' })
|
||||
|
||||
await test.step(`Select a segment and an axis, see that a relevant constraint is enabled`, async () => {
|
||||
await topHorzSegmentClick()
|
||||
await page.keyboard.down('Shift')
|
||||
await constrainButton.click()
|
||||
await expect(absXButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
await yAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await constrainButton.click()
|
||||
await absXButton.and(page.locator(':not([disabled])')).waitFor()
|
||||
await expect(absXButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await emptySpaceClick()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
// same selection but click the axis first
|
||||
await xAxisClick()
|
||||
await constrainButton.click()
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
await topHorzSegmentClick()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.keyboard.up('Shift')
|
||||
await constrainButton.click()
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
await test.step(`Same selection but click the axis first`, async () => {
|
||||
await yAxisClick()
|
||||
await constrainButton.click()
|
||||
await expect(absXButton).toBeDisabled()
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
await topHorzSegmentClick()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.keyboard.up('Shift')
|
||||
await constrainButton.click()
|
||||
await expect(absXButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await emptySpaceClick()
|
||||
|
||||
// check the same selection again by putting cursor in code first then selecting axis
|
||||
await page
|
||||
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
||||
.click()
|
||||
await page.keyboard.down('Shift')
|
||||
await constrainButton.click()
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
await xAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await constrainButton.click()
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
await test.step(`Same selection but code selection then axis`, async () => {
|
||||
await page
|
||||
.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`)
|
||||
.click()
|
||||
await page.keyboard.down('Shift')
|
||||
await constrainButton.click()
|
||||
await expect(absXButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
await yAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await constrainButton.click()
|
||||
await expect(absXButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await emptySpaceClick()
|
||||
@ -182,9 +194,7 @@ test.describe('Testing selections', () => {
|
||||
process.platform === 'linux' ? 'Control' : 'Meta'
|
||||
)
|
||||
await page.waitForTimeout(100)
|
||||
await page
|
||||
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
||||
.click()
|
||||
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
|
||||
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
await page.waitForTimeout(500)
|
||||
@ -928,6 +938,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
|
||||
// test fillet button with the body in the scene
|
||||
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
||||
extrude001 = extrude(10, sketch001)`
|
||||
await u.codeLocator.clear()
|
||||
await u.codeLocator.fill(codeToAdd)
|
||||
await selectSegment()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
|
@ -258,7 +258,7 @@ test.describe('Testing settings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test(
|
||||
test.fixme(
|
||||
`Project settings override user settings on desktop`,
|
||||
{ tag: ['@electron', '@skipWin'] },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
@ -318,7 +318,6 @@ test.describe('Testing settings', () => {
|
||||
timeout: 5_000,
|
||||
})
|
||||
.toContain(`themeColor = "${userThemeColor}"`)
|
||||
// Only close the button after we've confirmed
|
||||
})
|
||||
|
||||
await test.step('Set project theme color', async () => {
|
||||
|
1
interface.d.ts
vendored
@ -78,6 +78,7 @@ export interface IElectronAPI {
|
||||
) => Electron.IpcRenderer
|
||||
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||
appRestart: () => void
|
||||
getArgvParsed: () => any
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zoo-modeling-app",
|
||||
"version": "0.26.3",
|
||||
"version": "0.27.0",
|
||||
"private": true,
|
||||
"productName": "Zoo Modeling App",
|
||||
"author": {
|
||||
@ -40,7 +40,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-updater": "6.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
@ -65,7 +65,8 @@
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"web-vitals": "^3.5.2",
|
||||
"xstate": "^5.17.4"
|
||||
"xstate": "^5.17.4",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -22,6 +22,10 @@ import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||
maybeWriteToDisk()
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
} from 'react-router-dom'
|
||||
import { ErrorPage } from './components/ErrorPage'
|
||||
import { Settings } from './routes/Settings'
|
||||
import { Telemetry } from './routes/Telemetry'
|
||||
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||
import SignIn from './routes/SignIn'
|
||||
import { Auth } from './Auth'
|
||||
@ -28,6 +29,7 @@ import {
|
||||
homeLoader,
|
||||
onboardingRedirectLoader,
|
||||
settingsLoader,
|
||||
telemetryLoader,
|
||||
} from 'lib/routeLoaders'
|
||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||
@ -43,6 +45,7 @@ import { coreDump } from 'lang/wasm'
|
||||
import { useMemo } from 'react'
|
||||
import { AppStateProvider } from 'AppState'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { RouteProvider } from 'components/RouteProvider'
|
||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||
|
||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||
@ -56,19 +59,21 @@ const router = createRouter([
|
||||
* inefficient re-renders, use the react profiler to see. */
|
||||
element: (
|
||||
<CommandBarProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<ProjectsContextProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
<RouteProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<ProjectsContextProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</RouteProvider>
|
||||
</CommandBarProvider>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
@ -124,6 +129,16 @@ const router = createRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: PATHS.FILE + 'TELEMETRY',
|
||||
loader: telemetryLoader,
|
||||
children: [
|
||||
{
|
||||
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
||||
element: <Telemetry />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -149,6 +164,11 @@ const router = createRouter([
|
||||
loader: settingsLoader,
|
||||
element: <Settings />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
||||
loader: telemetryLoader,
|
||||
element: <Telemetry />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -141,6 +141,7 @@ export function Toolbar({
|
||||
>
|
||||
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
|
||||
{currentModeItems.map((maybeIconConfig, i) => {
|
||||
// Vertical Line Break
|
||||
if (maybeIconConfig === 'break') {
|
||||
return (
|
||||
<div
|
||||
@ -149,6 +150,7 @@ export function Toolbar({
|
||||
/>
|
||||
)
|
||||
} else if (Array.isArray(maybeIconConfig)) {
|
||||
// A button with a dropdown
|
||||
return (
|
||||
<ActionButtonDropdown
|
||||
Element="button"
|
||||
@ -215,6 +217,7 @@ export function Toolbar({
|
||||
}
|
||||
const itemConfig = maybeIconConfig
|
||||
|
||||
// A single button
|
||||
return (
|
||||
<div className="relative" key={itemConfig.id}>
|
||||
<ActionButton
|
||||
|
@ -202,12 +202,20 @@ const Overlay = ({
|
||||
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
|
||||
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
|
||||
|
||||
// It's possible for the pathToNode to request a newer AST node
|
||||
// than what's available in the AST at the moment of query.
|
||||
// It eventually settles on being updated.
|
||||
const _node1 = getNodeFromPath<Node<CallExpression>>(
|
||||
kclManager.ast,
|
||||
overlay.pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(_node1)) return
|
||||
|
||||
// For that reason, to prevent console noise, we do not use err here.
|
||||
if (_node1 instanceof Error) {
|
||||
console.warn('ast older than pathToNode, not fatal, eventually settles', '')
|
||||
return
|
||||
}
|
||||
const callExpression = _node1.node
|
||||
|
||||
const constraints = getConstraintInfo(
|
||||
@ -637,10 +645,16 @@ const ConstraintSymbol = ({
|
||||
kclManager.ast,
|
||||
kclManager.programMemory
|
||||
)
|
||||
|
||||
if (!transform) return
|
||||
const { modifiedAst } = transform
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
kclManager.updateAst(modifiedAst, true)
|
||||
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
|
||||
// Code editor will be updated in the modelingMachine.
|
||||
const newCode = recast(modifiedAst)
|
||||
if (err(newCode)) return
|
||||
await codeManager.updateCodeEditor(newCode)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
Vector3,
|
||||
} from 'three'
|
||||
import {
|
||||
ANGLE_SNAP_THRESHOLD_DEGREES,
|
||||
ARROWHEAD,
|
||||
AXIS_GROUP,
|
||||
DRAFT_POINT,
|
||||
@ -46,6 +47,7 @@ import {
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
sketchFromKclValue,
|
||||
sketchFromKclValueOptional,
|
||||
} from 'lang/wasm'
|
||||
import {
|
||||
engineCommandManager,
|
||||
@ -88,13 +90,15 @@ import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import {
|
||||
getRectangleCallExpressions,
|
||||
updateRectangleSketch,
|
||||
updateCenterRectangleSketch,
|
||||
} from 'lib/rectangleTool'
|
||||
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { err, Reason, reportRejection, trap } from 'lib/trap'
|
||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||
import { SegmentInputs } from 'lang/std/stdTypes'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { radToDeg } from 'three/src/math/MathUtils'
|
||||
|
||||
type DraftSegment = 'line' | 'tangentialArcTo'
|
||||
|
||||
@ -451,6 +455,7 @@ export class SceneEntities {
|
||||
const { modifiedAst } = addStartProfileAtRes
|
||||
|
||||
await kclManager.updateAst(modifiedAst, false)
|
||||
|
||||
this.removeIntersectionPlane()
|
||||
this.scene.remove(draftPointGroup)
|
||||
|
||||
@ -683,7 +688,7 @@ export class SceneEntities {
|
||||
})
|
||||
return nextAst
|
||||
}
|
||||
setUpDraftSegment = async (
|
||||
setupDraftSegment = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
forward: [number, number, number],
|
||||
up: [number, number, number],
|
||||
@ -798,11 +803,24 @@ export class SceneEntities {
|
||||
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||
)
|
||||
|
||||
const lastSegment = sketch.paths.slice(-1)[0]
|
||||
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
|
||||
const snappedPoint = {
|
||||
x: intersectsYAxis ? 0 : intersection2d.x,
|
||||
y: intersectsXAxis ? 0 : intersection2d.y,
|
||||
}
|
||||
// Get the angle between the previous segment (or sketch start)'s end and this one's
|
||||
const angle = Math.atan2(
|
||||
snappedPoint.y - lastSegment.to[1],
|
||||
snappedPoint.x - lastSegment.to[0]
|
||||
)
|
||||
|
||||
const isHorizontal =
|
||||
radToDeg(Math.abs(angle)) < ANGLE_SNAP_THRESHOLD_DEGREES ||
|
||||
Math.abs(radToDeg(Math.abs(angle) - Math.PI)) <
|
||||
ANGLE_SNAP_THRESHOLD_DEGREES
|
||||
const isVertical =
|
||||
Math.abs(radToDeg(Math.abs(angle) - Math.PI / 2)) <
|
||||
ANGLE_SNAP_THRESHOLD_DEGREES
|
||||
|
||||
let resolvedFunctionName: ToolTip = 'line'
|
||||
|
||||
@ -810,6 +828,12 @@ export class SceneEntities {
|
||||
// case-based logic for different segment types
|
||||
if (lastSegment.type === 'TangentialArcTo') {
|
||||
resolvedFunctionName = 'tangentialArcTo'
|
||||
} else if (isHorizontal) {
|
||||
// If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine
|
||||
resolvedFunctionName = 'xLine'
|
||||
} else if (isVertical) {
|
||||
// If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a yLine
|
||||
resolvedFunctionName = 'yLine'
|
||||
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
|
||||
// We consider a point placed on axes or origin to be absolute
|
||||
resolvedFunctionName = 'lineTo'
|
||||
@ -835,10 +859,11 @@ export class SceneEntities {
|
||||
}
|
||||
|
||||
await kclManager.executeAstMock(modifiedAst)
|
||||
|
||||
if (intersectsProfileStart) {
|
||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||
} else {
|
||||
await this.setUpDraftSegment(
|
||||
await this.setupDraftSegment(
|
||||
sketchPathToNode,
|
||||
forward,
|
||||
up,
|
||||
@ -846,6 +871,8 @@ export class SceneEntities {
|
||||
segmentName
|
||||
)
|
||||
}
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
|
||||
},
|
||||
onMove: (args) => {
|
||||
this.onDragSegment({
|
||||
@ -970,8 +997,179 @@ export class SceneEntities {
|
||||
if (trap(_node)) return
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
if (sketchInit.type !== 'PipeExpression') {
|
||||
return
|
||||
}
|
||||
|
||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||
|
||||
const newCode = recast(_ast)
|
||||
let _recastAst = parse(newCode)
|
||||
if (trap(_recastAst)) return
|
||||
_ast = _recastAst
|
||||
|
||||
// Update the primary AST and unequip the rectangle tool
|
||||
await kclManager.executeAstMock(_ast)
|
||||
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
||||
|
||||
// lee: I had this at the bottom of the function, but it's
|
||||
// possible sketchFromKclValue "fails" when sketching on a face,
|
||||
// and this couldn't wouldn't run.
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
||||
|
||||
const { execState } = await executeAst({
|
||||
ast: _ast,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
})
|
||||
const programMemory = execState.memory
|
||||
|
||||
// Prepare to update the THREEjs scene
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketch = sketchFromKclValue(
|
||||
programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sketch)) return
|
||||
const sgPaths = sketch.paths
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
// Update the starting segment of the THREEjs scene
|
||||
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
|
||||
// Update the rest of the segments of the THREEjs scene
|
||||
sgPaths.forEach((seg, index) =>
|
||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
setupDraftCenterRectangle = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
forward: [number, number, number],
|
||||
up: [number, number, number],
|
||||
sketchOrigin: [number, number, number],
|
||||
rectangleOrigin: [x: number, y: number]
|
||||
) => {
|
||||
let _ast = structuredClone(kclManager.ast)
|
||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
||||
_ast,
|
||||
sketchPathToNode || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
|
||||
// startSketchOn already exists
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
const tags: [string, string, string] = [
|
||||
findUniqueName(_ast, 'rectangleSegmentA'),
|
||||
findUniqueName(_ast, 'rectangleSegmentB'),
|
||||
findUniqueName(_ast, 'rectangleSegmentC'),
|
||||
]
|
||||
|
||||
startSketchOn[0].init = createPipeExpression([
|
||||
startSketchOnInit,
|
||||
...getRectangleCallExpressions(rectangleOrigin, tags),
|
||||
])
|
||||
|
||||
let _recastAst = parse(recast(_ast))
|
||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
||||
_ast = _recastAst
|
||||
|
||||
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
|
||||
sketchPathToNode,
|
||||
forward,
|
||||
up,
|
||||
position: sketchOrigin,
|
||||
maybeModdedAst: _ast,
|
||||
draftExpressionsIndices: { start: 0, end: 3 },
|
||||
})
|
||||
|
||||
sceneInfra.setCallbacks({
|
||||
onMove: async (args) => {
|
||||
// Update the width and height of the draft rectangle
|
||||
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
||||
pathToNodeTwo[1][0] = 0
|
||||
|
||||
const _node = getNodeFromPath<VariableDeclaration>(
|
||||
truncatedAst,
|
||||
pathToNodeTwo || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return Promise.reject(_node)
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
|
||||
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
||||
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||
updateCenterRectangleSketch(
|
||||
sketchInit,
|
||||
x,
|
||||
y,
|
||||
tags[0],
|
||||
rectangleOrigin[0],
|
||||
rectangleOrigin[1]
|
||||
)
|
||||
}
|
||||
|
||||
const { execState } = await executeAst({
|
||||
ast: truncatedAst,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
})
|
||||
const programMemory = execState.memory
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketch = sketchFromKclValue(
|
||||
programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sketch)) return Promise.reject(sketch)
|
||||
const sgPaths = sketch.paths
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
|
||||
sgPaths.forEach((seg, index) =>
|
||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
|
||||
)
|
||||
},
|
||||
onClick: async (args) => {
|
||||
// If there is a valid camera interaction that matches, do that instead
|
||||
const interaction = sceneInfra.camControls.getInteractionType(
|
||||
args.mouseEvent
|
||||
)
|
||||
if (interaction !== 'none') return
|
||||
// Commit the rectangle to the full AST/code and return to sketch.idle
|
||||
const cornerPoint = args.intersectionPoint?.twoD
|
||||
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
||||
|
||||
const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0])
|
||||
const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1])
|
||||
|
||||
const _node = getNodeFromPath<VariableDeclaration>(
|
||||
_ast,
|
||||
sketchPathToNode || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
updateCenterRectangleSketch(
|
||||
sketchInit,
|
||||
x,
|
||||
y,
|
||||
tags[0],
|
||||
rectangleOrigin[0],
|
||||
rectangleOrigin[1]
|
||||
)
|
||||
|
||||
let _recastAst = parse(recast(_ast))
|
||||
if (trap(_recastAst)) return
|
||||
@ -979,7 +1177,12 @@ export class SceneEntities {
|
||||
|
||||
// Update the primary AST and unequip the rectangle tool
|
||||
await kclManager.executeAstMock(_ast)
|
||||
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
||||
sceneInfra.modelingSend({ type: 'Finish center rectangle' })
|
||||
|
||||
// lee: I had this at the bottom of the function, but it's
|
||||
// possible sketchFromKclValue "fails" when sketching on a face,
|
||||
// and this couldn't wouldn't run.
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
||||
|
||||
const { execState } = await executeAst({
|
||||
ast: _ast,
|
||||
@ -1166,13 +1369,17 @@ export class SceneEntities {
|
||||
if (err(moddedResult)) return
|
||||
modded = moddedResult.modifiedAst
|
||||
|
||||
let _recastAst = parse(recast(modded))
|
||||
const newCode = recast(modded)
|
||||
if (err(newCode)) return
|
||||
let _recastAst = parse(newCode)
|
||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
||||
_ast = _recastAst
|
||||
|
||||
// Update the primary AST and unequip the rectangle tool
|
||||
await kclManager.executeAstMock(_ast)
|
||||
sceneInfra.modelingSend({ type: 'Finish circle' })
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -1208,6 +1415,7 @@ export class SceneEntities {
|
||||
forward,
|
||||
position,
|
||||
})
|
||||
await codeManager.writeToFile()
|
||||
}
|
||||
},
|
||||
onDrag: async ({
|
||||
@ -1490,10 +1698,13 @@ export class SceneEntities {
|
||||
this.sceneProgramMemory = programMemory
|
||||
|
||||
const maybeSketch = programMemory.get(variableDeclarationName)
|
||||
let sketch = undefined
|
||||
const sg = sketchFromKclValue(maybeSketch, variableDeclarationName)
|
||||
if (!err(sg)) {
|
||||
sketch = sg
|
||||
let sketch: Sketch | undefined
|
||||
const sk = sketchFromKclValueOptional(
|
||||
maybeSketch,
|
||||
variableDeclarationName
|
||||
)
|
||||
if (!(sk instanceof Reason)) {
|
||||
sketch = sk
|
||||
} else if ((maybeSketch as Solid).sketch) {
|
||||
sketch = (maybeSketch as Solid).sketch
|
||||
}
|
||||
|
@ -50,6 +50,8 @@ export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||
|
||||
export const X_AXIS = 'xAxis'
|
||||
export const Y_AXIS = 'yAxis'
|
||||
/** If a segment angle is less than this many degrees off a meanginful angle it'll snap to it */
|
||||
export const ANGLE_SNAP_THRESHOLD_DEGREES = 3
|
||||
/** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */
|
||||
export const DRAFT_POINT_GROUP = 'draft-point-group'
|
||||
/** the THREEjs representation of a "snapped" point that is not yet placed */
|
||||
|
12
src/commandLineArgs.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import yargs from 'yargs'
|
||||
import { hideBin } from 'yargs/helpers'
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.option('telemetry', {
|
||||
alias: 't',
|
||||
type: 'boolean',
|
||||
description: 'Writes startup telemetry to file on disk.',
|
||||
})
|
||||
.parse()
|
||||
|
||||
export default argv
|
@ -145,7 +145,7 @@ export function useCalc({
|
||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||
for (const { key, value } of availableVarInfo.variables) {
|
||||
const error = _programMem.set(key, {
|
||||
type: 'UserVal',
|
||||
type: 'String',
|
||||
value,
|
||||
__meta: [],
|
||||
})
|
||||
|
@ -1161,6 +1161,29 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
stopwatch: (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.95705 5.99046C7.05643 6.44935 6.33654 7.19809 5.91336 8.11602C5.49019 9.03396 5.38838 10.0676 5.62434 11.0505C5.8603 12.0334 6.42029 12.9081 7.21408 13.5339C8.00787 14.1597 8.98922 14.5 10 14.5C11.0108 14.5 11.9921 14.1597 12.7859 13.5339C13.5797 12.9082 14.1397 12.0334 14.3757 11.0505C14.6116 10.0676 14.5098 9.03396 14.0866 8.11603C13.6635 7.19809 12.9436 6.44935 12.043 5.99046L12.497 5.09946C13.5977 5.66032 14.4776 6.57544 14.9948 7.69737C15.512 8.81929 15.6364 10.0827 15.348 11.2839C15.0596 12.4852 14.3752 13.5544 13.405 14.3192C12.4348 15.0841 11.2354 15.5 10 15.5C8.7646 15.5 7.56517 15.0841 6.59499 14.3192C5.6248 13.5544 4.94037 12.4852 4.65197 11.2839C4.36357 10.0827 4.488 8.81929 5.00522 7.69736C5.52243 6.57544 6.40231 5.66032 7.50306 5.09946L7.95705 5.99046Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M10 5.5V4M10 4H8M10 4H12" stroke="currentColor" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.8536 7.85356L10.3536 10.3536C10.1583 10.5488 9.84171 10.5488 9.64645 10.3536C9.45118 10.1583 9.45118 9.84172 9.64645 9.64645L12.1464 7.14645L12.8536 7.85356Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
} as const
|
||||
|
||||
export type CustomIconName = keyof typeof CustomIconMap
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
KclSamplesManifestItem,
|
||||
} from 'lib/getKclSamplesManifest'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { markOnce } from 'lib/performance'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -54,6 +55,7 @@ export const FileMachineProvider = ({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
markOnce('code/didLoadFile')
|
||||
async function fetchKclSamples() {
|
||||
setKclSamples(await getKclSamplesManifest())
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import usePlatform from 'hooks/usePlatform'
|
||||
import { FileEntry } from 'lib/project'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { normalizeLineEndings } from 'lib/codeEditor'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
@ -196,8 +197,7 @@ const FileTreeItem = ({
|
||||
return
|
||||
}
|
||||
|
||||
// Don't try to read a file that was removed.
|
||||
if (isCurrentFile && eventType !== 'unlink') {
|
||||
if (isCurrentFile && eventType === 'change') {
|
||||
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
|
||||
code = normalizeLineEndings(code)
|
||||
codeManager.updateCodeStateEditor(code)
|
||||
@ -242,7 +242,7 @@ const FileTreeItem = ({
|
||||
// Show the renaming form
|
||||
addCurrentItemToRenaming()
|
||||
} else if (e.code === 'Space') {
|
||||
void handleClick()
|
||||
void handleClick().catch(reportRejection)
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,7 +293,7 @@ const FileTreeItem = ({
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onClick={(e) => {
|
||||
e.currentTarget.focus()
|
||||
void handleClick()
|
||||
void handleClick().catch(reportRejection)
|
||||
}}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
|
@ -96,6 +96,23 @@ export function LowerRightControls({
|
||||
Report a bug
|
||||
</Tooltip>
|
||||
</a>
|
||||
<Link
|
||||
to={
|
||||
location.pathname.includes(PATHS.FILE)
|
||||
? filePath + PATHS.TELEMETRY + '?tab=project'
|
||||
: PATHS.HOME + PATHS.TELEMETRY
|
||||
}
|
||||
data-testid="telemetry-link"
|
||||
>
|
||||
<CustomIcon
|
||||
name="stopwatch"
|
||||
className={`w-5 h-5 ${linkOverrideClassName}`}
|
||||
/>
|
||||
<span className="sr-only">Telemetry</span>
|
||||
<Tooltip position="top" contentClassName="text-xs">
|
||||
Telemetry
|
||||
</Tooltip>
|
||||
</Link>
|
||||
<Link
|
||||
to={
|
||||
location.pathname.includes(PATHS.FILE)
|
||||
|
@ -63,6 +63,7 @@ import {
|
||||
import {
|
||||
moveValueIntoNewVariablePath,
|
||||
sketchOnExtrudedFace,
|
||||
sketchOnOffsetPlane,
|
||||
startSketchOnDefault,
|
||||
} from 'lang/modifyAst'
|
||||
import { Program, parse, recast } from 'lang/wasm'
|
||||
@ -304,6 +305,7 @@ export const ModelingMachineProvider = ({
|
||||
const dispatchSelection = (selection?: EditorSelection) => {
|
||||
if (!selection) return // TODO less of hack for the below please
|
||||
if (!editorManager.editorView) return
|
||||
|
||||
setTimeout(() => {
|
||||
if (!editorManager.editorView) return
|
||||
editorManager.editorView.dispatch({
|
||||
@ -482,7 +484,7 @@ export const ModelingMachineProvider = ({
|
||||
engineCommandManager.exportInfo = {
|
||||
intent: ExportIntent.Save,
|
||||
// This never gets used its only for make.
|
||||
name: '',
|
||||
name: file?.name?.replace('.kcl', `.${event.data.type}`) || '',
|
||||
}
|
||||
|
||||
const format = {
|
||||
@ -635,13 +637,16 @@ export const ModelingMachineProvider = ({
|
||||
),
|
||||
'animate-to-face': fromPromise(async ({ input }) => {
|
||||
if (!input) return undefined
|
||||
if (input.type === 'extrudeFace') {
|
||||
const sketched = sketchOnExtrudedFace(
|
||||
kclManager.ast,
|
||||
input.sketchPathToNode,
|
||||
input.extrudePathToNode,
|
||||
input.faceInfo
|
||||
)
|
||||
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
|
||||
const sketched =
|
||||
input.type === 'extrudeFace'
|
||||
? sketchOnExtrudedFace(
|
||||
kclManager.ast,
|
||||
input.sketchPathToNode,
|
||||
input.extrudePathToNode,
|
||||
input.faceInfo
|
||||
)
|
||||
: sketchOnOffsetPlane(kclManager.ast, input.pathToNode)
|
||||
if (err(sketched)) {
|
||||
const sketchedError = new Error(
|
||||
'Incompatible face, please try another'
|
||||
@ -653,10 +658,9 @@ export const ModelingMachineProvider = ({
|
||||
|
||||
await kclManager.executeAstMock(modifiedAst)
|
||||
|
||||
await letEngineAnimateAndSyncCamAfter(
|
||||
engineCommandManager,
|
||||
input.faceId
|
||||
)
|
||||
const id =
|
||||
input.type === 'extrudeFace' ? input.faceId : input.planeId
|
||||
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
return {
|
||||
sketchPathToNode: pathToNewSketchNode,
|
||||
@ -732,6 +736,11 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.origin
|
||||
)
|
||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
pathToNodeMap,
|
||||
selectionRanges,
|
||||
@ -768,6 +777,11 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.origin
|
||||
)
|
||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
pathToNodeMap,
|
||||
selectionRanges,
|
||||
@ -813,6 +827,11 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.origin
|
||||
)
|
||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
pathToNodeMap,
|
||||
selectionRanges,
|
||||
@ -846,6 +865,11 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.origin
|
||||
)
|
||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
pathToNodeMap,
|
||||
selectionRanges,
|
||||
@ -881,6 +905,11 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.origin
|
||||
)
|
||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
pathToNodeMap,
|
||||
selectionRanges,
|
||||
@ -917,6 +946,11 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.origin
|
||||
)
|
||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
pathToNodeMap,
|
||||
selectionRanges,
|
||||
@ -953,6 +987,11 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.origin
|
||||
)
|
||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
pathToNodeMap,
|
||||
selectionRanges,
|
||||
@ -999,6 +1038,11 @@ export const ModelingMachineProvider = ({
|
||||
sketchDetails.origin
|
||||
)
|
||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
{ 0: pathToReplacedNode },
|
||||
selectionRanges,
|
||||
|
@ -4,7 +4,7 @@ import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||
import { lineHighlightField } from 'editor/highlightextension'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { onMouseDragMakeANewNumber, onMouseDragRegex } from 'lib/utils'
|
||||
import {
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
@ -43,6 +43,7 @@ import {
|
||||
completionKeymap,
|
||||
} from '@codemirror/autocomplete'
|
||||
import CodeEditor from './CodeEditor'
|
||||
import { codeManagerHistoryCompartment } from 'lang/codeManager'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
@ -89,7 +90,7 @@ export const KclEditorPane = () => {
|
||||
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
||||
}),
|
||||
lineHighlightField,
|
||||
history(),
|
||||
codeManagerHistoryCompartment.of(history()),
|
||||
closeBrackets(),
|
||||
codeFolding(),
|
||||
keymap.of([
|
||||
@ -121,7 +122,6 @@ export const KclEditorPane = () => {
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
@ -129,7 +129,9 @@ export const KclEditorPane = () => {
|
||||
closeBrackets(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
syntaxHighlighting(defaultHighlightStyle, {
|
||||
fallback: true,
|
||||
}),
|
||||
rectangularSelection(),
|
||||
dropCursor(),
|
||||
interact({
|
||||
@ -137,29 +139,12 @@ export const KclEditorPane = () => {
|
||||
// a rule for a number dragger
|
||||
{
|
||||
// the regexp matching the value
|
||||
regexp: /-?\b\d+\.?\d*\b/g,
|
||||
regexp: onMouseDragRegex,
|
||||
// set cursor to "ew-resize" on hover
|
||||
cursor: 'ew-resize',
|
||||
// change number value based on mouse X movement on drag
|
||||
onDrag: (text, setText, e) => {
|
||||
const multiplier =
|
||||
e.shiftKey && e.metaKey
|
||||
? 0.01
|
||||
: e.metaKey
|
||||
? 0.1
|
||||
: e.shiftKey
|
||||
? 10
|
||||
: 1
|
||||
|
||||
const delta = e.movementX * multiplier
|
||||
|
||||
const newVal = roundOff(
|
||||
Number(text) + delta,
|
||||
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
|
||||
)
|
||||
|
||||
if (isNaN(newVal)) return
|
||||
setText(newVal.toString())
|
||||
onMouseDragMakeANewNumber(text, setText, e)
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -5,12 +5,12 @@ import {
|
||||
ProgramMemory,
|
||||
Path,
|
||||
ExtrudeSurface,
|
||||
sketchFromKclValue,
|
||||
sketchFromKclValueOptional,
|
||||
} from 'lang/wasm'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { useResolvedTheme } from 'hooks/useResolvedTheme'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { Reason, trap } from 'lib/trap'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
@ -89,17 +89,17 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
const processedMemory: any = {}
|
||||
for (const [key, val] of programMemory?.visibleEntries()) {
|
||||
if (
|
||||
(val.type === 'UserVal' && val.value.type === 'Sketch') ||
|
||||
val.type === 'Sketch' ||
|
||||
// @ts-ignore
|
||||
(val.type !== 'Function' && val.type !== 'UserVal')
|
||||
val.type !== 'Function'
|
||||
) {
|
||||
const sg = sketchFromKclValue(val, key)
|
||||
const sk = sketchFromKclValueOptional(val, key)
|
||||
if (val.type === 'Solid') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
} else if (!err(sg)) {
|
||||
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => {
|
||||
} else if (!(sk instanceof Reason)) {
|
||||
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else {
|
||||
@ -110,8 +110,6 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
processedMemory[key] = `__function(${(val as any)?.expression?.params
|
||||
?.map?.(({ identifier }: any) => identifier?.name || '')
|
||||
.join(', ')})__`
|
||||
} else {
|
||||
processedMemory[key] = val.value
|
||||
}
|
||||
}
|
||||
return processedMemory
|
||||
|
33
src/components/RouteProvider.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useEffect, useState, createContext, ReactNode } from 'react'
|
||||
import { useNavigation, useLocation } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { markOnce } from 'lib/performance'
|
||||
|
||||
export const RouteProviderContext = createContext({})
|
||||
|
||||
export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
const [first, setFirstState] = useState(true)
|
||||
const navigation = useNavigation()
|
||||
const location = useLocation()
|
||||
useEffect(() => {
|
||||
// On initialization, the react-router-dom does not send a 'loading' state event.
|
||||
// it sends an idle event first.
|
||||
const pathname = first ? location.pathname : navigation.location?.pathname
|
||||
const isHome = pathname === PATHS.HOME
|
||||
const isFile =
|
||||
pathname?.includes(PATHS.FILE) &&
|
||||
pathname?.substring(pathname?.length - 4) === '.kcl'
|
||||
if (isHome) {
|
||||
markOnce('code/willLoadHome')
|
||||
} else if (isFile) {
|
||||
markOnce('code/willLoadFile')
|
||||
}
|
||||
setFirstState(false)
|
||||
}, [navigation])
|
||||
|
||||
return (
|
||||
<RouteProviderContext.Provider value={{}}>
|
||||
{children}
|
||||
</RouteProviderContext.Provider>
|
||||
)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { trap } from 'lib/trap'
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
import React, { createContext, useEffect, useState } from 'react'
|
||||
@ -42,6 +42,7 @@ import { getAppSettingsFilePath } from 'lib/desktop'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -288,6 +289,44 @@ export const SettingsAuthProviderBase = ({
|
||||
settingsWithCommandConfigs,
|
||||
])
|
||||
|
||||
// Due to the route provider, i've moved this to the SettingsAuthProvider instead of CommandBarProvider
|
||||
// This will register the commands to route to Telemetry, Home, and Settings.
|
||||
useEffect(() => {
|
||||
const filePath =
|
||||
PATHS.FILE +
|
||||
'/' +
|
||||
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||
createRouteCommands(navigate, location, filePath)
|
||||
commandBarSend({
|
||||
type: 'Remove commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteHomeCommand,
|
||||
RouteSettingsCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
if (location.pathname === PATHS.HOME) {
|
||||
commandBarSend({
|
||||
type: 'Add commands',
|
||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||
})
|
||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||
commandBarSend({
|
||||
type: 'Add commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteSettingsCommand,
|
||||
RouteHomeCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [location])
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
// This is only done if the theme setting is set to 'system'.
|
||||
// It can't be done in XState (in an invoked callback, for example)
|
||||
|
72
src/components/TelemetryExplorer.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { getMarks } from 'lib/performance'
|
||||
|
||||
import {
|
||||
printDeltaTotal,
|
||||
printInvocationCount,
|
||||
printMarkDownTable,
|
||||
printRawMarks,
|
||||
} from 'lib/telemetry'
|
||||
|
||||
export function TelemetryExplorer() {
|
||||
const marks = getMarks()
|
||||
const markdownTable = printMarkDownTable(marks)
|
||||
const rawMarks = printRawMarks(marks)
|
||||
const deltaTotalTable = printDeltaTotal(marks)
|
||||
const invocationCount = printInvocationCount(marks)
|
||||
// TODO data-telemetry-type
|
||||
// TODO data-telemetry-name
|
||||
return (
|
||||
<div>
|
||||
<h1 className="pb-4">Marks</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{marks.map((mark, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{JSON.stringify(mark, null, 2)}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Startup Performance</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{markdownTable.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Delta and Totals</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{deltaTotalTable.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Raw Marks</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{rawMarks.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Invocation Count</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{invocationCount.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
81
src/editor/manager.test.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { editorManager } from 'lib/singletons'
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
|
||||
describe('EditorManager Class', () => {
|
||||
describe('makeUniqueDiagnostics', () => {
|
||||
it('should filter out duplicated diagnostics', () => {
|
||||
const duplicatedDiagnostics: Diagnostic[] = [
|
||||
{
|
||||
from: 2,
|
||||
to: 10,
|
||||
severity: 'hint',
|
||||
message: 'my cool message',
|
||||
},
|
||||
{
|
||||
from: 2,
|
||||
to: 10,
|
||||
severity: 'hint',
|
||||
message: 'my cool message',
|
||||
},
|
||||
{
|
||||
from: 2,
|
||||
to: 10,
|
||||
severity: 'hint',
|
||||
message: 'my cool message',
|
||||
},
|
||||
]
|
||||
|
||||
const expected: Diagnostic[] = [
|
||||
{
|
||||
from: 2,
|
||||
to: 10,
|
||||
severity: 'hint',
|
||||
message: 'my cool message',
|
||||
},
|
||||
]
|
||||
|
||||
const actual = editorManager.makeUniqueDiagnostics(duplicatedDiagnostics)
|
||||
expect(actual).toStrictEqual(expected)
|
||||
})
|
||||
it('should filter out duplicated diagnostic and keep some original ones', () => {
|
||||
const duplicatedDiagnostics: Diagnostic[] = [
|
||||
{
|
||||
from: 0,
|
||||
to: 10,
|
||||
severity: 'hint',
|
||||
message: 'my cool message',
|
||||
},
|
||||
{
|
||||
from: 0,
|
||||
to: 10,
|
||||
severity: 'hint',
|
||||
message: 'my cool message',
|
||||
},
|
||||
{
|
||||
from: 88,
|
||||
to: 99,
|
||||
severity: 'hint',
|
||||
message: 'my super cool message',
|
||||
},
|
||||
]
|
||||
|
||||
const expected: Diagnostic[] = [
|
||||
{
|
||||
from: 0,
|
||||
to: 10,
|
||||
severity: 'hint',
|
||||
message: 'my cool message',
|
||||
},
|
||||
{
|
||||
from: 88,
|
||||
to: 99,
|
||||
severity: 'hint',
|
||||
message: 'my super cool message',
|
||||
},
|
||||
]
|
||||
|
||||
const actual = editorManager.makeUniqueDiagnostics(duplicatedDiagnostics)
|
||||
expect(actual).toStrictEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,4 +1,5 @@
|
||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
@ -12,6 +13,7 @@ import {
|
||||
setDiagnosticsEffect,
|
||||
} from '@codemirror/lint'
|
||||
import { StateFrom } from 'xstate'
|
||||
import { markOnce } from 'lib/performance'
|
||||
|
||||
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
||||
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
||||
@ -22,10 +24,6 @@ export const modelingMachineEvent = modelingMachineAnnotation.of(true)
|
||||
const setDiagnosticsAnnotation = Annotation.define<boolean>()
|
||||
export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(true)
|
||||
|
||||
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
|
||||
private _copilotEnabled: boolean = true
|
||||
@ -59,6 +57,49 @@ export default class EditorManager {
|
||||
|
||||
setEditorView(editorView: EditorView) {
|
||||
this._editorView = editorView
|
||||
this.overrideTreeHighlighterUpdateForPerformanceTracking()
|
||||
}
|
||||
|
||||
overrideTreeHighlighterUpdateForPerformanceTracking() {
|
||||
// @ts-ignore
|
||||
this._editorView?.plugins.forEach((e) => {
|
||||
let sawATreeDiff = false
|
||||
|
||||
// we cannot use <>.constructor.name since it will get destroyed
|
||||
// when packaging the application.
|
||||
const isTreeHighlightPlugin =
|
||||
e?.value &&
|
||||
e.value?.hasOwnProperty('tree') &&
|
||||
e.value?.hasOwnProperty('decoratedTo') &&
|
||||
e.value?.hasOwnProperty('decorations')
|
||||
|
||||
if (isTreeHighlightPlugin) {
|
||||
let originalUpdate = e.value.update
|
||||
// @ts-ignore
|
||||
function performanceTrackingUpdate(args) {
|
||||
/**
|
||||
* TreeHighlighter.update will be called multiple times on start up.
|
||||
* We do not want to track the highlight performance of an empty update.
|
||||
* mark the syntax highlight one time when the new tree comes in with the
|
||||
* initial code
|
||||
*/
|
||||
const treeIsDifferent =
|
||||
// @ts-ignore
|
||||
!sawATreeDiff && this.tree !== syntaxTree(args.state)
|
||||
if (treeIsDifferent && !sawATreeDiff) {
|
||||
markOnce('code/willSyntaxHighlight')
|
||||
}
|
||||
// Call the original function
|
||||
// @ts-ignore
|
||||
originalUpdate.apply(this, [args])
|
||||
if (treeIsDifferent && !sawATreeDiff) {
|
||||
markOnce('code/didSyntaxHighlight')
|
||||
sawATreeDiff = true
|
||||
}
|
||||
}
|
||||
e.value.update = performanceTrackingUpdate
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get editorView(): EditorView | null {
|
||||
@ -117,20 +158,29 @@ export default class EditorManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of Diagnostics remove any duplicates by hashing a key
|
||||
* in the format of from + ' ' + to + ' ' + message.
|
||||
*/
|
||||
makeUniqueDiagnostics(duplicatedDiagnostics: Diagnostic[]): Diagnostic[] {
|
||||
const uniqueDiagnostics: Diagnostic[] = []
|
||||
const seenDiagnostic: { [key: string]: boolean } = {}
|
||||
|
||||
duplicatedDiagnostics.forEach((diagnostic: Diagnostic) => {
|
||||
const hash = `${diagnostic.from} ${diagnostic.to} ${diagnostic.message}`
|
||||
if (!seenDiagnostic[hash]) {
|
||||
uniqueDiagnostics.push(diagnostic)
|
||||
seenDiagnostic[hash] = true
|
||||
}
|
||||
})
|
||||
|
||||
return uniqueDiagnostics
|
||||
}
|
||||
|
||||
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this._editorView) return
|
||||
// Clear out any existing diagnostics that are the same.
|
||||
for (const diagnostic of diagnostics) {
|
||||
for (const otherDiagnostic of diagnostics) {
|
||||
if (diagnosticIsEqual(diagnostic, otherDiagnostic)) {
|
||||
diagnostics = diagnostics.filter(
|
||||
(d) => !diagnosticIsEqual(d, diagnostic)
|
||||
)
|
||||
diagnostics.push(diagnostic)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
diagnostics = this.makeUniqueDiagnostics(diagnostics)
|
||||
|
||||
this._editorView.dispatch({
|
||||
effects: [setDiagnosticsEffect.of(diagnostics)],
|
||||
|
@ -88,6 +88,10 @@ export function useEngineConnectionSubscriptions() {
|
||||
? [codeRef.range]
|
||||
: [codeRef.range, consumedCodeRef.range]
|
||||
)
|
||||
} else if (artifact?.type === 'plane') {
|
||||
const codeRef = artifact.codeRef
|
||||
if (err(codeRef)) return
|
||||
editorManager.setHighlightRange([codeRef.range])
|
||||
} else {
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}
|
||||
@ -186,8 +190,42 @@ export function useEngineConnectionSubscriptions() {
|
||||
})
|
||||
return
|
||||
}
|
||||
const artifact =
|
||||
engineCommandManager.artifactGraph.get(planeOrFaceId)
|
||||
|
||||
if (artifact?.type === 'plane') {
|
||||
const planeInfo = await getFaceDetails(planeOrFaceId)
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Select default plane',
|
||||
data: {
|
||||
type: 'offsetPlane',
|
||||
zAxis: [
|
||||
planeInfo.z_axis.x,
|
||||
planeInfo.z_axis.y,
|
||||
planeInfo.z_axis.z,
|
||||
],
|
||||
yAxis: [
|
||||
planeInfo.y_axis.x,
|
||||
planeInfo.y_axis.y,
|
||||
planeInfo.y_axis.z,
|
||||
],
|
||||
position: [
|
||||
planeInfo.origin.x,
|
||||
planeInfo.origin.y,
|
||||
planeInfo.origin.z,
|
||||
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
],
|
||||
planeId: planeOrFaceId,
|
||||
pathToNode: artifact.codeRef.pathToNode,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Artifact is likely an extrusion face
|
||||
const faceId = planeOrFaceId
|
||||
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
||||
const extrusion = getSweepFromSuspectedSweepSurface(
|
||||
faceId,
|
||||
engineCommandManager.artifactGraph
|
||||
|
@ -2,13 +2,13 @@ import {
|
||||
SetVarNameModal,
|
||||
createSetVarNameModal,
|
||||
} from 'components/SetVarNameModal'
|
||||
import { editorManager, kclManager } from 'lib/singletons'
|
||||
import { reportRejection, trap } from 'lib/trap'
|
||||
import { editorManager, kclManager, codeManager } from 'lib/singletons'
|
||||
import { reportRejection, trap, err } from 'lib/trap'
|
||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
import { PathToNode, SourceRange } from 'lang/wasm'
|
||||
import { PathToNode, SourceRange, recast } from 'lang/wasm'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { toSync } from 'lib/utils'
|
||||
|
||||
@ -57,6 +57,11 @@ export function useConvertToVariable(range?: SourceRange) {
|
||||
)
|
||||
|
||||
await kclManager.updateAst(_modifiedAst, true)
|
||||
|
||||
const newCode = recast(_modifiedAst)
|
||||
if (err(newCode)) return
|
||||
codeManager.updateCodeEditor(newCode)
|
||||
|
||||
return pathToReplacedNode
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
|
@ -8,8 +8,10 @@ import ModalContainer from 'react-modal-promise'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { AppStreamProvider } from 'AppState'
|
||||
import { ToastUpdate } from 'components/ToastUpdate'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
||||
|
||||
markOnce('code/willAuth')
|
||||
// uncomment for xstate inspector
|
||||
// import { DEV } from 'env'
|
||||
// import { inspect } from '@xstate/inspect'
|
||||
|