Compare commits
88 Commits
stream-pau
...
v0.27.0
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 | |||
64f0f5b773 | |||
f452f9bf00 | |||
97705234c6 | |||
30dfc167d3 | |||
d8105627c0 | |||
6b7fac3642 | |||
35805916aa | |||
4a4400e979 | |||
efd1f288b9 | |||
0337ab9cff | |||
f0dda692f6 | |||
2ce0c59d08 | |||
393b43d485 | |||
4fbcde8773 | |||
12d444fa69 | |||
683b4488af | |||
e1c1e07046 | |||
984420c155 | |||
7bad60dfa3 | |||
aaca88220c | |||
360384e8c8 | |||
ab2ad1313f | |||
897205acc2 | |||
862ca1124e | |||
d9981d9d7b | |||
8df0581831 | |||
54e6358df1 | |||
daf20a978d | |||
8e64798dda | |||
a1ceb4fa47 | |||
2db8d13051 | |||
aceb8052e2 | |||
62fae1e93b | |||
2abfbb9788 | |||
ad1cd56891 | |||
26951364cf | |||
26e995dc3f | |||
a8b816a3e2 | |||
43bec115c0 | |||
0c6c646fe7 | |||
0d52851da2 | |||
6b105897f7 | |||
9ff51de301 | |||
c161f578fd | |||
4804eedf3e | |||
99db31a6a4 | |||
90b57ec202 | |||
3f86f99f5e | |||
83e2b093a6 | |||
58f7e0086d |
41
docs/kcl/arcTo.md
Normal file
@ -7,6 +7,7 @@ layout: manual
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
* [Types](kcl/types)
|
* [Types](kcl/types)
|
||||||
|
* [Modules](kcl/modules)
|
||||||
* [Known Issues](kcl/KNOWN-ISSUES)
|
* [Known Issues](kcl/KNOWN-ISSUES)
|
||||||
* [`abs`](kcl/abs)
|
* [`abs`](kcl/abs)
|
||||||
* [`acos`](kcl/acos)
|
* [`acos`](kcl/acos)
|
||||||
@ -19,6 +20,7 @@ layout: manual
|
|||||||
* [`angledLineToX`](kcl/angledLineToX)
|
* [`angledLineToX`](kcl/angledLineToX)
|
||||||
* [`angledLineToY`](kcl/angledLineToY)
|
* [`angledLineToY`](kcl/angledLineToY)
|
||||||
* [`arc`](kcl/arc)
|
* [`arc`](kcl/arc)
|
||||||
|
* [`arcTo`](kcl/arcTo)
|
||||||
* [`asin`](kcl/asin)
|
* [`asin`](kcl/asin)
|
||||||
* [`assert`](kcl/assert)
|
* [`assert`](kcl/assert)
|
||||||
* [`assertEqual`](kcl/assertEqual)
|
* [`assertEqual`](kcl/assertEqual)
|
||||||
|
@ -31,7 +31,7 @@ map(array: [KclValue], map_fn: FunctionParam) -> [KclValue]
|
|||||||
r = 10 // radius
|
r = 10 // radius
|
||||||
fn drawCircle = (id) => {
|
fn drawCircle = (id) => {
|
||||||
return startSketchOn("XY")
|
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.
|
// 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.
|
// Call `map`, using an anonymous function instead of a named one.
|
||||||
circles = map([1..3], (id) => {
|
circles = map([1..3], (id) => {
|
||||||
return startSketchOn("XY")
|
return startSketchOn("XY")
|
||||||
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
|
|> circle({ center: [id * 2 * r, 0], radius: r }, %)
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ Offset a plane by a distance along its normal.
|
|||||||
For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ' plane and 10 units away from it.
|
For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ' plane and 10 units away from it.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
offsetPlane(std_plane: StandardPlane, offset: number) -> PlaneData
|
offsetPlane(std_plane: StandardPlane, offset: number) -> Plane
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ offsetPlane(std_plane: StandardPlane, offset: number) -> PlaneData
|
|||||||
|
|
||||||
### Returns
|
### Returns
|
||||||
|
|
||||||
[`PlaneData`](/docs/kcl/types/PlaneData) - Data for a plane.
|
[`Plane`](/docs/kcl/types/Plane) - A plane.
|
||||||
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
@ -96,24 +96,24 @@ fn cube = (length, center) => {
|
|||||||
p3 = [l + x, -l + y]
|
p3 = [l + x, -l + y]
|
||||||
|
|
||||||
return startSketchAt(p0)
|
return startSketchAt(p0)
|
||||||
|> lineTo(p1, %)
|
|> lineTo(p1, %)
|
||||||
|> lineTo(p2, %)
|
|> lineTo(p2, %)
|
||||||
|> lineTo(p3, %)
|
|> lineTo(p3, %)
|
||||||
|> lineTo(p0, %)
|
|> lineTo(p0, %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(length, %)
|
|> extrude(length, %)
|
||||||
}
|
}
|
||||||
|
|
||||||
width = 20
|
width = 20
|
||||||
fn transform = (i) => {
|
fn transform = (i) => {
|
||||||
return {
|
return {
|
||||||
// Move down each time.
|
// Move down each time.
|
||||||
translate: [0, 0, -i * width],
|
translate: [0, 0, -i * width],
|
||||||
// Make the cube longer, wider and flatter each time.
|
// Make the cube longer, wider and flatter each time.
|
||||||
scale: [pow(1.1, i), pow(1.1, i), pow(0.9, i)],
|
scale: [pow(1.1, i), pow(1.1, i), pow(0.9, i)],
|
||||||
// Turn by 15 degrees each time.
|
// Turn by 15 degrees each time.
|
||||||
rotation: { angle: 15 * i, origin: "local" }
|
rotation: { angle: 15 * i, origin: "local" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
myCubes = cube(width, [100, 0])
|
myCubes = cube(width, [100, 0])
|
||||||
@ -133,25 +133,25 @@ fn cube = (length, center) => {
|
|||||||
p3 = [l + x, -l + y]
|
p3 = [l + x, -l + y]
|
||||||
|
|
||||||
return startSketchAt(p0)
|
return startSketchAt(p0)
|
||||||
|> lineTo(p1, %)
|
|> lineTo(p1, %)
|
||||||
|> lineTo(p2, %)
|
|> lineTo(p2, %)
|
||||||
|> lineTo(p3, %)
|
|> lineTo(p3, %)
|
||||||
|> lineTo(p0, %)
|
|> lineTo(p0, %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(length, %)
|
|> extrude(length, %)
|
||||||
}
|
}
|
||||||
|
|
||||||
width = 20
|
width = 20
|
||||||
fn transform = (i) => {
|
fn transform = (i) => {
|
||||||
return {
|
return {
|
||||||
translate: [0, 0, -i * width],
|
translate: [0, 0, -i * width],
|
||||||
rotation: {
|
rotation: {
|
||||||
angle: 90 * i,
|
angle: 90 * i,
|
||||||
// Rotate around the overall scene's origin.
|
// Rotate around the overall scene's origin.
|
||||||
origin: "global"
|
origin: "global"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
myCubes = cube(width, [100, 100])
|
myCubes = cube(width, [100, 100])
|
||||||
|> patternTransform(4, transform, %)
|
|> patternTransform(4, transform, %)
|
||||||
```
|
```
|
||||||
@ -168,16 +168,16 @@ t = 0.005 // taper factor [0-1)
|
|||||||
fn transform = (replicaId) => {
|
fn transform = (replicaId) => {
|
||||||
scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8))
|
scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8))
|
||||||
return {
|
return {
|
||||||
translate: [0, 0, replicaId * 10],
|
translate: [0, 0, replicaId * 10],
|
||||||
scale: [scale, scale, 0]
|
scale: [scale, scale, 0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Each layer is just a pretty thin cylinder.
|
// Each layer is just a pretty thin cylinder.
|
||||||
fn layer = () => {
|
fn layer = () => {
|
||||||
return startSketchOn("XY")
|
return startSketchOn("XY")
|
||||||
// or some other plane idk
|
// or some other plane idk
|
||||||
|> circle({ center: [0, 0], radius: 1 }, %, $tag1)
|
|> circle({ center: [0, 0], radius: 1 }, %, $tag1)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)
|
||||||
}
|
}
|
||||||
// The vase is 100 layers tall.
|
// The vase is 100 layers tall.
|
||||||
// The 100 layers are replica of each other, with a slight transformation applied to each.
|
// 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) => {
|
fn cylinder = (radius, tag) => {
|
||||||
return startSketchAt([0, 0])
|
return startSketchAt([0, 0])
|
||||||
|> circle({ radius: radius, center: segEnd(tag) }, %)
|
|> circle({ radius: radius, center: segEnd(tag) }, %)
|
||||||
|> extrude(radius, %)
|
|> extrude(radius, %)
|
||||||
}
|
}
|
||||||
|
|
||||||
cylinder(1, line1)
|
cylinder(1, line1)
|
||||||
|
@ -38,11 +38,11 @@ cube = startSketchAt([0, 0])
|
|||||||
|
|
||||||
fn cylinder = (radius, tag) => {
|
fn cylinder = (radius, tag) => {
|
||||||
return startSketchAt([0, 0])
|
return startSketchAt([0, 0])
|
||||||
|> circle({
|
|> circle({
|
||||||
radius: radius,
|
radius: radius,
|
||||||
center: segStart(tag)
|
center: segStart(tag)
|
||||||
}, %)
|
}, %)
|
||||||
|> extrude(radius, %)
|
|> extrude(radius, %)
|
||||||
}
|
}
|
||||||
|
|
||||||
cylinder(1, line1)
|
cylinder(1, line1)
|
||||||
|
4680
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 |
|
| 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 |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: `gltf`| | No |
|
| `format` |enum: `gltf`| | No |
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
@ -56,7 +56,7 @@ Wavefront OBJ format.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| 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 |
|
| `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 |
|
| `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 |
|
| 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 |
|
| `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 |
|
| `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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| `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 |
|
| `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 |
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ A plane.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: `Plane`| | No |
|
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
|
||||||
| `id` |`string`| The id of the plane. | No |
|
| `id` |`string`| The id of the plane. | No |
|
||||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
||||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | 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"
|
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
|
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.
|
Data for start sketch on. You can start a sketch on a plane or an solid.
|
||||||
|
|
||||||
|
@ -62,6 +62,8 @@ test(
|
|||||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
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
|
// Click the export button
|
||||||
await exportButton.click()
|
await exportButton.click()
|
||||||
@ -96,7 +98,7 @@ test(
|
|||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const outputGltf = await fsp.readFile('output.gltf')
|
const outputGltf = await fsp.readFile(exportFileName)
|
||||||
return outputGltf.byteLength
|
return outputGltf.byteLength
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 0
|
return 0
|
||||||
@ -106,8 +108,8 @@ test(
|
|||||||
)
|
)
|
||||||
.toBeGreaterThan(300_000)
|
.toBeGreaterThan(300_000)
|
||||||
|
|
||||||
// clean up output.gltf
|
// clean up exported file
|
||||||
await fsp.rm('output.gltf')
|
await fsp.rm(exportFileName)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -138,6 +140,8 @@ test(
|
|||||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
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
|
// Click the export button
|
||||||
await exportButton.click()
|
await exportButton.click()
|
||||||
@ -171,7 +175,7 @@ test(
|
|||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const outputGltf = await fsp.readFile('output.gltf')
|
const outputGltf = await fsp.readFile(exportFileName)
|
||||||
return outputGltf.byteLength
|
return outputGltf.byteLength
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 0
|
return 0
|
||||||
@ -181,8 +185,8 @@ test(
|
|||||||
)
|
)
|
||||||
.toBeGreaterThan(100_000)
|
.toBeGreaterThan(100_000)
|
||||||
|
|
||||||
// clean up output.gltf
|
// clean up exported file
|
||||||
await fsp.rm('output.gltf')
|
await fsp.rm(exportFileName)
|
||||||
})
|
})
|
||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -247,7 +247,7 @@ test.describe('Can export from electron app', () => {
|
|||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const outputGltf = await fsp.readFile('output.gltf')
|
const outputGltf = await fsp.readFile('main.gltf')
|
||||||
return outputGltf.byteLength
|
return outputGltf.byteLength
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 0
|
return 0
|
||||||
@ -257,8 +257,8 @@ test.describe('Can export from electron app', () => {
|
|||||||
)
|
)
|
||||||
.toBeGreaterThan(300_000)
|
.toBeGreaterThan(300_000)
|
||||||
|
|
||||||
// clean up output.gltf
|
// clean up exported file
|
||||||
await fsp.rm('output.gltf')
|
await fsp.rm('main.gltf')
|
||||||
})
|
})
|
||||||
|
|
||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
|
@ -202,19 +202,35 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const viewport = { width: 1200, height: 500 }
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.setViewportSize(viewport)
|
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
|
|
||||||
const center = {
|
await page.waitForTimeout(100)
|
||||||
x: viewport.width / 2,
|
await u.openAndClearDebugPanel()
|
||||||
y: viewport.height / 2,
|
await u.sendCustomCmd({
|
||||||
}
|
type: 'modeling_cmd_req',
|
||||||
const modelAreaSize = await u.getModelViewAreaSize()
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
vantage: { x: 0, y: -1250, z: 580 },
|
||||||
|
center: { x: 0, y: 0, z: 0 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// If we have the code pane open, we should see the code.
|
// If we have the code pane open, we should see the code.
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
@ -228,7 +244,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await expect(u.codeLocator).not.toBeVisible()
|
await expect(u.codeLocator).not.toBeVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPX = [center.x + 65, 458]
|
const startPX = [665, 458]
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
let prevContent = ''
|
let prevContent = ''
|
||||||
@ -239,7 +255,7 @@ test.describe('Sketch tests', () => {
|
|||||||
// Wait for the render.
|
// Wait for the render.
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
// Select the sketch
|
// Select the sketch
|
||||||
await page.mouse.click(center.x + 100, 370)
|
await page.mouse.click(700, 370)
|
||||||
}
|
}
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit Sketch' })
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
@ -250,74 +266,45 @@ test.describe('Sketch tests', () => {
|
|||||||
prevContent = await page.locator('.cm-content').innerText()
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
await u.openAndClearDebugPanel()
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
vantage: { x: 0, y: -1250, z: 580 - modelAreaSize.w },
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
const step5 = { steps: 5 }
|
const step5 = { steps: 5 }
|
||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
test.step('drag startProfileAt handle', async () => {
|
// drag startProfieAt handle
|
||||||
await page.mouse.move(startPX[0], startPX[1])
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
if (openPanes.includes('code')) {
|
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
if (openPanes.includes('code')) {
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// drag line handle
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
test.step('drag line handle', async () => {
|
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||||
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
||||||
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
await page.mouse.down()
|
||||||
await page.mouse.down()
|
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
|
||||||
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
|
await page.mouse.up()
|
||||||
await page.mouse.up()
|
await page.waitForTimeout(100)
|
||||||
await page.waitForTimeout(100)
|
if (openPanes.includes('code')) {
|
||||||
if (openPanes.includes('code')) {
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test.step('drag tangentialArcTo handle', async () => {
|
// drag tangentialArcTo handle
|
||||||
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||||
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
|
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(
|
await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5)
|
||||||
tangentEnd.x + dragPX,
|
await page.mouse.up()
|
||||||
tangentEnd.y - dragPX,
|
await page.waitForTimeout(100)
|
||||||
step5
|
if (openPanes.includes('code')) {
|
||||||
)
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
await page.mouse.up()
|
}
|
||||||
await page.waitForTimeout(100)
|
|
||||||
if (openPanes.includes('code')) {
|
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Open the code pane
|
// Open the code pane
|
||||||
await u.openKclCodePanel()
|
await u.openKclCodePanel()
|
||||||
@ -593,7 +580,7 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
const startPX = [665, 458]
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
|
|
||||||
@ -609,7 +596,7 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
// drag startProfileAt handle
|
// drag startProfieAt handle
|
||||||
await page.mouse.move(startPX[0], startPX[1])
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
@ -651,7 +638,6 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
|
||||||
const viewportSize = { width: 1200, height: 500 }
|
const viewportSize = { width: 1200, height: 500 }
|
||||||
await page.setViewportSize(viewportSize)
|
await page.setViewportSize(viewportSize)
|
||||||
|
|
||||||
@ -675,19 +661,15 @@ test.describe('Sketch tests', () => {
|
|||||||
200
|
200
|
||||||
)
|
)
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
|
||||||
|
|
||||||
let codeStr = "sketch001 = startSketchOn('XY')"
|
let codeStr = "sketch001 = startSketchOn('XY')"
|
||||||
|
|
||||||
await page.mouse.click(center.x - 50, viewportSize.height * 0.55)
|
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
const { click00r } = await getMovementUtils({ center, page })
|
await click00r(0, 0)
|
||||||
|
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
||||||
let coord = await click00r(0, 0)
|
|
||||||
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(50, 0)
|
await click00r(50, 0)
|
||||||
@ -716,15 +698,14 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
// when exiting the sketch above the camera is still looking down at XY,
|
// when exiting the sketch above the camera is still looking down at XY,
|
||||||
// so selecting the plane again is a bit easier.
|
// so selecting the plane again is a bit easier.
|
||||||
await page.mouse.move(center.x - 100, center.y + 50, { steps: 5 })
|
await page.mouse.click(center.x + 200, center.y + 100)
|
||||||
await page.mouse.click(center.x - 100, center.y + 50)
|
|
||||||
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
||||||
codeStr += "sketch002 = startSketchOn('XY')"
|
codeStr += "sketch002 = startSketchOn('XY')"
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
coord = await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
||||||
@ -782,21 +763,20 @@ test.describe('Sketch tests', () => {
|
|||||||
await u.updateCamPosition(camPos)
|
await u.updateCamPosition(camPos)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
|
||||||
await page.mouse.move(0, 0)
|
await page.mouse.move(0, 0)
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.move(center.x + 100, 200, { steps: 10 })
|
await page.mouse.move(700, 200, { steps: 10 })
|
||||||
await page.mouse.click(center.x + 100, 200, { delay: 200 })
|
await page.mouse.click(700, 200, { delay: 200 })
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`sketch001 = startSketchOn('-XZ')`
|
`sketch001 = startSketchOn('-XZ')`
|
||||||
)
|
)
|
||||||
|
|
||||||
let prevContent = await page.locator('.cm-content').innerText()
|
let prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
const pointA = [center.x + 100, 200]
|
const pointA = [700, 200]
|
||||||
const pointB = [center.x + 300, 200]
|
const pointB = [900, 200]
|
||||||
const pointC = [center.x + 300, 400]
|
const pointC = [900, 400]
|
||||||
|
|
||||||
// draw three lines
|
// draw three lines
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
@ -933,9 +913,7 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
await page.mouse.click(622, 355)
|
||||||
|
|
||||||
await page.mouse.click(center.x + 22, 355)
|
|
||||||
|
|
||||||
await page.waitForTimeout(800)
|
await page.waitForTimeout(800)
|
||||||
await page.getByText(`END')`).click()
|
await page.getByText(`END')`).click()
|
||||||
@ -1296,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) =>
|
const gltfFilename = filenames.filter((t: string) =>
|
||||||
t.includes('.gltf')
|
t.includes('.gltf')
|
||||||
)[0]
|
)[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}`
|
cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${parentPath}/${gltfFilename} ${imagePath}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@ -8,21 +8,6 @@ import {
|
|||||||
Locator,
|
Locator,
|
||||||
test,
|
test,
|
||||||
} from '@playwright/test'
|
} from '@playwright/test'
|
||||||
import {
|
|
||||||
OrthographicCamera,
|
|
||||||
Mesh,
|
|
||||||
Scene,
|
|
||||||
Raycaster,
|
|
||||||
PlaneGeometry,
|
|
||||||
MeshBasicMaterial,
|
|
||||||
DoubleSide,
|
|
||||||
Vector2,
|
|
||||||
Vector3,
|
|
||||||
} from 'three'
|
|
||||||
import {
|
|
||||||
RAYCASTABLE_PLANE,
|
|
||||||
INTERSECTION_PLANE_LAYER,
|
|
||||||
} from 'clientSideScene/constants'
|
|
||||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import fsSync from 'fs'
|
import fsSync from 'fs'
|
||||||
@ -272,138 +257,52 @@ export const circleMove = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rollingRound(n: number, digitsAfterDecimal: number) {
|
export const getMovementUtils = (opts: any) => {
|
||||||
const s = String(n).split('.')
|
// The way we truncate is kinda odd apparently, so we need this function
|
||||||
|
// "[k]itty[c]ad round"
|
||||||
|
const kcRound = (n: number) => Math.trunc(n * 100) / 100
|
||||||
|
|
||||||
// There are no decimals, just return the number.
|
// To translate between screen and engine ("[U]nit") coordinates
|
||||||
if (s.length === 1) return n
|
// NOTE: these pretty much can't be perfect because of screen scaling.
|
||||||
|
// Handle on a case-by-case.
|
||||||
|
const toU = (x: number, y: number) => [
|
||||||
|
kcRound(x * 0.0678),
|
||||||
|
kcRound(-y * 0.0678), // Y is inverted in our coordinate system
|
||||||
|
]
|
||||||
|
|
||||||
// Find the closest 9. We don't care about anything beyond that.
|
// Turn the array into a string with specific formatting
|
||||||
const nineIndex = s[1].indexOf('9')
|
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
|
||||||
|
|
||||||
const fractStr = nineIndex > 0 ? s[1].slice(0, nineIndex + 1) : s[1]
|
// Combine because used often
|
||||||
|
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
|
||||||
let fract = Number(fractStr) / 10 ** fractStr.length
|
|
||||||
|
|
||||||
for (let i = fractStr.length - 1; i >= 0; i -= 1) {
|
|
||||||
if (i === digitsAfterDecimal) break
|
|
||||||
fract = Math.round(fract * 10 ** i) / 10 ** i
|
|
||||||
}
|
|
||||||
|
|
||||||
return (Number(s[0]) + fract).toFixed(digitsAfterDecimal)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMovementUtils = async (opts: any) => {
|
|
||||||
const sceneInfra = await opts.page.evaluate(() => window.sceneInfra)
|
|
||||||
|
|
||||||
// Various data for raycasting into the scene to get our XY.
|
|
||||||
const hundredM = 100_0000
|
|
||||||
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
|
||||||
const planeMaterial = new MeshBasicMaterial({
|
|
||||||
color: 0xff0000,
|
|
||||||
side: DoubleSide,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.5,
|
|
||||||
})
|
|
||||||
const scene = new Scene()
|
|
||||||
const intersectionPlane = new Mesh(planeGeometry, planeMaterial)
|
|
||||||
intersectionPlane.userData = { type: RAYCASTABLE_PLANE }
|
|
||||||
intersectionPlane.name = RAYCASTABLE_PLANE
|
|
||||||
intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
|
|
||||||
scene.add(intersectionPlane)
|
|
||||||
const planeRaycaster = new Raycaster()
|
|
||||||
planeRaycaster.far = Infinity
|
|
||||||
planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
|
||||||
|
|
||||||
const kcRound = (n: number) => Math.round(n * 100) / 100
|
|
||||||
|
|
||||||
// Make it easier to click around from center ("click [from] zero zero")
|
// Make it easier to click around from center ("click [from] zero zero")
|
||||||
const click00 = (x: number, y: number) =>
|
const click00 = (x: number, y: number) =>
|
||||||
opts.page.mouse.click(x, y, { delay: 100 })
|
opts.page.mouse.click(opts.center.x + x, opts.center.y + y, { delay: 100 })
|
||||||
|
|
||||||
// Relative clicker, must keep state
|
// Relative clicker, must keep state
|
||||||
let last = { x: 0, y: 0 }
|
let last = { x: 0, y: 0 }
|
||||||
let lastScreenSpace = { x: 0, y: 0 }
|
|
||||||
|
|
||||||
const click00r = async (x?: number, y?: number) => {
|
const click00r = async (x?: number, y?: number) => {
|
||||||
// reset relative coordinates when anything is undefined
|
// reset relative coordinates when anything is undefined
|
||||||
if (x === undefined || y === undefined) {
|
if (x === undefined || y === undefined) {
|
||||||
last = { x: 0, y: 0 }
|
last.x = 0
|
||||||
lastScreenSpace = { x: 0, y: 0 }
|
last.y = 0
|
||||||
return {
|
return
|
||||||
nextXY: [0, 0],
|
|
||||||
kcl: `[0, 0]`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const absX = opts.center.x + x
|
await circleMove(
|
||||||
const absY = opts.center.y + y
|
opts.page,
|
||||||
|
opts.center.x + last.x + x,
|
||||||
const nextX = last.x + x
|
opts.center.y + last.y + y,
|
||||||
const nextY = last.y + y
|
10,
|
||||||
|
10
|
||||||
const targetX = opts.center.x + nextX
|
|
||||||
const targetY = opts.center.y + -nextY
|
|
||||||
|
|
||||||
// Use the current camera specification
|
|
||||||
const camera = await opts.page.evaluate(() => {
|
|
||||||
window.sceneInfra.camControls.onCameraChange(true)
|
|
||||||
return window.sceneInfra.camControls.camera
|
|
||||||
})
|
|
||||||
|
|
||||||
const windowWH = await opts.page.evaluate(() => ({
|
|
||||||
w: window.innerWidth,
|
|
||||||
h: window.innerHeight,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// I didn't write this math, it's copied from sceneInfra.ts, and I understand
|
|
||||||
// it's just normalizing the point, but why *-2 ± 1 I have no idea.
|
|
||||||
const mouseVector = new Vector2(
|
|
||||||
(targetX / windowWH.w) * 2 - 1,
|
|
||||||
-(targetY / windowWH.h) * 2 + 1
|
|
||||||
)
|
)
|
||||||
planeRaycaster.setFromCamera(mouseVector, camera)
|
await click00(last.x + x, last.y + y)
|
||||||
const intersections = planeRaycaster.intersectObjects(scene.children, true)
|
|
||||||
|
|
||||||
const planePosition = intersections[0].object.position
|
|
||||||
const inversePlaneQuaternion = intersections[0].object.quaternion
|
|
||||||
.clone()
|
|
||||||
.invert()
|
|
||||||
let transformedPoint = intersections[0].point.clone()
|
|
||||||
if (transformedPoint) {
|
|
||||||
transformedPoint.applyQuaternion(inversePlaneQuaternion)
|
|
||||||
}
|
|
||||||
const twoD = new Vector2(
|
|
||||||
// I think the intersection plane doesn't get scale when nearly everything else does, maybe that should change
|
|
||||||
transformedPoint.x / sceneInfra._baseUnitMultiplier,
|
|
||||||
transformedPoint.y / sceneInfra._baseUnitMultiplier
|
|
||||||
) // z should be 0
|
|
||||||
const planePositionCorrected = new Vector3(
|
|
||||||
...planePosition
|
|
||||||
).applyQuaternion(inversePlaneQuaternion)
|
|
||||||
twoD.sub(new Vector2(...planePositionCorrected))
|
|
||||||
|
|
||||||
await circleMove(opts.page, targetX, targetY, 10, 10)
|
|
||||||
await click00(targetX, targetY)
|
|
||||||
|
|
||||||
last.x += x
|
last.x += x
|
||||||
last.y += y
|
last.y += y
|
||||||
|
|
||||||
const relativeScreenSpace = {
|
// Returns the new absolute coordinate if you need it.
|
||||||
x: twoD.x - lastScreenSpace.x,
|
return [last.x, last.y]
|
||||||
y: -(twoD.y - lastScreenSpace.y),
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScreenSpace.x = kcRound(twoD.x)
|
|
||||||
lastScreenSpace.y = kcRound(twoD.y)
|
|
||||||
|
|
||||||
// Returns the new absolute coordinate and the screen space coordinate if you need it.
|
|
||||||
return {
|
|
||||||
nextXY: [last.x, last.y],
|
|
||||||
kcl: `[${kcRound(relativeScreenSpace.x)}, ${-kcRound(
|
|
||||||
relativeScreenSpace.y
|
|
||||||
)}]`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { toSU, toU, click00r }
|
return { toSU, toU, click00r }
|
||||||
@ -457,30 +356,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||||
|
|
||||||
const util = {
|
const util = {
|
||||||
async getModelViewAreaSize() {
|
|
||||||
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
|
|
||||||
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
|
|
||||||
|
|
||||||
const sidebar = page.getByTestId('modeling-sidebar')
|
|
||||||
const bb = await sidebar.boundingBox()
|
|
||||||
return {
|
|
||||||
w: windowInnerWidth - (bb?.width ?? 0),
|
|
||||||
h: windowInnerHeight - (bb?.height ?? 0),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getCenterOfModelViewArea() {
|
|
||||||
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
|
|
||||||
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
|
|
||||||
|
|
||||||
const sidebar = page.getByTestId('modeling-sidebar')
|
|
||||||
const bb = await sidebar.boundingBox()
|
|
||||||
const goRightPx = (bb?.width ?? 0) / 2
|
|
||||||
const borderWidthsCombined = 2
|
|
||||||
return {
|
|
||||||
x: Math.round(windowInnerWidth / 2 + goRightPx) - borderWidthsCombined,
|
|
||||||
y: Math.round(windowInnerHeight / 2),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
||||||
waitForPageLoad: () => waitForPageLoad(page),
|
waitForPageLoad: () => waitForPageLoad(page),
|
||||||
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
||||||
|
@ -43,12 +43,10 @@ test.describe('Testing constraints', () => {
|
|||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
const startXPx = 500
|
||||||
|
|
||||||
const startXPx = center.x - 100
|
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await page.mouse.click(center.x + 234, 244)
|
await page.mouse.click(834, 244)
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
@ -743,19 +743,18 @@ extrude001 = extrude(5, sketch001)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await u.waitForAuthSkipAppStart()
|
|
||||||
|
|
||||||
// Selectors and constants
|
// Selectors and constants
|
||||||
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
const lineToolButton = page.getByTestId('line')
|
const lineToolButton = page.getByTestId('line')
|
||||||
const segmentOverlays = page.getByTestId('segment-overlay')
|
const segmentOverlays = page.getByTestId('segment-overlay')
|
||||||
const sketchOriginLocation = await u.getCenterOfModelViewArea()
|
const sketchOriginLocation = { x: 600, y: 250 }
|
||||||
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
||||||
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
||||||
|
|
||||||
await test.step(`Get into sketch mode`, async () => {
|
await test.step(`Get into sketch mode`, async () => {
|
||||||
await page.mouse.click(sketchOriginLocation.x, sketchOriginLocation.y)
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
await expect(editSketchButton).toBeVisible()
|
await expect(editSketchButton).toBeVisible()
|
||||||
await editSketchButton.click()
|
await editSketchButton.click()
|
||||||
|
|
||||||
@ -766,18 +765,12 @@ extrude001 = extrude(5, sketch001)
|
|||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
|
|
||||||
|
|
||||||
// Our lines are translucent (surprise!), so we need to get on portion
|
|
||||||
// of the line that is only on the background, and not on top of something
|
|
||||||
// like the axis lines.
|
|
||||||
line1.x -= 1
|
|
||||||
line1.y -= 1
|
|
||||||
|
|
||||||
await test.step(`Check the sketch line color before`, async () => {
|
await test.step(`Check the sketch line color before`, async () => {
|
||||||
await expect
|
await expect
|
||||||
.poll(() => u.getGreatestPixDiff(line1, darkThemeSegmentColor))
|
.poll(() =>
|
||||||
.toBeLessThanOrEqual(34)
|
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
||||||
|
)
|
||||||
|
.toBeLessThan(15)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Change theme to light using command palette`, async () => {
|
await test.step(`Change theme to light using command palette`, async () => {
|
||||||
@ -792,8 +785,10 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await test.step(`Check the sketch line color after`, async () => {
|
await test.step(`Check the sketch line color after`, async () => {
|
||||||
await expect
|
await expect
|
||||||
.poll(() => u.getGreatestPixDiff(line1, lightThemeSegmentColor))
|
.poll(() =>
|
||||||
.toBeLessThanOrEqual(34)
|
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
||||||
|
)
|
||||||
|
.toBeLessThan(15)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -503,16 +503,14 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
const center = await u.getCenterOfModelViewArea()
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
// This basically waits for sketch mode to be ready.
|
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
async () => page.mouse.click(center.x, 180),
|
() => page.mouse.click(625, 165),
|
||||||
'default_camera_get_settings',
|
'default_camera_get_settings',
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
await page.waitForTimeout(150)
|
||||||
await page.waitForTimeout(300)
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
const firstClickPosition = [612, 238]
|
const firstClickPosition = [612, 238]
|
||||||
const secondClickPosition = [661, 242]
|
const secondClickPosition = [661, 242]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zoo-modeling-app",
|
"name": "zoo-modeling-app",
|
||||||
"version": "0.26.5",
|
"version": "0.27.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"author": {
|
"author": {
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "6.3.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"html2canvas-pro": "^1.5.8",
|
"html2canvas-pro": "^1.5.8",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
|
46
src/App.tsx
@ -1,14 +1,15 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
|
import { Stream } from './components/Stream'
|
||||||
import { AppHeader } from './components/AppHeader'
|
import { AppHeader } from './components/AppHeader'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||||
import { type IndexLoaderData } from 'lib/types'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
import { codeManager, engineCommandManager, sceneInfra } from 'lib/singletons'
|
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
@ -21,8 +22,6 @@ import Gizmo from 'components/Gizmo'
|
|||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||||
import EngineStreamContext from 'hooks/useEngineStreamContext'
|
|
||||||
import { EngineStream } from 'components/EngineStream'
|
|
||||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||||
maybeWriteToDisk()
|
maybeWriteToDisk()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
@ -38,13 +37,6 @@ export function App() {
|
|||||||
// the coredump.
|
// the coredump.
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// Stream related refs and data
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
||||||
const modelingSidebarRef = useRef<HTMLUListElement>(null)
|
|
||||||
let [searchParams] = useSearchParams()
|
|
||||||
const pool = searchParams.get('pool')
|
|
||||||
|
|
||||||
const projectName = project?.name || null
|
const projectName = project?.name || null
|
||||||
const projectPath = project?.path || null
|
const projectPath = project?.path || null
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -65,10 +57,6 @@ export function App() {
|
|||||||
app: { onboardingStatus },
|
app: { onboardingStatus },
|
||||||
} = settings.context
|
} = settings.context
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
sceneInfra.camControls.modelingSidebarRef = modelingSidebarRef
|
|
||||||
}, [modelingSidebarRef.current])
|
|
||||||
|
|
||||||
useHotkeys('backspace', (e) => {
|
useHotkeys('backspace', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
})
|
})
|
||||||
@ -96,26 +84,14 @@ export function App() {
|
|||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<ModelingSidebar paneOpacity={paneOpacity} ref={modelingSidebarRef} />
|
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||||
<EngineStreamContext.Provider
|
<Stream />
|
||||||
options={{
|
{/* <CamToggle /> */}
|
||||||
input: {
|
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||||
videoRef,
|
<UnitsMenu />
|
||||||
canvasRef,
|
<Gizmo />
|
||||||
mediaStream: null,
|
<CameraProjectionToggle />
|
||||||
authToken: auth?.context?.token ?? null,
|
</LowerRightControls>
|
||||||
pool,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EngineStream />
|
|
||||||
{/* <CamToggle /> */}
|
|
||||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
|
||||||
<UnitsMenu />
|
|
||||||
<Gizmo />
|
|
||||||
<CameraProjectionToggle />
|
|
||||||
</LowerRightControls>
|
|
||||||
</EngineStreamContext.Provider>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import {
|
|||||||
} from 'lib/toolbar'
|
} from 'lib/toolbar'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -49,7 +48,7 @@ export function Toolbar({
|
|||||||
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
||||||
|
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const { overallState, immediateState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
|
|
||||||
@ -57,7 +56,6 @@ export function Toolbar({
|
|||||||
(overallState !== NetworkHealthState.Ok &&
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
overallState !== NetworkHealthState.Weak) ||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
isExecuting ||
|
isExecuting ||
|
||||||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished ||
|
|
||||||
!isStreamReady
|
!isStreamReady
|
||||||
|
|
||||||
const currentMode =
|
const currentMode =
|
||||||
@ -143,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 */}
|
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
|
||||||
{currentModeItems.map((maybeIconConfig, i) => {
|
{currentModeItems.map((maybeIconConfig, i) => {
|
||||||
|
// Vertical Line Break
|
||||||
if (maybeIconConfig === 'break') {
|
if (maybeIconConfig === 'break') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -151,6 +150,7 @@ export function Toolbar({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (Array.isArray(maybeIconConfig)) {
|
} else if (Array.isArray(maybeIconConfig)) {
|
||||||
|
// A button with a dropdown
|
||||||
return (
|
return (
|
||||||
<ActionButtonDropdown
|
<ActionButtonDropdown
|
||||||
Element="button"
|
Element="button"
|
||||||
@ -217,6 +217,7 @@ export function Toolbar({
|
|||||||
}
|
}
|
||||||
const itemConfig = maybeIconConfig
|
const itemConfig = maybeIconConfig
|
||||||
|
|
||||||
|
// A single button
|
||||||
return (
|
return (
|
||||||
<div className="relative" key={itemConfig.id}>
|
<div className="relative" key={itemConfig.id}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { Models } from '@kittycad/lib'
|
|
||||||
import { MutableRefObject } from 'react'
|
|
||||||
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
||||||
import {
|
import {
|
||||||
Euler,
|
Euler,
|
||||||
@ -89,9 +87,6 @@ class CameraRateLimiter {
|
|||||||
|
|
||||||
export class CameraControls {
|
export class CameraControls {
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
modelingSidebarRef: MutableRefObject<HTMLUListElement | null> = {
|
|
||||||
current: null,
|
|
||||||
}
|
|
||||||
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
||||||
camera: PerspectiveCamera | OrthographicCamera
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
target: Vector3
|
target: Vector3
|
||||||
@ -100,13 +95,6 @@ export class CameraControls {
|
|||||||
wasDragging: boolean
|
wasDragging: boolean
|
||||||
mouseDownPosition: Vector2
|
mouseDownPosition: Vector2
|
||||||
mouseNewPosition: Vector2
|
mouseNewPosition: Vector2
|
||||||
cameraDragStartXY = new Vector2()
|
|
||||||
old:
|
|
||||||
| {
|
|
||||||
camera: PerspectiveCamera | OrthographicCamera
|
|
||||||
target: Vector3
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
rotationSpeed = 0.3
|
rotationSpeed = 0.3
|
||||||
enableRotate = true
|
enableRotate = true
|
||||||
enablePan = true
|
enablePan = true
|
||||||
@ -473,7 +461,6 @@ export class CameraControls {
|
|||||||
if (this.syncDirection === 'engineToClient') {
|
if (this.syncDirection === 'engineToClient') {
|
||||||
const interaction = this.getInteractionType(event)
|
const interaction = this.getInteractionType(event)
|
||||||
if (interaction === 'none') return
|
if (interaction === 'none') return
|
||||||
|
|
||||||
void this.engineCommandManager.sendSceneCommand({
|
void this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -922,123 +909,18 @@ export class CameraControls {
|
|||||||
up: { x: 0, y: 0, z: 1 },
|
up: { x: 0, y: 0, z: 1 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
await this.engineCommandManager.sendSceneCommand({
|
||||||
await this.centerModelRelativeToPanes({
|
|
||||||
zoomToFit: true,
|
|
||||||
resetLastPaneWidth: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.cameraDragStartXY = new Vector2()
|
|
||||||
this.cameraDragStartXY.x = 0
|
|
||||||
this.cameraDragStartXY.y = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async restoreCameraPosition(): Promise<void> {
|
|
||||||
if (!this.old) return
|
|
||||||
|
|
||||||
this.camera = this.old.camera.clone()
|
|
||||||
this.target = this.old.target.clone()
|
|
||||||
|
|
||||||
void this.engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'default_camera_look_at',
|
type: 'zoom_to_fit',
|
||||||
...convertThreeCamValuesToEngineCam({
|
object_ids: [], // leave empty to zoom to all objects
|
||||||
isPerspective: true,
|
padding: 0.2, // padding around the objects
|
||||||
position: this.camera.position,
|
animated: false, // don't animate the zoom for now
|
||||||
quaternion: this.camera.quaternion,
|
|
||||||
zoom: this.camera.zoom,
|
|
||||||
target: this.target,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private lastFramePaneWidth: number = 0
|
|
||||||
|
|
||||||
async centerModelRelativeToPanes(args?: {
|
|
||||||
zoomObjectId?: string
|
|
||||||
zoomToFit?: boolean
|
|
||||||
resetLastPaneWidth?: boolean
|
|
||||||
}): Promise<void> {
|
|
||||||
const panes = this.modelingSidebarRef?.current
|
|
||||||
if (!panes) return
|
|
||||||
|
|
||||||
const panesWidth = panes.offsetWidth + panes.offsetLeft
|
|
||||||
|
|
||||||
if (args?.resetLastPaneWidth) {
|
|
||||||
this.lastFramePaneWidth = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const goPx =
|
|
||||||
(panesWidth - this.lastFramePaneWidth) / 2 / window.devicePixelRatio
|
|
||||||
this.lastFramePaneWidth = panesWidth
|
|
||||||
|
|
||||||
// Originally I had tried to use the default_camera_look_at endpoint and
|
|
||||||
// some quaternion math to move the camera right, but it ended up being
|
|
||||||
// overly complicated, and I think the threejs scene also doesn't have the
|
|
||||||
// camera coordinates after a zoom-to-fit... So this is much easier, and
|
|
||||||
// maps better to screen coordinates.
|
|
||||||
|
|
||||||
const requests: Models['ModelingCmdReq_type'][] = [
|
|
||||||
{
|
|
||||||
cmd: {
|
|
||||||
type: 'camera_drag_start',
|
|
||||||
interaction: 'pan',
|
|
||||||
window: { x: goPx < 0 ? -goPx : 0, y: 0 },
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: {
|
|
||||||
type: 'camera_drag_move',
|
|
||||||
interaction: 'pan',
|
|
||||||
window: {
|
|
||||||
x: goPx < 0 ? 0 : goPx,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
if (args?.zoomToFit) {
|
|
||||||
requests.unshift({
|
|
||||||
cmd: {
|
|
||||||
type: 'zoom_to_fit',
|
|
||||||
object_ids: args?.zoomObjectId ? [args?.zoomObjectId] : [], // leave empty to zoom to all objects
|
|
||||||
padding: 0.2, // padding around the objects
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.engineCommandManager
|
|
||||||
.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_batch_req',
|
|
||||||
batch_id: uuidv4(),
|
|
||||||
responses: true,
|
|
||||||
requests,
|
|
||||||
})
|
|
||||||
// engineCommandManager can't subscribe to batch responses so we'll send
|
|
||||||
// this one off by its lonesome after.
|
|
||||||
.then(() =>
|
|
||||||
this.engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'camera_drag_end',
|
|
||||||
interaction: 'pan',
|
|
||||||
window: {
|
|
||||||
x: goPx < 0 ? 0 : goPx,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async tweenCameraToQuaternion(
|
async tweenCameraToQuaternion(
|
||||||
targetQuaternion: Quaternion,
|
targetQuaternion: Quaternion,
|
||||||
targetPosition = new Vector3(),
|
targetPosition = new Vector3(),
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
import {
|
import { useRef, useEffect, useState, useMemo, Fragment } from 'react'
|
||||||
CSSProperties,
|
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
useMemo,
|
|
||||||
Fragment,
|
|
||||||
} from 'react'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
@ -249,13 +242,6 @@ const Overlay = ({
|
|||||||
state.matches({ Sketch: 'Rectangle tool' })
|
state.matches({ Sketch: 'Rectangle tool' })
|
||||||
)
|
)
|
||||||
|
|
||||||
// Line labels will cover the constraints overlay if this is not used.
|
|
||||||
// For each line label, ThreeJS increments each CSS2DObject z-index as they
|
|
||||||
// are added. I have looked into overriding renderOrder and depthTest and
|
|
||||||
// while renderOrder is set, ThreeJS still sets z-index on these 2D objects.
|
|
||||||
// It is easier to set this to a large number, such as a billion.
|
|
||||||
const zIndex = 1000000000
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`absolute w-0 h-0`}>
|
<div className={`absolute w-0 h-0`}>
|
||||||
<div
|
<div
|
||||||
@ -266,7 +252,6 @@ const Overlay = ({
|
|||||||
data-overlay-angle={overlay.angle}
|
data-overlay-angle={overlay.angle}
|
||||||
className="pointer-events-auto absolute w-0 h-0"
|
className="pointer-events-auto absolute w-0 h-0"
|
||||||
style={{
|
style={{
|
||||||
zIndex,
|
|
||||||
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
@ -275,7 +260,6 @@ const Overlay = ({
|
|||||||
data-overlay-toolbar-index={overlayIndex}
|
data-overlay-toolbar-index={overlayIndex}
|
||||||
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
||||||
style={{
|
style={{
|
||||||
zIndex,
|
|
||||||
transform: `translate3d(calc(${
|
transform: `translate3d(calc(${
|
||||||
overlay.windowCoords[0] + xOffset
|
overlay.windowCoords[0] + xOffset
|
||||||
}px + ${xAlignment}), calc(${
|
}px + ${xAlignment}), calc(${
|
||||||
@ -317,7 +301,6 @@ const Overlay = ({
|
|||||||
*/}
|
*/}
|
||||||
{callExpression?.callee?.name !== 'circle' && (
|
{callExpression?.callee?.name !== 'circle' && (
|
||||||
<SegmentMenu
|
<SegmentMenu
|
||||||
style={{ zIndex }}
|
|
||||||
verticalPosition={
|
verticalPosition={
|
||||||
overlay.windowCoords[1] > window.innerHeight / 2
|
overlay.windowCoords[1] > window.innerHeight / 2
|
||||||
? 'top'
|
? 'top'
|
||||||
@ -459,17 +442,15 @@ const SegmentMenu = ({
|
|||||||
verticalPosition,
|
verticalPosition,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
stdLibFnName,
|
stdLibFnName,
|
||||||
style,
|
|
||||||
}: {
|
}: {
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
stdLibFnName: string
|
stdLibFnName: string
|
||||||
style?: CSSProperties
|
|
||||||
}) => {
|
}) => {
|
||||||
const { send } = useModelingContext()
|
const { send } = useModelingContext()
|
||||||
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
||||||
return (
|
return (
|
||||||
<Popover style={style} className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
// 63.5 is definitely a bit of a magic number, play with it until it looked right
|
|
||||||
// if it were 64, that would feel like it's something in the engine where a random
|
|
||||||
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
|
|
||||||
export const ZOOM_MAGIC_NUMBER = 63.5
|
|
||||||
|
|
||||||
export const INTERSECTION_PLANE_LAYER = 1
|
|
||||||
export const SKETCH_LAYER = 2
|
|
||||||
|
|
||||||
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
|
||||||
|
|
||||||
// redundant types so that it can be changed temporarily but CI will catch the wrong type
|
|
||||||
export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
|
|
||||||
export const DEBUG_SHOW_BOTH_SCENES: false = false
|
|
||||||
|
|
||||||
export const X_AXIS = 'xAxis'
|
|
||||||
export const Y_AXIS = 'yAxis'
|
|
||||||
export const AXIS_GROUP = 'axisGroup'
|
|
||||||
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
|
||||||
export const ARROWHEAD = 'arrowhead'
|
|
||||||
export const SEGMENT_LENGTH_LABEL = 'segment-length-label'
|
|
||||||
export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text'
|
|
||||||
export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30
|
|
@ -2,7 +2,10 @@ import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
|||||||
import {
|
import {
|
||||||
GridHelper,
|
GridHelper,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
|
OrthographicCamera,
|
||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
|
Group,
|
||||||
|
Mesh,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
@ -25,9 +28,15 @@ export function createGridHelper({
|
|||||||
gridHelper.rotation.x = Math.PI / 2
|
gridHelper.rotation.x = Math.PI / 2
|
||||||
return gridHelper
|
return gridHelper
|
||||||
}
|
}
|
||||||
|
const fudgeFactor = 72.66985970437086
|
||||||
|
|
||||||
// Re-export scale.ts
|
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
||||||
export * from './scale'
|
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
|
||||||
|
|
||||||
|
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
||||||
|
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
||||||
|
4000 /
|
||||||
|
window.innerHeight
|
||||||
|
|
||||||
export function isQuaternionVertical(q: Quaternion) {
|
export function isQuaternionVertical(q: Quaternion) {
|
||||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { OrthographicCamera, PerspectiveCamera, Group, Mesh } from 'three'
|
|
||||||
|
|
||||||
export const fudgeFactor = 72.66985970437086
|
|
||||||
|
|
||||||
export const orthoScale = (
|
|
||||||
cam: OrthographicCamera | PerspectiveCamera,
|
|
||||||
innerHeight?: number
|
|
||||||
) => (0.55 * fudgeFactor) / cam.zoom / (innerHeight ?? window.innerHeight)
|
|
||||||
|
|
||||||
export const perspScale = (
|
|
||||||
cam: PerspectiveCamera,
|
|
||||||
group: Group | Mesh,
|
|
||||||
innerHeight?: number
|
|
||||||
) =>
|
|
||||||
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
|
||||||
4000 /
|
|
||||||
(innerHeight ?? window.innerHeight)
|
|
@ -47,6 +47,7 @@ import {
|
|||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
sketchFromKclValue,
|
sketchFromKclValue,
|
||||||
|
sketchFromKclValueOptional,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
import {
|
import {
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
@ -89,9 +90,10 @@ import { EngineCommandManager } from 'lang/std/engineConnection'
|
|||||||
import {
|
import {
|
||||||
getRectangleCallExpressions,
|
getRectangleCallExpressions,
|
||||||
updateRectangleSketch,
|
updateRectangleSketch,
|
||||||
|
updateCenterRectangleSketch,
|
||||||
} from 'lib/rectangleTool'
|
} from 'lib/rectangleTool'
|
||||||
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
|
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 { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||||
import { SegmentInputs } from 'lang/std/stdTypes'
|
import { SegmentInputs } from 'lang/std/stdTypes'
|
||||||
@ -1043,6 +1045,174 @@ export class SceneEntities {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
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') {
|
||||||
|
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
|
||||||
|
_ast = _recastAst
|
||||||
|
|
||||||
|
// Update the primary AST and unequip the rectangle tool
|
||||||
|
await kclManager.executeAstMock(_ast)
|
||||||
|
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,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
setupDraftCircle = async (
|
setupDraftCircle = async (
|
||||||
sketchPathToNode: PathToNode,
|
sketchPathToNode: PathToNode,
|
||||||
forward: [number, number, number],
|
forward: [number, number, number],
|
||||||
@ -1528,10 +1698,13 @@ export class SceneEntities {
|
|||||||
this.sceneProgramMemory = programMemory
|
this.sceneProgramMemory = programMemory
|
||||||
|
|
||||||
const maybeSketch = programMemory.get(variableDeclarationName)
|
const maybeSketch = programMemory.get(variableDeclarationName)
|
||||||
let sketch = undefined
|
let sketch: Sketch | undefined
|
||||||
const sg = sketchFromKclValue(maybeSketch, variableDeclarationName)
|
const sk = sketchFromKclValueOptional(
|
||||||
if (!err(sg)) {
|
maybeSketch,
|
||||||
sketch = sg
|
variableDeclarationName
|
||||||
|
)
|
||||||
|
if (!(sk instanceof Reason)) {
|
||||||
|
sketch = sk
|
||||||
} else if ((maybeSketch as Solid).sketch) {
|
} else if ((maybeSketch as Solid).sketch) {
|
||||||
sketch = (maybeSketch as Solid).sketch
|
sketch = (maybeSketch as Solid).sketch
|
||||||
}
|
}
|
||||||
|
@ -291,14 +291,14 @@ export class SceneInfra {
|
|||||||
engineCommandManager
|
engineCommandManager
|
||||||
)
|
)
|
||||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||||
this.camControls.camera.layers.enable(constants.SKETCH_LAYER)
|
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
||||||
if (constants.DEBUG_SHOW_INTERSECTION_PLANE)
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
this.camControls.camera.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// RAYCASTERS
|
// RAYCASTERS
|
||||||
this.raycaster.layers.enable(constants.SKETCH_LAYER)
|
this.raycaster.layers.enable(SKETCH_LAYER)
|
||||||
this.raycaster.layers.disable(0)
|
this.raycaster.layers.disable(0)
|
||||||
this.planeRaycaster.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// GRID
|
// GRID
|
||||||
const size = 100
|
const size = 100
|
||||||
@ -333,7 +333,7 @@ export class SceneInfra {
|
|||||||
this.camControls.target
|
this.camControls.target
|
||||||
)
|
)
|
||||||
const axisGroup = this.scene
|
const axisGroup = this.scene
|
||||||
.getObjectByName(constants.AXIS_GROUP)
|
.getObjectByName(AXIS_GROUP)
|
||||||
?.getObjectByName('gridHelper')
|
?.getObjectByName('gridHelper')
|
||||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||||
}
|
}
|
||||||
@ -344,6 +344,7 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
animate = () => {
|
animate = () => {
|
||||||
|
requestAnimationFrame(this.animate)
|
||||||
TWEEN.update() // This will update all tweens during the animation loop
|
TWEEN.update() // This will update all tweens during the animation loop
|
||||||
if (!this.isFovAnimationInProgress) {
|
if (!this.isFovAnimationInProgress) {
|
||||||
// console.log('animation frame', this.cameraControls.camera)
|
// console.log('animation frame', this.cameraControls.camera)
|
||||||
@ -351,7 +352,6 @@ export class SceneInfra {
|
|||||||
this.renderer.render(this.scene, this.camControls.camera)
|
this.renderer.render(this.scene, this.camControls.camera)
|
||||||
this.labelRenderer.render(this.scene, this.camControls.camera)
|
this.labelRenderer.render(this.scene, this.camControls.camera)
|
||||||
}
|
}
|
||||||
requestAnimationFrame(this.animate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose = () => {
|
dispose = () => {
|
||||||
@ -655,11 +655,11 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
||||||
const axisGroup = this.scene.children.find(
|
const axisGroup = this.scene.children.find(
|
||||||
({ userData }) => userData?.type === constants.AXIS_GROUP
|
({ userData }) => userData?.type === AXIS_GROUP
|
||||||
)
|
)
|
||||||
const axisMap: { [key: string]: Axis } = {
|
const axisMap: { [key: string]: Axis } = {
|
||||||
[constants.X_AXIS]: 'x-axis',
|
[X_AXIS]: 'x-axis',
|
||||||
[constants.Y_AXIS]: 'y-axis',
|
[Y_AXIS]: 'y-axis',
|
||||||
}
|
}
|
||||||
axisGroup?.children.forEach((_mesh) => {
|
axisGroup?.children.forEach((_mesh) => {
|
||||||
const mesh = _mesh as Mesh
|
const mesh = _mesh as Mesh
|
||||||
|
@ -300,7 +300,7 @@ class StraightSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible: true,
|
isHandlesVisible,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
})
|
})
|
||||||
@ -476,7 +476,7 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible: true,
|
isHandlesVisible,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
angle,
|
angle,
|
||||||
@ -542,7 +542,7 @@ class CircleSegment implements SegmentUtils {
|
|||||||
}
|
}
|
||||||
group.name = CIRCLE_SEGMENT
|
group.name = CIRCLE_SEGMENT
|
||||||
|
|
||||||
group.add(arcMesh, arrowGroup, circleCenterGroup)
|
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
|
||||||
const updateOverlaysCallback = this.update({
|
const updateOverlaysCallback = this.update({
|
||||||
prevSegment,
|
prevSegment,
|
||||||
input,
|
input,
|
||||||
@ -677,7 +677,7 @@ class CircleSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible: true,
|
isHandlesVisible,
|
||||||
from: from,
|
from: from,
|
||||||
to: [center[0], center[1]],
|
to: [center[0], center[1]],
|
||||||
angle: Math.PI / 4,
|
angle: Math.PI / 4,
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
|
||||||
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
|
||||||
import CommandBarArgument from './CommandBarArgument'
|
import CommandBarArgument from './CommandBarArgument'
|
||||||
import CommandComboBox from '../CommandComboBox'
|
import CommandComboBox from '../CommandComboBox'
|
||||||
import CommandBarReview from './CommandBarReview'
|
import CommandBarReview from './CommandBarReview'
|
||||||
@ -16,7 +14,6 @@ export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
|||||||
export const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const { immediateState } = useNetworkContext()
|
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -28,14 +25,6 @@ export const CommandBar = () => {
|
|||||||
commandBarSend({ type: 'Close' })
|
commandBarSend({ type: 'Close' })
|
||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
|
||||||
) {
|
|
||||||
commandBarSend({ type: 'Close' })
|
|
||||||
}
|
|
||||||
}, [immediateState])
|
|
||||||
|
|
||||||
// Hook up keyboard shortcuts
|
// Hook up keyboard shortcuts
|
||||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||||
if (commandBarState.context.commands.length === 0) return
|
if (commandBarState.context.commands.length === 0) return
|
||||||
|
@ -2,20 +2,13 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||||
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
|
||||||
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
|
||||||
|
|
||||||
export function CommandBarOpenButton() {
|
export function CommandBarOpenButton() {
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { immediateState } = useNetworkContext()
|
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
const isDisabled =
|
|
||||||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
disabled={isDisabled}
|
|
||||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||||
onClick={() => commandBarSend({ type: 'Open' })}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
data-testid="command-bar-open-button"
|
data-testid="command-bar-open-button"
|
||||||
|
@ -1,293 +0,0 @@
|
|||||||
import { MouseEventHandler, useEffect, useRef } from 'react'
|
|
||||||
import { useAppState } from 'AppState'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
|
||||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
|
||||||
import { btnName } from 'lib/cameraControls'
|
|
||||||
import { trap } from 'lib/trap'
|
|
||||||
import { sendSelectEventToEngine } from 'lib/selections'
|
|
||||||
import { kclManager, engineCommandManager } from 'lib/singletons'
|
|
||||||
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
|
||||||
import { useRouteLoaderData } from 'react-router-dom'
|
|
||||||
import { PATHS } from 'lib/paths'
|
|
||||||
import { IndexLoaderData } from 'lib/types'
|
|
||||||
import useEngineStreamContext, {
|
|
||||||
EngineStreamState,
|
|
||||||
EngineStreamTransition,
|
|
||||||
} from 'hooks/useEngineStreamContext'
|
|
||||||
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
|
||||||
|
|
||||||
export const EngineStream = () => {
|
|
||||||
const { setAppState } = useAppState()
|
|
||||||
|
|
||||||
const { overallState } = useNetworkContext()
|
|
||||||
const { settings } = useSettingsAuthContext()
|
|
||||||
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
|
||||||
const last = useRef<number>(Date.now())
|
|
||||||
|
|
||||||
const settingsEngine = {
|
|
||||||
theme: settings.context.app.theme.current,
|
|
||||||
enableSSAO: settings.context.app.enableSSAO.current,
|
|
||||||
highlightEdges: settings.context.modeling.highlightEdges.current,
|
|
||||||
showScaleGrid: settings.context.modeling.showScaleGrid.current,
|
|
||||||
cameraProjection: settings.context.modeling.cameraProjection.current,
|
|
||||||
}
|
|
||||||
|
|
||||||
const { state: modelingMachineState, send: modelingMachineActorSend } =
|
|
||||||
useModelingContext()
|
|
||||||
|
|
||||||
const engineStreamActor = useEngineStreamContext.useActorRef()
|
|
||||||
const engineStreamState = engineStreamActor.getSnapshot()
|
|
||||||
|
|
||||||
const streamIdleMode = settings.context.app.streamIdleMode.current
|
|
||||||
|
|
||||||
const configure = () => {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.StartOrReconfigureEngine,
|
|
||||||
modelingMachineActorSend,
|
|
||||||
settings: settingsEngine,
|
|
||||||
setAppState,
|
|
||||||
|
|
||||||
// It's possible a reconnect happens as we drag the window :')
|
|
||||||
onMediaStream(mediaStream: MediaStream) {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.SetMediaStream,
|
|
||||||
mediaStream,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const play = () => {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.Play,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
engineCommandManager.addEventListener(
|
|
||||||
EngineCommandManagerEvents.SceneReady,
|
|
||||||
play
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
engineCommandManager.removeEventListener(
|
|
||||||
EngineCommandManagerEvents.SceneReady,
|
|
||||||
play
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const video = engineStreamState.context.videoRef?.current
|
|
||||||
if (!video) return
|
|
||||||
const canvas = engineStreamState.context.canvasRef?.current
|
|
||||||
if (!canvas) return
|
|
||||||
|
|
||||||
new ResizeObserver(() => {
|
|
||||||
if (Date.now() - last.current < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE)
|
|
||||||
return
|
|
||||||
last.current = Date.now()
|
|
||||||
|
|
||||||
if (
|
|
||||||
Math.abs(video.width - window.innerWidth) > 4 ||
|
|
||||||
Math.abs(video.height - window.innerHeight) > 4
|
|
||||||
) {
|
|
||||||
timeoutStart.current = Date.now()
|
|
||||||
configure()
|
|
||||||
}
|
|
||||||
}).observe(document.body)
|
|
||||||
}, [engineStreamState.value])
|
|
||||||
|
|
||||||
// When the video and canvas element references are set, start the engine.
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
engineStreamState.context.canvasRef.current &&
|
|
||||||
engineStreamState.context.videoRef.current
|
|
||||||
) {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.StartOrReconfigureEngine,
|
|
||||||
modelingMachineActorSend,
|
|
||||||
settings: settingsEngine,
|
|
||||||
setAppState,
|
|
||||||
onMediaStream(mediaStream: MediaStream) {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.SetMediaStream,
|
|
||||||
mediaStream,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
engineStreamState.context.canvasRef.current,
|
|
||||||
engineStreamState.context.videoRef.current,
|
|
||||||
])
|
|
||||||
|
|
||||||
// On settings change, reconfigure the engine. When paused this gets really tricky,
|
|
||||||
// and also requires onMediaStream to be set!
|
|
||||||
useEffect(() => {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.StartOrReconfigureEngine,
|
|
||||||
modelingMachineActorSend,
|
|
||||||
settings: settingsEngine,
|
|
||||||
setAppState,
|
|
||||||
onMediaStream(mediaStream: MediaStream) {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.SetMediaStream,
|
|
||||||
mediaStream,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [settings.context])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to execute code when the file changes
|
|
||||||
* but only if the scene is already ready.
|
|
||||||
* See onSceneReady for the initial scene setup.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
|
|
||||||
console.log('execute on file change')
|
|
||||||
void kclManager.executeCode(true).catch(trap)
|
|
||||||
}
|
|
||||||
}, [file?.path, engineCommandManager.engineConnection])
|
|
||||||
|
|
||||||
const IDLE_TIME_MS = Number(streamIdleMode)
|
|
||||||
|
|
||||||
// When streamIdleMode is changed, setup or teardown the timeouts
|
|
||||||
const timeoutStart = useRef<number | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
timeoutStart.current = streamIdleMode ? Date.now() : null
|
|
||||||
}, [streamIdleMode])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let frameId: ReturnType<typeof window.requestAnimationFrame> = 0
|
|
||||||
const frameLoop = () => {
|
|
||||||
// Do not pause if the user is in the middle of an operation
|
|
||||||
if (!modelingMachineState.matches('idle')) {
|
|
||||||
// In fact, stop the timeout, because we don't want to trigger the
|
|
||||||
// pause when we exit the operation.
|
|
||||||
timeoutStart.current = null
|
|
||||||
} else if (timeoutStart.current) {
|
|
||||||
const elapsed = Date.now() - timeoutStart.current
|
|
||||||
if (elapsed >= IDLE_TIME_MS) {
|
|
||||||
timeoutStart.current = null
|
|
||||||
engineStreamActor.send({ type: EngineStreamTransition.Pause })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frameId = window.requestAnimationFrame(frameLoop)
|
|
||||||
}
|
|
||||||
frameId = window.requestAnimationFrame(frameLoop)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.cancelAnimationFrame(frameId)
|
|
||||||
}
|
|
||||||
}, [modelingMachineState])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!streamIdleMode) return
|
|
||||||
|
|
||||||
const onAnyInput = () => {
|
|
||||||
// Just in case it happens in the middle of the user turning off
|
|
||||||
// idle mode.
|
|
||||||
if (!streamIdleMode) {
|
|
||||||
timeoutStart.current = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (engineStreamState.value === EngineStreamState.Paused) {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.StartOrReconfigureEngine,
|
|
||||||
modelingMachineActorSend,
|
|
||||||
settings: settingsEngine,
|
|
||||||
setAppState,
|
|
||||||
onMediaStream(mediaStream: MediaStream) {
|
|
||||||
engineStreamActor.send({
|
|
||||||
type: EngineStreamTransition.SetMediaStream,
|
|
||||||
mediaStream,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
timeoutStart.current = Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's possible after a reconnect, the user doesn't move their mouse at
|
|
||||||
// all, meaning the timer is not reset to run. We need to set it every
|
|
||||||
// time our effect dependencies change then.
|
|
||||||
timeoutStart.current = Date.now()
|
|
||||||
|
|
||||||
window.document.addEventListener('keydown', onAnyInput)
|
|
||||||
window.document.addEventListener('keyup', onAnyInput)
|
|
||||||
window.document.addEventListener('mousemove', onAnyInput)
|
|
||||||
window.document.addEventListener('mousedown', onAnyInput)
|
|
||||||
window.document.addEventListener('mouseup', onAnyInput)
|
|
||||||
window.document.addEventListener('scroll', onAnyInput)
|
|
||||||
window.document.addEventListener('touchstart', onAnyInput)
|
|
||||||
window.document.addEventListener('touchstop', onAnyInput)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
timeoutStart.current = null
|
|
||||||
window.document.removeEventListener('keydown', onAnyInput)
|
|
||||||
window.document.removeEventListener('keyup', onAnyInput)
|
|
||||||
window.document.removeEventListener('mousemove', onAnyInput)
|
|
||||||
window.document.removeEventListener('mousedown', onAnyInput)
|
|
||||||
window.document.removeEventListener('mouseup', onAnyInput)
|
|
||||||
window.document.removeEventListener('scroll', onAnyInput)
|
|
||||||
window.document.removeEventListener('touchstart', onAnyInput)
|
|
||||||
window.document.removeEventListener('touchstop', onAnyInput)
|
|
||||||
}
|
|
||||||
}, [streamIdleMode, engineStreamState.value])
|
|
||||||
|
|
||||||
const isNetworkOkay =
|
|
||||||
overallState === NetworkHealthState.Ok ||
|
|
||||||
overallState === NetworkHealthState.Weak
|
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
|
||||||
if (!isNetworkOkay) return
|
|
||||||
if (!engineStreamState.context.videoRef.current) return
|
|
||||||
if (modelingMachineState.matches('Sketch')) return
|
|
||||||
if (modelingMachineState.matches({ idle: 'showPlanes' })) return
|
|
||||||
|
|
||||||
if (btnName(e.nativeEvent).left) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
sendSelectEventToEngine(e, engineStreamState.context.videoRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 z-0"
|
|
||||||
id="stream"
|
|
||||||
data-testid="stream"
|
|
||||||
onMouseUp={handleMouseUp}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
onContextMenuCapture={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
autoPlay
|
|
||||||
muted
|
|
||||||
key={engineStreamActor.id + 'video'}
|
|
||||||
ref={engineStreamState.context.videoRef}
|
|
||||||
controls={false}
|
|
||||||
className="cursor-pointer"
|
|
||||||
disablePictureInPicture
|
|
||||||
id="video-stream"
|
|
||||||
/>
|
|
||||||
<canvas
|
|
||||||
key={engineStreamActor.id + 'canvas'}
|
|
||||||
ref={engineStreamState.context.canvasRef}
|
|
||||||
className="cursor-pointer"
|
|
||||||
id="freeze-frame"
|
|
||||||
>
|
|
||||||
No canvas support
|
|
||||||
</canvas>
|
|
||||||
<ClientSideScene
|
|
||||||
cameraControls={settings.context.modeling.mouseControls.current}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,47 +1,40 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useEngineCommands } from './EngineCommands'
|
import { useEngineCommands } from './EngineCommands'
|
||||||
|
import { Spinner } from './Spinner'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import useEngineStreamContext, {
|
|
||||||
EngineStreamState,
|
|
||||||
} from 'hooks/useEngineStreamContext'
|
|
||||||
import { CommandLogType } from 'lang/std/engineConnection'
|
|
||||||
|
|
||||||
export const ModelStateIndicator = () => {
|
export const ModelStateIndicator = () => {
|
||||||
const [commands] = useEngineCommands()
|
const [commands] = useEngineCommands()
|
||||||
const [isDone, setIsDone] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const engineStreamActor = useEngineStreamContext.useActorRef()
|
|
||||||
const engineStreamState = engineStreamActor.getSnapshot()
|
|
||||||
|
|
||||||
const lastCommandType = commands[commands.length - 1]?.type
|
const lastCommandType = commands[commands.length - 1]?.type
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (lastCommandType === CommandLogType.SetDefaultSystemProperties) {
|
|
||||||
setIsDone(false)
|
|
||||||
}
|
|
||||||
if (lastCommandType === CommandLogType.ExecutionDone) {
|
|
||||||
setIsDone(true)
|
|
||||||
}
|
|
||||||
}, [lastCommandType])
|
|
||||||
|
|
||||||
let className = 'w-6 h-6 '
|
let className = 'w-6 h-6 '
|
||||||
let icon = <div className={className}></div>
|
let icon = <Spinner className={className} />
|
||||||
let dataTestId = 'model-state-indicator'
|
let dataTestId = 'model-state-indicator'
|
||||||
|
|
||||||
if (engineStreamState.value === EngineStreamState.Paused) {
|
if (lastCommandType === 'receive-reliable') {
|
||||||
className += 'text-secondary'
|
className +=
|
||||||
icon = <CustomIcon data-testid={dataTestId + '-paused'} name="parallel" />
|
'bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
||||||
} else if (engineStreamState.value === EngineStreamState.Resuming) {
|
icon = (
|
||||||
className += 'text-secondary'
|
<CustomIcon
|
||||||
icon = <CustomIcon data-testid={dataTestId + '-resuming'} name="parallel" />
|
data-testid={dataTestId + '-receive-reliable'}
|
||||||
} else if (isDone) {
|
name="checkmark"
|
||||||
className += 'text-secondary'
|
/>
|
||||||
|
)
|
||||||
|
} else if (lastCommandType === 'execution-done') {
|
||||||
|
className +=
|
||||||
|
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
||||||
icon = (
|
icon = (
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
data-testid={dataTestId + '-execution-done'}
|
data-testid={dataTestId + '-execution-done'}
|
||||||
name="checkmark"
|
name="checkmark"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
} else if (lastCommandType === 'export-done') {
|
||||||
|
className +=
|
||||||
|
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
||||||
|
icon = (
|
||||||
|
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
modelingMachine,
|
modelingMachine,
|
||||||
modelingMachineDefaultContext,
|
modelingMachineDefaultContext,
|
||||||
} from 'machines/modelingMachine'
|
} from 'machines/modelingMachine'
|
||||||
|
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import {
|
import {
|
||||||
isCursorInSketchCommandRange,
|
isCursorInSketchCommandRange,
|
||||||
@ -62,6 +63,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
moveValueIntoNewVariablePath,
|
moveValueIntoNewVariablePath,
|
||||||
sketchOnExtrudedFace,
|
sketchOnExtrudedFace,
|
||||||
|
sketchOnOffsetPlane,
|
||||||
startSketchOnDefault,
|
startSketchOnDefault,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { Program, parse, recast } from 'lang/wasm'
|
import { Program, parse, recast } from 'lang/wasm'
|
||||||
@ -111,8 +113,13 @@ export const ModelingMachineProvider = ({
|
|||||||
auth,
|
auth,
|
||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme },
|
app: { theme, enableSSAO },
|
||||||
modeling: { defaultUnit, highlightEdges, cameraProjection },
|
modeling: {
|
||||||
|
defaultUnit,
|
||||||
|
cameraProjection,
|
||||||
|
highlightEdges,
|
||||||
|
showScaleGrid,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
@ -123,6 +130,9 @@ export const ModelingMachineProvider = ({
|
|||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
const persistedContext = useMemo(() => getPersistedContext(), [])
|
const persistedContext = useMemo(() => getPersistedContext(), [])
|
||||||
|
|
||||||
|
let [searchParams] = useSearchParams()
|
||||||
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
@ -474,7 +484,7 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager.exportInfo = {
|
engineCommandManager.exportInfo = {
|
||||||
intent: ExportIntent.Save,
|
intent: ExportIntent.Save,
|
||||||
// This never gets used its only for make.
|
// This never gets used its only for make.
|
||||||
name: '',
|
name: file?.name?.replace('.kcl', `.${event.data.type}`) || '',
|
||||||
}
|
}
|
||||||
|
|
||||||
const format = {
|
const format = {
|
||||||
@ -627,13 +637,16 @@ export const ModelingMachineProvider = ({
|
|||||||
),
|
),
|
||||||
'animate-to-face': fromPromise(async ({ input }) => {
|
'animate-to-face': fromPromise(async ({ input }) => {
|
||||||
if (!input) return undefined
|
if (!input) return undefined
|
||||||
if (input.type === 'extrudeFace') {
|
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
|
||||||
const sketched = sketchOnExtrudedFace(
|
const sketched =
|
||||||
kclManager.ast,
|
input.type === 'extrudeFace'
|
||||||
input.sketchPathToNode,
|
? sketchOnExtrudedFace(
|
||||||
input.extrudePathToNode,
|
kclManager.ast,
|
||||||
input.faceInfo
|
input.sketchPathToNode,
|
||||||
)
|
input.extrudePathToNode,
|
||||||
|
input.faceInfo
|
||||||
|
)
|
||||||
|
: sketchOnOffsetPlane(kclManager.ast, input.pathToNode)
|
||||||
if (err(sketched)) {
|
if (err(sketched)) {
|
||||||
const sketchedError = new Error(
|
const sketchedError = new Error(
|
||||||
'Incompatible face, please try another'
|
'Incompatible face, please try another'
|
||||||
@ -645,13 +658,9 @@ export const ModelingMachineProvider = ({
|
|||||||
|
|
||||||
await kclManager.executeAstMock(modifiedAst)
|
await kclManager.executeAstMock(modifiedAst)
|
||||||
|
|
||||||
await letEngineAnimateAndSyncCamAfter(
|
const id =
|
||||||
engineCommandManager,
|
input.type === 'extrudeFace' ? input.faceId : input.planeId
|
||||||
input.faceId
|
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
|
||||||
)
|
|
||||||
await sceneInfra.camControls.centerModelRelativeToPanes({
|
|
||||||
resetLastPaneWidth: true,
|
|
||||||
})
|
|
||||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNewSketchNode,
|
sketchPathToNode: pathToNewSketchNode,
|
||||||
@ -672,9 +681,6 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
input.planeId
|
input.planeId
|
||||||
)
|
)
|
||||||
await sceneInfra.camControls.centerModelRelativeToPanes({
|
|
||||||
resetLastPaneWidth: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
@ -697,9 +703,6 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
info?.sketchDetails?.faceId || ''
|
info?.sketchDetails?.faceId || ''
|
||||||
)
|
)
|
||||||
await sceneInfra.camControls.centerModelRelativeToPanes({
|
|
||||||
resetLastPaneWidth: true,
|
|
||||||
})
|
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: sketchPathToNode || [],
|
sketchPathToNode: sketchPathToNode || [],
|
||||||
zAxis: info.sketchDetails.zAxis || null,
|
zAxis: info.sketchDetails.zAxis || null,
|
||||||
@ -1068,6 +1071,21 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useSetupEngineManager(
|
||||||
|
streamRef,
|
||||||
|
modelingSend,
|
||||||
|
modelingState.context,
|
||||||
|
{
|
||||||
|
pool: pool,
|
||||||
|
theme: theme.current,
|
||||||
|
highlightEdges: highlightEdges.current,
|
||||||
|
enableSSAO: enableSSAO.current,
|
||||||
|
showScaleGrid: showScaleGrid.current,
|
||||||
|
cameraProjection: cameraProjection.current,
|
||||||
|
},
|
||||||
|
token
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kclManager.registerExecuteCallback(() => {
|
kclManager.registerExecuteCallback(() => {
|
||||||
modelingSend({ type: 'Re-execute' })
|
modelingSend({ type: 'Re-execute' })
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
completionKeymap,
|
completionKeymap,
|
||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
import CodeEditor from './CodeEditor'
|
import CodeEditor from './CodeEditor'
|
||||||
|
import { codeManagerHistoryCompartment } from 'lang/codeManager'
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -89,7 +90,7 @@ export const KclEditorPane = () => {
|
|||||||
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
||||||
}),
|
}),
|
||||||
lineHighlightField,
|
lineHighlightField,
|
||||||
history(),
|
codeManagerHistoryCompartment.of(history()),
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
codeFolding(),
|
codeFolding(),
|
||||||
keymap.of([
|
keymap.of([
|
||||||
@ -121,7 +122,6 @@ export const KclEditorPane = () => {
|
|||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
history(),
|
|
||||||
foldGutter(),
|
foldGutter(),
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
indentOnInput(),
|
indentOnInput(),
|
||||||
|
@ -5,12 +5,12 @@ import {
|
|||||||
ProgramMemory,
|
ProgramMemory,
|
||||||
Path,
|
Path,
|
||||||
ExtrudeSurface,
|
ExtrudeSurface,
|
||||||
sketchFromKclValue,
|
sketchFromKclValueOptional,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { useResolvedTheme } from 'hooks/useResolvedTheme'
|
import { useResolvedTheme } from 'hooks/useResolvedTheme'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { err, trap } from 'lib/trap'
|
import { Reason, trap } from 'lib/trap'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
@ -93,13 +93,13 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
val.type !== 'Function'
|
val.type !== 'Function'
|
||||||
) {
|
) {
|
||||||
const sg = sketchFromKclValue(val, key)
|
const sk = sketchFromKclValueOptional(val, key)
|
||||||
if (val.type === 'Solid') {
|
if (val.type === 'Solid') {
|
||||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||||
return rest
|
return rest
|
||||||
})
|
})
|
||||||
} else if (!err(sg)) {
|
} else if (!(sk instanceof Reason)) {
|
||||||
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => {
|
processedMemory[key] = sk.paths.map(({ __geoMeta, ...rest }: Path) => {
|
||||||
return rest
|
return rest
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,11 +6,6 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useContext,
|
useContext,
|
||||||
MutableRefObject,
|
|
||||||
forwardRef,
|
|
||||||
// https://stackoverflow.com/a/77055468 Thank you.
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
|
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
|
||||||
@ -24,12 +19,9 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||||
import { sceneInfra } from 'lib/singletons'
|
|
||||||
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
ref: MutableRefObject<HTMLDivElement>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BadgeInfoComputed {
|
interface BadgeInfoComputed {
|
||||||
@ -41,34 +33,19 @@ function getPlatformString(): 'web' | 'desktop' {
|
|||||||
return isDesktop() ? 'desktop' : 'web'
|
return isDesktop() ? 'desktop' : 'web'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelingSidebar = forwardRef<
|
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||||
HTMLUListElement,
|
|
||||||
ModelingSidebarProps
|
|
||||||
>(function ModelingSidebar({ paneOpacity }, outerRef) {
|
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const kclContext = useKclContext()
|
const kclContext = useKclContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
const { send, state, context } = useModelingContext()
|
const { send, context } = useModelingContext()
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
onboardingStatus.current === 'camera' ||
|
onboardingStatus.current === 'camera' ||
|
||||||
context.store?.openPanes.length === 0
|
context.store?.openPanes.length === 0
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto '
|
: 'pointer-events-auto '
|
||||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||||
const innerRef = useRef<HTMLUListElement>(null)
|
|
||||||
|
|
||||||
// forwardRef's type causes me to do this type narrowing.
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof outerRef === 'function') {
|
|
||||||
outerRef(innerRef.current)
|
|
||||||
} else {
|
|
||||||
if (outerRef) {
|
|
||||||
outerRef.current = innerRef.current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [innerRef.current])
|
|
||||||
|
|
||||||
const paneCallbackProps = useMemo(
|
const paneCallbackProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -182,37 +159,8 @@ export const ModelingSidebar = forwardRef<
|
|||||||
[context.store?.openPanes, send]
|
[context.store?.openPanes, send]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Don't send camera adjustment commands after 1 pane is open. It
|
|
||||||
// won't make any difference.
|
|
||||||
if (context.store?.openPanes.length > 1) return
|
|
||||||
|
|
||||||
void sceneInfra.camControls.centerModelRelativeToPanes()
|
|
||||||
}, [context.store?.openPanes])
|
|
||||||
|
|
||||||
// If the panes are resized then center the model also
|
|
||||||
useEffect(() => {
|
|
||||||
if (!innerRef.current) return
|
|
||||||
|
|
||||||
let last = Date.now()
|
|
||||||
const observer = new ResizeObserver(() => {
|
|
||||||
if (Date.now() - last < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE) return
|
|
||||||
if (!innerRef.current) return
|
|
||||||
|
|
||||||
last = Date.now()
|
|
||||||
void sceneInfra.camControls.centerModelRelativeToPanes()
|
|
||||||
})
|
|
||||||
|
|
||||||
observer.observe(innerRef.current)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [state, innerRef.current])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Resizable
|
<Resizable
|
||||||
data-testid="modeling-sidebar"
|
|
||||||
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
||||||
defaultSize={{
|
defaultSize={{
|
||||||
width: '550px',
|
width: '550px',
|
||||||
@ -244,7 +192,6 @@ export const ModelingSidebar = forwardRef<
|
|||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
id="pane-buttons-section"
|
id="pane-buttons-section"
|
||||||
data-testid="pane-buttons-section"
|
|
||||||
className={
|
className={
|
||||||
'w-fit p-2 flex flex-col gap-2 ' +
|
'w-fit p-2 flex flex-col gap-2 ' +
|
||||||
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
|
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
|
||||||
@ -289,8 +236,6 @@ export const ModelingSidebar = forwardRef<
|
|||||||
</ul>
|
</ul>
|
||||||
<ul
|
<ul
|
||||||
id="pane-section"
|
id="pane-section"
|
||||||
data-testid="pane-section"
|
|
||||||
ref={innerRef}
|
|
||||||
className={
|
className={
|
||||||
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
|
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
|
||||||
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
|
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
|
||||||
@ -320,7 +265,7 @@ export const ModelingSidebar = forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
interface ModelingPaneButtonProps
|
interface ModelingPaneButtonProps
|
||||||
extends React.HTMLAttributes<HTMLButtonElement> {
|
extends React.HTMLAttributes<HTMLButtonElement> {
|
||||||
|
340
src/components/Stream.tsx
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||||
|
import Loading from './Loading'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||||
|
import { btnName } from 'lib/cameraControls'
|
||||||
|
import { sendSelectEventToEngine } from 'lib/selections'
|
||||||
|
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||||
|
import { useAppStream } from 'AppState'
|
||||||
|
import {
|
||||||
|
EngineCommandManagerEvents,
|
||||||
|
EngineConnectionStateType,
|
||||||
|
DisconnectingType,
|
||||||
|
} from 'lang/std/engineConnection'
|
||||||
|
import { useRouteLoaderData } from 'react-router-dom'
|
||||||
|
import { PATHS } from 'lib/paths'
|
||||||
|
import { IndexLoaderData } from 'lib/types'
|
||||||
|
|
||||||
|
enum StreamState {
|
||||||
|
Playing = 'playing',
|
||||||
|
Paused = 'paused',
|
||||||
|
Resuming = 'resuming',
|
||||||
|
Unset = 'unset',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Stream = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
const { settings } = useSettingsAuthContext()
|
||||||
|
const { state, send } = useModelingContext()
|
||||||
|
const { mediaStream } = useAppStream()
|
||||||
|
const { overallState, immediateState } = useNetworkContext()
|
||||||
|
const [streamState, setStreamState] = useState(StreamState.Unset)
|
||||||
|
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
|
|
||||||
|
const IDLE = settings.context.app.streamIdleMode.current
|
||||||
|
|
||||||
|
const isNetworkOkay =
|
||||||
|
overallState === NetworkHealthState.Ok ||
|
||||||
|
overallState === NetworkHealthState.Weak
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute code and show a "building scene message"
|
||||||
|
* in Stream.tsx in the meantime.
|
||||||
|
*
|
||||||
|
* I would like for this to live somewhere more central,
|
||||||
|
* but it seems to me that we need the video element ref
|
||||||
|
* to be able to play the video after the code has been
|
||||||
|
* executed. If we can find a way to do this from a more
|
||||||
|
* central place, we can move this code there.
|
||||||
|
*/
|
||||||
|
function executeCodeAndPlayStream() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
kclManager.executeCode(true).then(async () => {
|
||||||
|
await videoRef.current?.play().catch((e) => {
|
||||||
|
console.warn('Video playing was prevented', e, videoRef.current)
|
||||||
|
})
|
||||||
|
setStreamState(StreamState.Playing)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to execute code when the file changes
|
||||||
|
* but only if the scene is already ready.
|
||||||
|
* See onSceneReady for the initial scene setup.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
|
||||||
|
console.log('execute on file change')
|
||||||
|
executeCodeAndPlayStream()
|
||||||
|
}
|
||||||
|
}, [file?.path, engineCommandManager.engineConnection])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
immediateState.type === EngineConnectionStateType.Disconnecting &&
|
||||||
|
immediateState.value.type === DisconnectingType.Pause
|
||||||
|
) {
|
||||||
|
setStreamState(StreamState.Paused)
|
||||||
|
}
|
||||||
|
}, [immediateState])
|
||||||
|
|
||||||
|
// Linux has a default behavior to paste text on middle mouse up
|
||||||
|
// This adds a listener to block that pasting if the click target
|
||||||
|
// is not a text input, so users can move in the 3D scene with
|
||||||
|
// middle mouse drag with a text input focused without pasting.
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePaste = (e: ClipboardEvent) => {
|
||||||
|
const isHtmlElement = e.target && e.target instanceof HTMLElement
|
||||||
|
const isEditable =
|
||||||
|
(isHtmlElement && !('explicitOriginalTarget' in e)) ||
|
||||||
|
('explicitOriginalTarget' in e &&
|
||||||
|
((e.explicitOriginalTarget as HTMLElement).contentEditable ===
|
||||||
|
'true' ||
|
||||||
|
['INPUT', 'TEXTAREA'].some(
|
||||||
|
(tagName) =>
|
||||||
|
tagName === (e.explicitOriginalTarget as HTMLElement).tagName
|
||||||
|
)))
|
||||||
|
if (!isEditable) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis?.window?.document?.addEventListener('paste', handlePaste, {
|
||||||
|
capture: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const IDLE_TIME_MS = 1000 * 60 * 2
|
||||||
|
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
|
||||||
|
|
||||||
|
const teardown = () => {
|
||||||
|
// Already paused
|
||||||
|
if (streamState === StreamState.Paused) return
|
||||||
|
|
||||||
|
videoRef.current?.pause()
|
||||||
|
setStreamState(StreamState.Paused)
|
||||||
|
sceneInfra.modelingSend({ type: 'Cancel' })
|
||||||
|
// Give video time to pause
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
engineCommandManager.tearDown({ idleMode: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
if (globalThis.window.document.visibilityState === 'hidden') {
|
||||||
|
clearTimeout(timeoutIdIdleA)
|
||||||
|
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
|
||||||
|
} else if (!engineCommandManager.engineConnection?.isReady()) {
|
||||||
|
clearTimeout(timeoutIdIdleA)
|
||||||
|
setStreamState(StreamState.Resuming)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teardown everything if we go hidden or reconnect
|
||||||
|
if (IDLE) {
|
||||||
|
globalThis?.window?.document?.addEventListener(
|
||||||
|
'visibilitychange',
|
||||||
|
onVisibilityChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
|
||||||
|
|
||||||
|
const onAnyInput = () => {
|
||||||
|
if (streamState === StreamState.Playing) {
|
||||||
|
// Clear both timers
|
||||||
|
clearTimeout(timeoutIdIdleA)
|
||||||
|
clearTimeout(timeoutIdIdleB)
|
||||||
|
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
||||||
|
}
|
||||||
|
if (streamState === StreamState.Paused) {
|
||||||
|
setStreamState(StreamState.Resuming)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IDLE) {
|
||||||
|
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
|
||||||
|
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
|
||||||
|
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
|
||||||
|
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
|
||||||
|
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IDLE) {
|
||||||
|
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a listener to execute code and play the stream
|
||||||
|
* on initial stream setup.
|
||||||
|
*/
|
||||||
|
engineCommandManager.addEventListener(
|
||||||
|
EngineCommandManagerEvents.SceneReady,
|
||||||
|
executeCodeAndPlayStream
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
engineCommandManager.removeEventListener(
|
||||||
|
EngineCommandManagerEvents.SceneReady,
|
||||||
|
executeCodeAndPlayStream
|
||||||
|
)
|
||||||
|
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
|
||||||
|
capture: true,
|
||||||
|
})
|
||||||
|
if (IDLE) {
|
||||||
|
clearTimeout(timeoutIdIdleA)
|
||||||
|
clearTimeout(timeoutIdIdleB)
|
||||||
|
|
||||||
|
globalThis?.window?.document?.removeEventListener(
|
||||||
|
'visibilitychange',
|
||||||
|
onVisibilityChange
|
||||||
|
)
|
||||||
|
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
|
||||||
|
globalThis?.window?.document?.removeEventListener(
|
||||||
|
'mousemove',
|
||||||
|
onAnyInput
|
||||||
|
)
|
||||||
|
globalThis?.window?.document?.removeEventListener(
|
||||||
|
'mousedown',
|
||||||
|
onAnyInput
|
||||||
|
)
|
||||||
|
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
|
||||||
|
globalThis?.window?.document?.removeEventListener(
|
||||||
|
'touchstart',
|
||||||
|
onAnyInput
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [IDLE, streamState])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play the vid
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!kclManager.isExecuting) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// execute in the next event loop
|
||||||
|
videoRef.current?.play().catch((e) => {
|
||||||
|
console.warn('Video playing was prevented', e, videoRef.current)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [kclManager.isExecuting])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
typeof window === 'undefined' ||
|
||||||
|
typeof RTCPeerConnection === 'undefined'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if (!videoRef.current) return
|
||||||
|
if (!mediaStream) return
|
||||||
|
|
||||||
|
// The browser complains if we try to load a new stream without pausing first.
|
||||||
|
// Do not immediately play the stream!
|
||||||
|
try {
|
||||||
|
videoRef.current.srcObject = mediaStream
|
||||||
|
videoRef.current.pause()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Attempted to pause stream while play was still loading', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
videoElement: videoRef.current,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
}, [mediaStream])
|
||||||
|
|
||||||
|
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
// If we've got no stream or connection, don't do anything
|
||||||
|
if (!isNetworkOkay) return
|
||||||
|
if (!videoRef.current) return
|
||||||
|
// If we're in sketch mode, don't send a engine-side select event
|
||||||
|
if (state.matches('Sketch')) return
|
||||||
|
if (state.matches({ idle: 'showPlanes' })) return
|
||||||
|
// If we're mousing up from a camera drag, don't send a select event
|
||||||
|
if (sceneInfra.camControls.wasDragging === true) return
|
||||||
|
|
||||||
|
if (btnName(e.nativeEvent).left) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
sendSelectEventToEngine(e, videoRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
id="stream"
|
||||||
|
data-testid="stream"
|
||||||
|
onClick={handleMouseUp}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
onContextMenuCapture={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
muted
|
||||||
|
autoPlay
|
||||||
|
controls={false}
|
||||||
|
onPlay={() => setIsLoading(false)}
|
||||||
|
className="w-full cursor-pointer h-full"
|
||||||
|
disablePictureInPicture
|
||||||
|
id="video-stream"
|
||||||
|
/>
|
||||||
|
<ClientSideScene
|
||||||
|
cameraControls={settings.context.modeling.mouseControls.current}
|
||||||
|
/>
|
||||||
|
{(streamState === StreamState.Paused ||
|
||||||
|
streamState === StreamState.Resuming) && (
|
||||||
|
<div className="text-center absolute inset-0">
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center h-screen"
|
||||||
|
data-testid="paused"
|
||||||
|
>
|
||||||
|
<div className="border-primary border p-2 rounded-sm">
|
||||||
|
<svg
|
||||||
|
width="8"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 8 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M2 12V0H0V12H2ZM8 12V0H6V12H8Z"
|
||||||
|
fill="var(--primary)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-base mt-2 text-primary bold">
|
||||||
|
{streamState === StreamState.Paused && 'Paused'}
|
||||||
|
{streamState === StreamState.Resuming && 'Resuming'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(!isNetworkOkay || isLoading) && (
|
||||||
|
<div className="text-center absolute inset-0">
|
||||||
|
<Loading>
|
||||||
|
{!isNetworkOkay && !isLoading ? (
|
||||||
|
<span data-testid="loading-stream">Stream disconnected...</span>
|
||||||
|
) : (
|
||||||
|
!isLoading && (
|
||||||
|
<span data-testid="loading-stream">Loading stream...</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Loading>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -88,6 +88,10 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
? [codeRef.range]
|
? [codeRef.range]
|
||||||
: [codeRef.range, consumedCodeRef.range]
|
: [codeRef.range, consumedCodeRef.range]
|
||||||
)
|
)
|
||||||
|
} else if (artifact?.type === 'plane') {
|
||||||
|
const codeRef = artifact.codeRef
|
||||||
|
if (err(codeRef)) return
|
||||||
|
editorManager.setHighlightRange([codeRef.range])
|
||||||
} else {
|
} else {
|
||||||
editorManager.setHighlightRange([[0, 0]])
|
editorManager.setHighlightRange([[0, 0]])
|
||||||
}
|
}
|
||||||
@ -186,8 +190,42 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
})
|
})
|
||||||
return
|
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 faceId = planeOrFaceId
|
||||||
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
|
||||||
const extrusion = getSweepFromSuspectedSweepSurface(
|
const extrusion = getSweepFromSuspectedSweepSurface(
|
||||||
faceId,
|
faceId,
|
||||||
engineCommandManager.artifactGraph
|
engineCommandManager.artifactGraph
|
||||||
|
@ -1,237 +0,0 @@
|
|||||||
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
|
||||||
import { MutableRefObject } from 'react'
|
|
||||||
import { setup, assign } from 'xstate'
|
|
||||||
import { createActorContext } from '@xstate/react'
|
|
||||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
|
||||||
import { trap } from 'lib/trap'
|
|
||||||
|
|
||||||
export enum EngineStreamState {
|
|
||||||
Off = 'off',
|
|
||||||
On = 'on',
|
|
||||||
Playing = 'playing',
|
|
||||||
Paused = 'paused',
|
|
||||||
Resuming = 'resuming',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum EngineStreamTransition {
|
|
||||||
SetMediaStream = 'set-context',
|
|
||||||
Play = 'play',
|
|
||||||
Resume = 'resume',
|
|
||||||
Pause = 'pause',
|
|
||||||
StartOrReconfigureEngine = 'start-or-reconfigure-engine',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EngineStreamContext {
|
|
||||||
pool: string | null
|
|
||||||
authToken: string | null
|
|
||||||
mediaStream: MediaStream | null
|
|
||||||
videoRef: MutableRefObject<HTMLVideoElement | null>
|
|
||||||
canvasRef: MutableRefObject<HTMLCanvasElement | null>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDimensions(streamWidth: number, streamHeight: number) {
|
|
||||||
const factorOf = 4
|
|
||||||
const maxResolution = 2160
|
|
||||||
const ratio = Math.min(
|
|
||||||
Math.min(maxResolution / streamWidth, maxResolution / streamHeight),
|
|
||||||
1.0
|
|
||||||
)
|
|
||||||
const quadWidth = Math.round((streamWidth * ratio) / factorOf) * factorOf
|
|
||||||
const quadHeight = Math.round((streamHeight * ratio) / factorOf) * factorOf
|
|
||||||
return { width: quadWidth, height: quadHeight }
|
|
||||||
}
|
|
||||||
|
|
||||||
const engineStreamMachine = setup({
|
|
||||||
types: {
|
|
||||||
context: {} as EngineStreamContext,
|
|
||||||
input: {} as EngineStreamContext,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
[EngineStreamTransition.Play]({ context }, params: { zoomToFit: boolean }) {
|
|
||||||
const canvas = context.canvasRef.current
|
|
||||||
if (!canvas) return false
|
|
||||||
|
|
||||||
const video = context.videoRef.current
|
|
||||||
if (!video) return false
|
|
||||||
|
|
||||||
const mediaStream = context.mediaStream
|
|
||||||
if (!mediaStream) return false
|
|
||||||
|
|
||||||
video.style.display = 'block'
|
|
||||||
canvas.style.display = 'none'
|
|
||||||
|
|
||||||
video.srcObject = mediaStream
|
|
||||||
void sceneInfra.camControls
|
|
||||||
.restoreCameraPosition()
|
|
||||||
.then(() => video.play())
|
|
||||||
.catch((e) => {
|
|
||||||
console.warn('Video playing was prevented', e, video)
|
|
||||||
})
|
|
||||||
.then(() => kclManager.executeCode(params.zoomToFit))
|
|
||||||
.catch(trap)
|
|
||||||
},
|
|
||||||
[EngineStreamTransition.Pause]({ context }) {
|
|
||||||
const video = context.videoRef.current
|
|
||||||
if (!video) return
|
|
||||||
|
|
||||||
video.pause()
|
|
||||||
|
|
||||||
const canvas = context.canvasRef.current
|
|
||||||
if (!canvas) return
|
|
||||||
|
|
||||||
canvas.width = video.videoWidth
|
|
||||||
canvas.height = video.videoHeight
|
|
||||||
canvas.style.width = video.videoWidth + 'px'
|
|
||||||
canvas.style.height = video.videoHeight + 'px'
|
|
||||||
canvas.style.display = 'block'
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
if (!ctx) return
|
|
||||||
|
|
||||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
|
||||||
|
|
||||||
// Make sure we're on the next frame for no flickering between canvas
|
|
||||||
// and the video elements.
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
video.style.display = 'none'
|
|
||||||
|
|
||||||
// Destroy the media stream only. We will re-establish it. We could
|
|
||||||
// leave everything at pausing, preventing video decoders from running
|
|
||||||
// but we can do even better by significantly reducing network
|
|
||||||
// cards also.
|
|
||||||
context.mediaStream?.getVideoTracks()[0].stop()
|
|
||||||
video.srcObject = null
|
|
||||||
|
|
||||||
sceneInfra.camControls.old = {
|
|
||||||
camera: sceneInfra.camControls.camera.clone(),
|
|
||||||
target: sceneInfra.camControls.target.clone(),
|
|
||||||
}
|
|
||||||
|
|
||||||
engineCommandManager.tearDown({ idleMode: true })
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async [EngineStreamTransition.StartOrReconfigureEngine]({
|
|
||||||
context,
|
|
||||||
event,
|
|
||||||
}) {
|
|
||||||
if (!context.authToken) return
|
|
||||||
|
|
||||||
const video = context.videoRef.current
|
|
||||||
if (!video) return
|
|
||||||
|
|
||||||
const { width, height } = getDimensions(
|
|
||||||
window.innerWidth,
|
|
||||||
window.innerHeight
|
|
||||||
)
|
|
||||||
|
|
||||||
video.width = width
|
|
||||||
video.height = height
|
|
||||||
|
|
||||||
const settingsNext = {
|
|
||||||
// override the pool param (?pool=) to request a specific engine instance
|
|
||||||
// from a particular pool.
|
|
||||||
pool: context.pool,
|
|
||||||
...event.settings,
|
|
||||||
}
|
|
||||||
|
|
||||||
engineCommandManager.settings = settingsNext
|
|
||||||
|
|
||||||
engineCommandManager.start({
|
|
||||||
setMediaStream: event.onMediaStream,
|
|
||||||
setIsStreamReady: (isStreamReady) =>
|
|
||||||
event.setAppState({ isStreamReady }),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
token: context.authToken,
|
|
||||||
settings: settingsNext,
|
|
||||||
makeDefaultPlanes: () => {
|
|
||||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
|
||||||
},
|
|
||||||
modifyGrid: (hidden: boolean) => {
|
|
||||||
return modifyGrid(kclManager.engineCommandManager, hidden)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
event.modelingMachineActorSend({
|
|
||||||
type: 'Set context',
|
|
||||||
data: {
|
|
||||||
streamDimensions: {
|
|
||||||
streamWidth: width,
|
|
||||||
streamHeight: height,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async [EngineStreamTransition.Resume]({ context, event }) {
|
|
||||||
// engineCommandManager.engineConnection?.reattachMediaStream()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).createMachine({
|
|
||||||
context: (initial) => initial.input,
|
|
||||||
initial: EngineStreamState.Off,
|
|
||||||
states: {
|
|
||||||
[EngineStreamState.Off]: {
|
|
||||||
on: {
|
|
||||||
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
|
||||||
target: EngineStreamState.On,
|
|
||||||
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[EngineStreamState.On]: {
|
|
||||||
on: {
|
|
||||||
[EngineStreamTransition.SetMediaStream]: {
|
|
||||||
target: EngineStreamState.On,
|
|
||||||
actions: [
|
|
||||||
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
[EngineStreamTransition.Play]: {
|
|
||||||
target: EngineStreamState.Playing,
|
|
||||||
actions: [
|
|
||||||
{ type: EngineStreamTransition.Play, params: { zoomToFit: true } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[EngineStreamState.Playing]: {
|
|
||||||
on: {
|
|
||||||
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
|
||||||
target: EngineStreamState.Playing,
|
|
||||||
reenter: true,
|
|
||||||
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
|
||||||
},
|
|
||||||
[EngineStreamTransition.Pause]: {
|
|
||||||
target: EngineStreamState.Paused,
|
|
||||||
actions: [{ type: EngineStreamTransition.Pause }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[EngineStreamState.Paused]: {
|
|
||||||
on: {
|
|
||||||
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
|
||||||
target: EngineStreamState.Resuming,
|
|
||||||
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[EngineStreamState.Resuming]: {
|
|
||||||
on: {
|
|
||||||
[EngineStreamTransition.SetMediaStream]: {
|
|
||||||
target: EngineStreamState.Resuming,
|
|
||||||
actions: [
|
|
||||||
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
[EngineStreamTransition.Play]: {
|
|
||||||
target: EngineStreamState.Playing,
|
|
||||||
actions: [
|
|
||||||
{ type: EngineStreamTransition.Play, params: { zoomToFit: false } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default createActorContext(engineStreamMachine)
|
|
@ -2,7 +2,7 @@ import { executeAst, lintAst } from 'lang/langHelpers'
|
|||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { EngineCommandManager, CommandLogType } from './std/engineConnection'
|
import { EngineCommandManager } from './std/engineConnection'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||||
|
|
||||||
@ -290,9 +290,15 @@ export class KclManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await sceneInfra.camControls.centerModelRelativeToPanes({
|
await this.engineCommandManager.sendSceneCommand({
|
||||||
zoomToFit: true,
|
type: 'modeling_cmd_req',
|
||||||
zoomObjectId,
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'zoom_to_fit',
|
||||||
|
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
|
||||||
|
padding: 0.1, // padding around the objects
|
||||||
|
animated: false, // don't animate the zoom for now
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -322,7 +328,7 @@ export class KclManager {
|
|||||||
this.ast = { ...ast }
|
this.ast = { ...ast }
|
||||||
this._executeCallback()
|
this._executeCallback()
|
||||||
this.engineCommandManager.addCommandLog({
|
this.engineCommandManager.addCommandLog({
|
||||||
type: CommandLogType.ExecutionDone,
|
type: 'execution-done',
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -6,14 +6,17 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { editorManager } from 'lib/singletons'
|
import { editorManager } from 'lib/singletons'
|
||||||
import { Annotation, Transaction } from '@codemirror/state'
|
import { Annotation, Transaction } from '@codemirror/state'
|
||||||
import { KeyBinding } from '@codemirror/view'
|
import { EditorView, KeyBinding } from '@codemirror/view'
|
||||||
import { recast, Program } from 'lang/wasm'
|
import { recast, Program } from 'lang/wasm'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
import { Compartment } from '@codemirror/state'
|
||||||
|
import { history } from '@codemirror/commands'
|
||||||
|
|
||||||
const PERSIST_CODE_KEY = 'persistCode'
|
const PERSIST_CODE_KEY = 'persistCode'
|
||||||
|
|
||||||
const codeManagerUpdateAnnotation = Annotation.define<boolean>()
|
const codeManagerUpdateAnnotation = Annotation.define<boolean>()
|
||||||
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true)
|
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true)
|
||||||
|
export const codeManagerHistoryCompartment = new Compartment()
|
||||||
|
|
||||||
export default class CodeManager {
|
export default class CodeManager {
|
||||||
private _code: string = bracket
|
private _code: string = bracket
|
||||||
@ -90,9 +93,12 @@ export default class CodeManager {
|
|||||||
/**
|
/**
|
||||||
* Update the code in the editor.
|
* Update the code in the editor.
|
||||||
*/
|
*/
|
||||||
updateCodeEditor(code: string): void {
|
updateCodeEditor(code: string, clearHistory?: boolean): void {
|
||||||
this.code = code
|
this.code = code
|
||||||
if (editorManager.editorView) {
|
if (editorManager.editorView) {
|
||||||
|
if (clearHistory) {
|
||||||
|
clearCodeMirrorHistory(editorManager.editorView)
|
||||||
|
}
|
||||||
editorManager.editorView.dispatch({
|
editorManager.editorView.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: 0,
|
from: 0,
|
||||||
@ -101,7 +107,7 @@ export default class CodeManager {
|
|||||||
},
|
},
|
||||||
annotations: [
|
annotations: [
|
||||||
codeManagerUpdateEvent,
|
codeManagerUpdateEvent,
|
||||||
Transaction.addToHistory.of(true),
|
Transaction.addToHistory.of(!clearHistory),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -110,11 +116,11 @@ export default class CodeManager {
|
|||||||
/**
|
/**
|
||||||
* Update the code, state, and the code the code mirror editor sees.
|
* Update the code, state, and the code the code mirror editor sees.
|
||||||
*/
|
*/
|
||||||
updateCodeStateEditor(code: string): void {
|
updateCodeStateEditor(code: string, clearHistory?: boolean): void {
|
||||||
if (this._code !== code) {
|
if (this._code !== code) {
|
||||||
this.code = code
|
this.code = code
|
||||||
this.#updateState(code)
|
this.#updateState(code)
|
||||||
this.updateCodeEditor(code)
|
this.updateCodeEditor(code, clearHistory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,3 +173,17 @@ function safeLSSetItem(key: string, value: string) {
|
|||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
localStorage?.setItem(key, value)
|
localStorage?.setItem(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearCodeMirrorHistory(view: EditorView) {
|
||||||
|
// Clear history
|
||||||
|
view.dispatch({
|
||||||
|
effects: [codeManagerHistoryCompartment.reconfigure([])],
|
||||||
|
annotations: [codeManagerUpdateEvent],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add history back
|
||||||
|
view.dispatch({
|
||||||
|
effects: [codeManagerHistoryCompartment.reconfigure([history()])],
|
||||||
|
annotations: [codeManagerUpdateEvent],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
Expr,
|
Expr,
|
||||||
Literal,
|
Literal,
|
||||||
|
LiteralValue,
|
||||||
PipeSubstitution,
|
PipeSubstitution,
|
||||||
Identifier,
|
Identifier,
|
||||||
ArrayExpression,
|
ArrayExpression,
|
||||||
@ -18,6 +19,7 @@ import {
|
|||||||
ProgramMemory,
|
ProgramMemory,
|
||||||
SourceRange,
|
SourceRange,
|
||||||
sketchFromKclValue,
|
sketchFromKclValue,
|
||||||
|
isPathToNodeNumber,
|
||||||
} from './wasm'
|
} from './wasm'
|
||||||
import {
|
import {
|
||||||
isNodeSafeToReplacePath,
|
isNodeSafeToReplacePath,
|
||||||
@ -525,6 +527,60 @@ export function sketchOnExtrudedFace(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify the AST to create a new sketch using the variable declaration
|
||||||
|
* of an offset plane. The new sketch just has to come after the offset
|
||||||
|
* plane declaration.
|
||||||
|
*/
|
||||||
|
export function sketchOnOffsetPlane(
|
||||||
|
node: Node<Program>,
|
||||||
|
offsetPathToNode: PathToNode
|
||||||
|
) {
|
||||||
|
let _node = { ...node }
|
||||||
|
|
||||||
|
// Find the offset plane declaration
|
||||||
|
const offsetPlaneDeclarator = getNodeFromPath<VariableDeclarator>(
|
||||||
|
_node,
|
||||||
|
offsetPathToNode,
|
||||||
|
'VariableDeclarator',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
if (err(offsetPlaneDeclarator)) return offsetPlaneDeclarator
|
||||||
|
const { node: offsetPlaneNode } = offsetPlaneDeclarator
|
||||||
|
const offsetPlaneName = offsetPlaneNode.id.name
|
||||||
|
|
||||||
|
// Create a new sketch declaration
|
||||||
|
const newSketchName = findUniqueName(
|
||||||
|
node,
|
||||||
|
KCL_DEFAULT_CONSTANT_PREFIXES.SKETCH
|
||||||
|
)
|
||||||
|
const newSketch = createVariableDeclaration(
|
||||||
|
newSketchName,
|
||||||
|
createCallExpressionStdLib('startSketchOn', [
|
||||||
|
createIdentifier(offsetPlaneName),
|
||||||
|
]),
|
||||||
|
undefined,
|
||||||
|
'const'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decide where to insert the new sketch declaration
|
||||||
|
const offsetIndex = offsetPathToNode[1][0]
|
||||||
|
|
||||||
|
if (!isPathToNodeNumber(offsetIndex)) {
|
||||||
|
return new Error('Expected offsetIndex to be a number')
|
||||||
|
}
|
||||||
|
// and insert it
|
||||||
|
_node.body.splice(offsetIndex + 1, 0, newSketch)
|
||||||
|
const newPathToNode = structuredClone(offsetPathToNode)
|
||||||
|
newPathToNode[1][0] = offsetIndex + 1
|
||||||
|
|
||||||
|
// Return the modified AST and the path to the new sketch declaration
|
||||||
|
return {
|
||||||
|
modifiedAst: _node,
|
||||||
|
pathToNode: newPathToNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getLastIndex = (pathToNode: PathToNode): number =>
|
export const getLastIndex = (pathToNode: PathToNode): number =>
|
||||||
splitPathAtLastIndex(pathToNode).index
|
splitPathAtLastIndex(pathToNode).index
|
||||||
|
|
||||||
@ -573,7 +629,7 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
|
|||||||
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLiteral(value: string | number): Node<Literal> {
|
export function createLiteral(value: LiteralValue): Node<Literal> {
|
||||||
return {
|
return {
|
||||||
type: 'Literal',
|
type: 'Literal',
|
||||||
start: 0,
|
start: 0,
|
||||||
|
@ -77,22 +77,30 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
|||||||
code.indexOf(expectedExtrudeSnippet),
|
code.indexOf(expectedExtrudeSnippet),
|
||||||
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
|
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
|
||||||
]
|
]
|
||||||
const expedtedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
|
const expectedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
|
||||||
const expedtedExtrudeNodeResult = getNodeFromPath<VariableDeclarator>(
|
const expectedExtrudeNodeResult = getNodeFromPath<
|
||||||
ast,
|
VariableDeclarator | CallExpression
|
||||||
expedtedExtrudePath
|
>(ast, expectedExtrudePath)
|
||||||
)
|
if (err(expectedExtrudeNodeResult)) {
|
||||||
if (err(expedtedExtrudeNodeResult)) {
|
return expectedExtrudeNodeResult
|
||||||
return expedtedExtrudeNodeResult
|
|
||||||
}
|
}
|
||||||
const expectedExtrudeNode = expedtedExtrudeNodeResult.node
|
const expectedExtrudeNode = expectedExtrudeNodeResult.node
|
||||||
const init = expectedExtrudeNode.init
|
|
||||||
if (init.type !== 'CallExpression' && init.type !== 'PipeExpression') {
|
// check whether extrude is in the sketch pipe
|
||||||
return new Error(
|
const extrudeInSketchPipe = expectedExtrudeNode.type === 'CallExpression'
|
||||||
'Expected extrude expression is not a CallExpression or PipeExpression'
|
if (extrudeInSketchPipe) {
|
||||||
)
|
return expectedExtrudeNode
|
||||||
}
|
}
|
||||||
return init
|
if (!extrudeInSketchPipe) {
|
||||||
|
const init = expectedExtrudeNode.init
|
||||||
|
if (init.type !== 'CallExpression' && init.type !== 'PipeExpression') {
|
||||||
|
return new Error(
|
||||||
|
'Expected extrude expression is not a CallExpression or PipeExpression'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return init
|
||||||
|
}
|
||||||
|
return new Error('Expected extrude expression not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ast
|
// ast
|
||||||
@ -160,6 +168,23 @@ extrude001 = extrude(-15, sketch001)`
|
|||||||
expectedExtrudeSnippet
|
expectedExtrudeSnippet
|
||||||
)
|
)
|
||||||
}, 5_000)
|
}, 5_000)
|
||||||
|
it('should return the correct paths when extrusion occurs within the sketch pipe', async () => {
|
||||||
|
const code = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-10, 10], %)
|
||||||
|
|> line([20, 0], %)
|
||||||
|
|> line([0, -20], %)
|
||||||
|
|> line([-20, 0], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(15, %)`
|
||||||
|
const selectedSegmentSnippet = `line([20, 0], %)`
|
||||||
|
const expectedExtrudeSnippet = `extrude(15, %)`
|
||||||
|
await runGetPathToExtrudeForSegmentSelectionTest(
|
||||||
|
code,
|
||||||
|
selectedSegmentSnippet,
|
||||||
|
expectedExtrudeSnippet
|
||||||
|
)
|
||||||
|
}, 5_000)
|
||||||
it('should return the correct paths for a valid selection and extrusion in case of several extrusions and sketches', async () => {
|
it('should return the correct paths for a valid selection and extrusion in case of several extrusions and sketches', async () => {
|
||||||
const code = `sketch001 = startSketchOn('XY')
|
const code = `sketch001 = startSketchOn('XY')
|
||||||
|> startProfileAt([-30, 30], %)
|
|> startProfileAt([-30, 30], %)
|
||||||
@ -296,6 +321,34 @@ extrude001 = extrude(-15, sketch001)`
|
|||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
extrude001 = extrude(-15, sketch001)
|
extrude001 = extrude(-15, sketch001)
|
||||||
|
|> fillet({ radius: 3, tags: [seg01] }, %)`
|
||||||
|
|
||||||
|
await runModifyAstCloneWithFilletAndTag(
|
||||||
|
code,
|
||||||
|
segmentSnippets,
|
||||||
|
radiusValue,
|
||||||
|
expectedCode
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should add a fillet to the sketch pipe', async () => {
|
||||||
|
const code = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-10, 10], %)
|
||||||
|
|> line([20, 0], %)
|
||||||
|
|> line([0, -20], %)
|
||||||
|
|> line([-20, 0], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(-15, %)`
|
||||||
|
const segmentSnippets = ['line([0, -20], %)']
|
||||||
|
const radiusValue = 3
|
||||||
|
const expectedCode = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-10, 10], %)
|
||||||
|
|> line([20, 0], %)
|
||||||
|
|> line([0, -20], %, $seg01)
|
||||||
|
|> line([-20, 0], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(-15, %)
|
||||||
|> fillet({ radius: 3, tags: [seg01] }, %)`
|
|> fillet({ radius: 3, tags: [seg01] }, %)`
|
||||||
|
|
||||||
await runModifyAstCloneWithFilletAndTag(
|
await runModifyAstCloneWithFilletAndTag(
|
||||||
|
@ -146,7 +146,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
|
|
||||||
// Modify the extrude expression to include this fillet expression
|
// Modify the extrude expression to include this fillet expression
|
||||||
// CallExpression - no fillet
|
// CallExpression - no fillet
|
||||||
// PipeExpression - fillet exists
|
// PipeExpression - fillet exists or extrude in sketch pipe
|
||||||
|
|
||||||
let pathToFilletNode: PathToNode = []
|
let pathToFilletNode: PathToNode = []
|
||||||
|
|
||||||
@ -167,15 +167,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
)
|
)
|
||||||
pathToFilletNodes.push(pathToFilletNode)
|
pathToFilletNodes.push(pathToFilletNode)
|
||||||
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
||||||
// 2. case when fillet exists
|
// 2. case when fillet exists or extrude in sketch pipe
|
||||||
|
|
||||||
const existingFilletCall = extrudeDeclarator.init.body.find((node) => {
|
|
||||||
return node.type === 'CallExpression' && node.callee.name === 'fillet'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') {
|
|
||||||
return new Error('Fillet CallExpression not found.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// mutate the extrude node with the new fillet call
|
// mutate the extrude node with the new fillet call
|
||||||
extrudeDeclarator.init.body.push(filletCall)
|
extrudeDeclarator.init.body.push(filletCall)
|
||||||
@ -317,14 +309,14 @@ function locateExtrudeDeclarator(
|
|||||||
node: Program,
|
node: Program,
|
||||||
pathToExtrudeNode: PathToNode
|
pathToExtrudeNode: PathToNode
|
||||||
): { extrudeDeclarator: VariableDeclarator } | Error {
|
): { extrudeDeclarator: VariableDeclarator } | Error {
|
||||||
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
|
const nodeOfExtrudeCall = getNodeFromPath<VariableDeclaration>(
|
||||||
node,
|
node,
|
||||||
pathToExtrudeNode,
|
pathToExtrudeNode,
|
||||||
'VariableDeclaration'
|
'VariableDeclaration'
|
||||||
)
|
)
|
||||||
if (err(extrudeChunk)) return extrudeChunk
|
if (err(nodeOfExtrudeCall)) return nodeOfExtrudeCall
|
||||||
|
|
||||||
const { node: extrudeVarDecl } = extrudeChunk
|
const { node: extrudeVarDecl } = nodeOfExtrudeCall
|
||||||
const extrudeDeclarator = extrudeVarDecl.declarations[0]
|
const extrudeDeclarator = extrudeVarDecl.declarations[0]
|
||||||
if (!extrudeDeclarator) {
|
if (!extrudeDeclarator) {
|
||||||
return new Error('Extrude Declarator not found.')
|
return new Error('Extrude Declarator not found.')
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { parse, recast, initPromise, PathToNode } from './wasm'
|
import { parse, recast, initPromise, PathToNode, Identifier } from './wasm'
|
||||||
import {
|
import {
|
||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
hasSketchPipeBeenExtruded,
|
hasSketchPipeBeenExtruded,
|
||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
traverse,
|
traverse,
|
||||||
|
getNodeFromPath,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import {
|
import {
|
||||||
@ -266,6 +267,86 @@ describe('testing getNodePathFromSourceRange', () => {
|
|||||||
])
|
])
|
||||||
expect(selectWholeThing).toEqual(expected)
|
expect(selectWholeThing).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('finds the node in if-else condition', () => {
|
||||||
|
const code = `y = 0
|
||||||
|
x = if x > y {
|
||||||
|
x + 1
|
||||||
|
} else {
|
||||||
|
y
|
||||||
|
}`
|
||||||
|
const searchLn = `x > y`
|
||||||
|
const sourceIndex = code.indexOf(searchLn)
|
||||||
|
const ast = parse(code)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
|
||||||
|
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||||
|
expect(result).toEqual([
|
||||||
|
['body', ''],
|
||||||
|
[1, 'index'],
|
||||||
|
['declarations', 'VariableDeclaration'],
|
||||||
|
[0, 'index'],
|
||||||
|
['init', ''],
|
||||||
|
['cond', 'IfExpression'],
|
||||||
|
['left', 'BinaryExpression'],
|
||||||
|
])
|
||||||
|
const _node = getNodeFromPath<Identifier>(ast, result)
|
||||||
|
if (err(_node)) throw _node
|
||||||
|
expect(_node.node.type).toEqual('Identifier')
|
||||||
|
expect(_node.node.name).toEqual('x')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds the node in if-else then', () => {
|
||||||
|
const code = `y = 0
|
||||||
|
x = if x > y {
|
||||||
|
x + 1
|
||||||
|
} else {
|
||||||
|
y
|
||||||
|
}`
|
||||||
|
const searchLn = `x + 1`
|
||||||
|
const sourceIndex = code.indexOf(searchLn)
|
||||||
|
const ast = parse(code)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
|
||||||
|
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||||
|
expect(result).toEqual([
|
||||||
|
['body', ''],
|
||||||
|
[1, 'index'],
|
||||||
|
['declarations', 'VariableDeclaration'],
|
||||||
|
[0, 'index'],
|
||||||
|
['init', ''],
|
||||||
|
['then_val', 'IfExpression'],
|
||||||
|
['body', 'IfExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
['expression', 'ExpressionStatement'],
|
||||||
|
['left', 'BinaryExpression'],
|
||||||
|
])
|
||||||
|
const _node = getNodeFromPath<Identifier>(ast, result)
|
||||||
|
if (err(_node)) throw _node
|
||||||
|
expect(_node.node.type).toEqual('Identifier')
|
||||||
|
expect(_node.node.name).toEqual('x')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds the node in import statement item', () => {
|
||||||
|
const code = `import foo, bar as baz from 'thing.kcl'`
|
||||||
|
const searchLn = `bar`
|
||||||
|
const sourceIndex = code.indexOf(searchLn)
|
||||||
|
const ast = parse(code)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
|
||||||
|
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
||||||
|
expect(result).toEqual([
|
||||||
|
['body', ''],
|
||||||
|
[0, 'index'],
|
||||||
|
['items', 'ImportStatement'],
|
||||||
|
[1, 'index'],
|
||||||
|
['name', 'ImportItem'],
|
||||||
|
])
|
||||||
|
const _node = getNodeFromPath<Identifier>(ast, result)
|
||||||
|
if (err(_node)) throw _node
|
||||||
|
expect(_node.node.type).toEqual('Identifier')
|
||||||
|
expect(_node.node.name).toEqual('bar')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testing doesPipeHave', () => {
|
describe('testing doesPipeHave', () => {
|
||||||
@ -449,14 +530,25 @@ describe('Testing hasSketchPipeBeenExtruded', () => {
|
|||||||
|> line([-17.67, 0.85], %)
|
|> line([-17.67, 0.85], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
extrude001 = extrude(10, sketch001)
|
extrude001 = extrude(10, sketch001)
|
||||||
sketch002 = startSketchOn(extrude001, $seg01)
|
sketch002 = startSketchOn(extrude001, seg01)
|
||||||
|> startProfileAt([-12.94, 6.6], %)
|
|> startProfileAt([-12.94, 6.6], %)
|
||||||
|> line([2.45, -0.2], %)
|
|> line([2.45, -0.2], %)
|
||||||
|> line([-2, -1.25], %)
|
|> line([-2, -1.25], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|
sketch003 = startSketchOn(extrude001, 'END')
|
||||||
|
|> startProfileAt([8.14, 2.8], %)
|
||||||
|
|> line([-1.24, 4.39], %)
|
||||||
|
|> line([3.79, 1.91], %)
|
||||||
|
|> line([1.77, -2.95], %)
|
||||||
|
|> line([3.12, 1.74], %)
|
||||||
|
|> line([1.91, -4.09], %)
|
||||||
|
|> line([-5.6, -2.75], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(3.14, %)
|
||||||
`
|
`
|
||||||
it('finds sketch001 pipe to be extruded', async () => {
|
it('identifies sketch001 pipe as extruded (extrusion after pipe)', async () => {
|
||||||
const ast = parse(exampleCode)
|
const ast = parse(exampleCode)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const lineOfInterest = `line([4.99, -0.46], %, $seg01)`
|
const lineOfInterest = `line([4.99, -0.46], %, $seg01)`
|
||||||
@ -471,7 +563,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
|
|||||||
)
|
)
|
||||||
expect(extruded).toBeTruthy()
|
expect(extruded).toBeTruthy()
|
||||||
})
|
})
|
||||||
it('find sketch002 NOT pipe to be extruded', async () => {
|
it('identifies sketch002 pipe as not extruded', async () => {
|
||||||
const ast = parse(exampleCode)
|
const ast = parse(exampleCode)
|
||||||
if (err(ast)) throw ast
|
if (err(ast)) throw ast
|
||||||
const lineOfInterest = `line([2.45, -0.2], %)`
|
const lineOfInterest = `line([2.45, -0.2], %)`
|
||||||
@ -486,6 +578,21 @@ sketch002 = startSketchOn(extrude001, $seg01)
|
|||||||
)
|
)
|
||||||
expect(extruded).toBeFalsy()
|
expect(extruded).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
it('identifies sketch003 pipe as extruded (extrusion within pipe)', async () => {
|
||||||
|
const ast = parse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const lineOfInterest = `|> line([3.12, 1.74], %)`
|
||||||
|
const characterIndex =
|
||||||
|
exampleCode.indexOf(lineOfInterest) + lineOfInterest.length
|
||||||
|
const extruded = hasSketchPipeBeenExtruded(
|
||||||
|
{
|
||||||
|
range: [characterIndex, characterIndex],
|
||||||
|
type: 'default',
|
||||||
|
},
|
||||||
|
ast
|
||||||
|
)
|
||||||
|
expect(extruded).toBeTruthy()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Testing doesSceneHaveSweepableSketch', () => {
|
describe('Testing doesSceneHaveSweepableSketch', () => {
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
ProgramMemory,
|
ProgramMemory,
|
||||||
ReturnStatement,
|
ReturnStatement,
|
||||||
sketchFromKclValue,
|
sketchFromKclValue,
|
||||||
|
sketchFromKclValueOptional,
|
||||||
SourceRange,
|
SourceRange,
|
||||||
SyntaxType,
|
SyntaxType,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
@ -27,7 +28,7 @@ import {
|
|||||||
getConstraintLevelFromSourceRange,
|
getConstraintLevelFromSourceRange,
|
||||||
getConstraintType,
|
getConstraintType,
|
||||||
} from './std/sketchcombos'
|
} from './std/sketchcombos'
|
||||||
import { err } from 'lib/trap'
|
import { err, Reason } from 'lib/trap'
|
||||||
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
|
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
|
||||||
@ -317,6 +318,62 @@ function moreNodePathFromSourceRange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_node.type === 'PipeSubstitution' && isInRange) return path
|
if (_node.type === 'PipeSubstitution' && isInRange) return path
|
||||||
|
|
||||||
|
if (_node.type === 'IfExpression' && isInRange) {
|
||||||
|
const { cond, then_val, else_ifs, final_else } = _node
|
||||||
|
if (cond.start <= start && cond.end >= end) {
|
||||||
|
path.push(['cond', 'IfExpression'])
|
||||||
|
return moreNodePathFromSourceRange(cond, sourceRange, path)
|
||||||
|
}
|
||||||
|
if (then_val.start <= start && then_val.end >= end) {
|
||||||
|
path.push(['then_val', 'IfExpression'])
|
||||||
|
path.push(['body', 'IfExpression'])
|
||||||
|
return getNodePathFromSourceRange(then_val, sourceRange, path)
|
||||||
|
}
|
||||||
|
for (let i = 0; i < else_ifs.length; i++) {
|
||||||
|
const else_if = else_ifs[i]
|
||||||
|
if (else_if.start <= start && else_if.end >= end) {
|
||||||
|
path.push(['else_ifs', 'IfExpression'])
|
||||||
|
path.push([i, 'index'])
|
||||||
|
const { cond, then_val } = else_if
|
||||||
|
if (cond.start <= start && cond.end >= end) {
|
||||||
|
path.push(['cond', 'IfExpression'])
|
||||||
|
return moreNodePathFromSourceRange(cond, sourceRange, path)
|
||||||
|
}
|
||||||
|
path.push(['then_val', 'IfExpression'])
|
||||||
|
path.push(['body', 'IfExpression'])
|
||||||
|
return getNodePathFromSourceRange(then_val, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (final_else.start <= start && final_else.end >= end) {
|
||||||
|
path.push(['final_else', 'IfExpression'])
|
||||||
|
path.push(['body', 'IfExpression'])
|
||||||
|
return getNodePathFromSourceRange(final_else, sourceRange, path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_node.type === 'ImportStatement' && isInRange) {
|
||||||
|
const { items } = _node
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i]
|
||||||
|
if (item.start <= start && item.end >= end) {
|
||||||
|
path.push(['items', 'ImportStatement'])
|
||||||
|
path.push([i, 'index'])
|
||||||
|
if (item.name.start <= start && item.name.end >= end) {
|
||||||
|
path.push(['name', 'ImportItem'])
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (item.alias && item.alias.start <= start && item.alias.end >= end) {
|
||||||
|
path.push(['alias', 'ImportItem'])
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
console.error('not implemented: ' + node.type)
|
console.error('not implemented: ' + node.type)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
@ -790,7 +847,8 @@ export function hasExtrudeSketch({
|
|||||||
const varName = varDec.declarations[0].id.name
|
const varName = varDec.declarations[0].id.name
|
||||||
const varValue = programMemory?.get(varName)
|
const varValue = programMemory?.get(varName)
|
||||||
return (
|
return (
|
||||||
varValue?.type === 'Solid' || !err(sketchFromKclValue(varValue, varName))
|
varValue?.type === 'Solid' ||
|
||||||
|
!(sketchFromKclValueOptional(varValue, varName) instanceof Reason)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -871,7 +929,11 @@ export function findUsesOfTagInPipe(
|
|||||||
|
|
||||||
export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
||||||
const path = getNodePathFromSourceRange(ast, selection.range)
|
const path = getNodePathFromSourceRange(ast, selection.range)
|
||||||
const _node = getNodeFromPath<PipeExpression>(ast, path, 'PipeExpression')
|
const _node = getNodeFromPath<Node<PipeExpression>>(
|
||||||
|
ast,
|
||||||
|
path,
|
||||||
|
'PipeExpression'
|
||||||
|
)
|
||||||
if (err(_node)) return false
|
if (err(_node)) return false
|
||||||
const { node: pipeExpression } = _node
|
const { node: pipeExpression } = _node
|
||||||
if (pipeExpression.type !== 'PipeExpression') return false
|
if (pipeExpression.type !== 'PipeExpression') return false
|
||||||
@ -884,19 +946,33 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
|||||||
const varDec = _varDec.node
|
const varDec = _varDec.node
|
||||||
if (varDec.type !== 'VariableDeclarator') return false
|
if (varDec.type !== 'VariableDeclarator') return false
|
||||||
let extruded = false
|
let extruded = false
|
||||||
traverse(ast as any, {
|
// option 1: extrude or revolve is called in the sketch pipe
|
||||||
|
traverse(pipeExpression, {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
if (
|
if (
|
||||||
node.type === 'CallExpression' &&
|
node.type === 'CallExpression' &&
|
||||||
node.callee.type === 'Identifier' &&
|
(node.callee.name === 'extrude' || node.callee.name === 'revolve')
|
||||||
(node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
|
|
||||||
node.arguments?.[1]?.type === 'Identifier' &&
|
|
||||||
node.arguments[1].name === varDec.id.name
|
|
||||||
) {
|
) {
|
||||||
extruded = true
|
extruded = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
// option 2: extrude or revolve is called in the separate pipe
|
||||||
|
if (!extruded) {
|
||||||
|
traverse(ast as any, {
|
||||||
|
enter(node) {
|
||||||
|
if (
|
||||||
|
node.type === 'CallExpression' &&
|
||||||
|
node.callee.type === 'Identifier' &&
|
||||||
|
(node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
|
||||||
|
node.arguments?.[1]?.type === 'Identifier' &&
|
||||||
|
node.arguments[1].name === varDec.id.name
|
||||||
|
) {
|
||||||
|
extruded = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
return extruded
|
return extruded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,12 +98,22 @@ sketch004 = startSketchOn(extrude003, seg02)
|
|||||||
|> close(%)
|
|> close(%)
|
||||||
extrude004 = extrude(3, sketch004)
|
extrude004 = extrude(3, sketch004)
|
||||||
`
|
`
|
||||||
|
const exampleCodeOffsetPlanes = `
|
||||||
|
offsetPlane001 = offsetPlane("XY", 20)
|
||||||
|
offsetPlane002 = offsetPlane("XZ", -50)
|
||||||
|
offsetPlane003 = offsetPlane("YZ", 10)
|
||||||
|
|
||||||
|
sketch002 = startSketchOn(offsetPlane001)
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([6.78, 15.01], %)
|
||||||
|
`
|
||||||
|
|
||||||
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
|
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
|
||||||
const codeToWriteCacheFor = {
|
const codeToWriteCacheFor = {
|
||||||
exampleCode1,
|
exampleCode1,
|
||||||
sketchOnFaceOnFaceEtc,
|
sketchOnFaceOnFaceEtc,
|
||||||
exampleCodeNo3D,
|
exampleCodeNo3D,
|
||||||
|
exampleCodeOffsetPlanes,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
type CodeKey = keyof typeof codeToWriteCacheFor
|
type CodeKey = keyof typeof codeToWriteCacheFor
|
||||||
@ -165,6 +175,52 @@ afterAll(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('testing createArtifactGraph', () => {
|
describe('testing createArtifactGraph', () => {
|
||||||
|
describe('code with offset planes and a sketch:', () => {
|
||||||
|
let ast: Program
|
||||||
|
let theMap: ReturnType<typeof createArtifactGraph>
|
||||||
|
|
||||||
|
it('setup', () => {
|
||||||
|
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||||
|
const {
|
||||||
|
orderedCommands,
|
||||||
|
responseMap,
|
||||||
|
ast: _ast,
|
||||||
|
} = getCommands('exampleCodeOffsetPlanes')
|
||||||
|
ast = _ast
|
||||||
|
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`there should be one sketch`, () => {
|
||||||
|
const sketches = [...filterArtifacts({ types: ['path'] }, theMap)].map(
|
||||||
|
(path) => expandPath(path[1], theMap)
|
||||||
|
)
|
||||||
|
expect(sketches).toHaveLength(1)
|
||||||
|
sketches.forEach((path) => {
|
||||||
|
if (err(path)) throw path
|
||||||
|
expect(path.type).toBe('path')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`there should be three offsetPlanes`, () => {
|
||||||
|
const offsetPlanes = [
|
||||||
|
...filterArtifacts({ types: ['plane'] }, theMap),
|
||||||
|
].map((plane) => expandPlane(plane[1], theMap))
|
||||||
|
expect(offsetPlanes).toHaveLength(3)
|
||||||
|
offsetPlanes.forEach((path) => {
|
||||||
|
expect(path.type).toBe('plane')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`Only one offset plane should have a path`, () => {
|
||||||
|
const offsetPlanes = [
|
||||||
|
...filterArtifacts({ types: ['plane'] }, theMap),
|
||||||
|
].map((plane) => expandPlane(plane[1], theMap))
|
||||||
|
const offsetPlaneWithPaths = offsetPlanes.filter(
|
||||||
|
(plane) => plane.paths.length
|
||||||
|
)
|
||||||
|
expect(offsetPlaneWithPaths).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
describe('code with an extrusion, fillet and sketch of face:', () => {
|
describe('code with an extrusion, fillet and sketch of face:', () => {
|
||||||
let ast: Program
|
let ast: Program
|
||||||
let theMap: ReturnType<typeof createArtifactGraph>
|
let theMap: ReturnType<typeof createArtifactGraph>
|
||||||
|
@ -249,7 +249,20 @@ export function getArtifactsToUpdate({
|
|||||||
const cmd = command.cmd
|
const cmd = command.cmd
|
||||||
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
|
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
|
||||||
if (!response) return returnArr
|
if (!response) return returnArr
|
||||||
if (cmd.type === 'enable_sketch_mode') {
|
if (cmd.type === 'make_plane' && range[1] !== 0) {
|
||||||
|
// If we're calling `make_plane` and the code range doesn't end at `0`
|
||||||
|
// it's not a default plane, but a custom one from the offsetPlane standard library function
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
artifact: {
|
||||||
|
type: 'plane',
|
||||||
|
pathIds: [],
|
||||||
|
codeRef: { range, pathToNode },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else if (cmd.type === 'enable_sketch_mode') {
|
||||||
const plane = getArtifact(currentPlaneId)
|
const plane = getArtifact(currentPlaneId)
|
||||||
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
|
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
|
||||||
const codeRef =
|
const codeRef =
|
||||||
|
@ -406,14 +406,13 @@ class EngineConnection extends EventTarget {
|
|||||||
default:
|
default:
|
||||||
if (this.isConnecting()) break
|
if (this.isConnecting()) break
|
||||||
// Means we never could do an initial connection. Reconnect everything.
|
// Means we never could do an initial connection. Reconnect everything.
|
||||||
if (!this.pingPongSpan.ping)
|
if (!this.pingPongSpan.ping) this.connect().catch(reportRejection)
|
||||||
this.connect({ reconnect: false }).catch(reportRejection)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.connect({ reconnect: false })
|
this.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SHOULD ONLY BE USED FOR VITESTS
|
// SHOULD ONLY BE USED FOR VITESTS
|
||||||
@ -524,9 +523,7 @@ class EngineConnection extends EventTarget {
|
|||||||
this.idleMode = opts?.idleMode ?? false
|
this.idleMode = opts?.idleMode ?? false
|
||||||
clearInterval(this.pingIntervalId)
|
clearInterval(this.pingIntervalId)
|
||||||
|
|
||||||
this.disconnectAll()
|
if (opts?.idleMode) {
|
||||||
|
|
||||||
if (this.idleMode) {
|
|
||||||
this.state = {
|
this.state = {
|
||||||
type: EngineConnectionStateType.Disconnecting,
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
value: {
|
value: {
|
||||||
@ -545,6 +542,8 @@ class EngineConnection extends EventTarget {
|
|||||||
type: DisconnectingType.Quit,
|
type: DisconnectingType.Quit,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.disconnectAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -554,7 +553,7 @@ class EngineConnection extends EventTarget {
|
|||||||
* This will attempt the full handshake, and retry if the connection
|
* This will attempt the full handshake, and retry if the connection
|
||||||
* did not establish.
|
* did not establish.
|
||||||
*/
|
*/
|
||||||
connect(args: { reconnect: boolean }): Promise<void> {
|
connect(reconnecting?: boolean): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (this.isConnecting() || this.isReady()) {
|
if (this.isConnecting() || this.isReady()) {
|
||||||
return
|
return
|
||||||
@ -1166,7 +1165,7 @@ class EngineConnection extends EventTarget {
|
|||||||
this.websocket.addEventListener('message', this.onWebSocketMessage)
|
this.websocket.addEventListener('message', this.onWebSocketMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.reconnect) {
|
if (reconnecting) {
|
||||||
createWebSocketConnection()
|
createWebSocketConnection()
|
||||||
} else {
|
} else {
|
||||||
this.onNetworkStatusReady = () => {
|
this.onNetworkStatusReady = () => {
|
||||||
@ -1179,32 +1178,6 @@ class EngineConnection extends EventTarget {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async reattachMediaStream() {
|
|
||||||
return this.pc
|
|
||||||
?.createOffer({ iceRestart: true })
|
|
||||||
.then((offer: RTCSessionDescriptionInit) => {
|
|
||||||
this.state = {
|
|
||||||
type: EngineConnectionStateType.Connecting,
|
|
||||||
value: {
|
|
||||||
type: ConnectingType.SetLocalDescription,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return this.pc?.setLocalDescription(offer).then(() => {
|
|
||||||
this.send({
|
|
||||||
type: 'sdp_offer',
|
|
||||||
offer: offer as Models['RtcSessionDescription_type'],
|
|
||||||
})
|
|
||||||
this.state = {
|
|
||||||
type: EngineConnectionStateType.Connecting,
|
|
||||||
value: {
|
|
||||||
type: ConnectingType.OfferedSdp,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not change this back to an object or any, we should only be sending the
|
// Do not change this back to an object or any, we should only be sending the
|
||||||
// WebSocketRequest type!
|
// WebSocketRequest type!
|
||||||
unreliableSend(message: Models['WebSocketRequest_type']) {
|
unreliableSend(message: Models['WebSocketRequest_type']) {
|
||||||
@ -1256,17 +1229,8 @@ class EngineConnection extends EventTarget {
|
|||||||
this.websocket?.readyState === 3
|
this.websocket?.readyState === 3
|
||||||
|
|
||||||
if (closedPc && closedUDC && closedWS) {
|
if (closedPc && closedUDC && closedWS) {
|
||||||
if (!this.idleMode) {
|
// Do not notify the rest of the program that we have cut off anything.
|
||||||
// Do not notify the rest of the program that we have cut off anything.
|
this.state = { type: EngineConnectionStateType.Disconnected }
|
||||||
this.state = { type: EngineConnectionStateType.Disconnected }
|
|
||||||
} else {
|
|
||||||
this.state = {
|
|
||||||
type: EngineConnectionStateType.Disconnecting,
|
|
||||||
value: {
|
|
||||||
type: DisconnectingType.Pause,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1291,40 +1255,27 @@ export interface Subscription<T extends ModelTypes> {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CommandLogType {
|
|
||||||
SendModeling = 'send-modeling',
|
|
||||||
SendScene = 'send-scene',
|
|
||||||
ReceiveReliable = 'receive-reliable',
|
|
||||||
ExecutionDone = 'execution-done',
|
|
||||||
ExportDone = 'export-done',
|
|
||||||
SetDefaultSystemProperties = 'set_default_system_properties',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommandLog =
|
export type CommandLog =
|
||||||
| {
|
| {
|
||||||
type: CommandLogType.SendModeling
|
type: 'send-modeling'
|
||||||
data: EngineCommand
|
data: EngineCommand
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: CommandLogType.SendScene
|
type: 'send-scene'
|
||||||
data: EngineCommand
|
data: EngineCommand
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: CommandLogType.ReceiveReliable
|
type: 'receive-reliable'
|
||||||
data: OkWebSocketResponseData
|
data: OkWebSocketResponseData
|
||||||
id: string
|
id: string
|
||||||
cmd_type?: string
|
cmd_type?: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: CommandLogType.ExecutionDone
|
type: 'execution-done'
|
||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: CommandLogType.ExportDone
|
type: 'export-done'
|
||||||
data: null
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: CommandLogType.SetDefaultSystemProperties
|
|
||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1680,7 +1631,11 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
|
|
||||||
switch (this.exportInfo.intent) {
|
switch (this.exportInfo.intent) {
|
||||||
case ExportIntent.Save: {
|
case ExportIntent.Save: {
|
||||||
exportSave(event.data, this.pendingExport.toastId).then(() => {
|
exportSave({
|
||||||
|
data: event.data,
|
||||||
|
fileName: this.exportInfo.name,
|
||||||
|
toastId: this.pendingExport.toastId,
|
||||||
|
}).then(() => {
|
||||||
this.pendingExport?.resolve(null)
|
this.pendingExport?.resolve(null)
|
||||||
}, this.pendingExport?.reject)
|
}, this.pendingExport?.reject)
|
||||||
break
|
break
|
||||||
@ -1735,7 +1690,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
message.request_id
|
message.request_id
|
||||||
) {
|
) {
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: CommandLogType.ReceiveReliable,
|
type: 'receive-reliable',
|
||||||
data: message.resp,
|
data: message.resp,
|
||||||
id: message?.request_id || '',
|
id: message?.request_id || '',
|
||||||
cmd_type: pending?.command?.cmd?.type,
|
cmd_type: pending?.command?.cmd?.type,
|
||||||
@ -1769,7 +1724,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
if (!command) return
|
if (!command) return
|
||||||
if (command.type === 'modeling_cmd_req')
|
if (command.type === 'modeling_cmd_req')
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: CommandLogType.ReceiveReliable,
|
type: 'receive-reliable',
|
||||||
data: {
|
data: {
|
||||||
type: 'modeling',
|
type: 'modeling',
|
||||||
data: {
|
data: {
|
||||||
@ -1811,7 +1766,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineConnection?.connect({ reconnect: false })
|
this.engineConnection?.connect()
|
||||||
}
|
}
|
||||||
this.engineConnection.addEventListener(
|
this.engineConnection.addEventListener(
|
||||||
EngineConnectionEvents.ConnectionStarted,
|
EngineConnectionEvents.ConnectionStarted,
|
||||||
@ -1873,7 +1828,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.engineConnection?.tearDown(opts)
|
this.engineConnection?.tearDown(opts)
|
||||||
this.engineConnection = undefined
|
|
||||||
|
|
||||||
// Our window.tearDown assignment causes this case to happen which is
|
// Our window.tearDown assignment causes this case to happen which is
|
||||||
// only really for tests.
|
// only really for tests.
|
||||||
@ -1881,8 +1835,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
} else if (this.engineCommandManager?.engineConnection) {
|
} else if (this.engineCommandManager?.engineConnection) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.engineCommandManager?.engineConnection?.tearDown(opts)
|
this.engineCommandManager?.engineConnection?.tearDown(opts)
|
||||||
// @ts-ignore
|
|
||||||
this.engineCommandManager.engineConnection = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async startNewSession() {
|
async startNewSession() {
|
||||||
@ -1981,7 +1933,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
) {
|
) {
|
||||||
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
|
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: CommandLogType.SendScene,
|
type: 'send-scene',
|
||||||
data: command,
|
data: command,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -2040,7 +1992,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
toastId,
|
toastId,
|
||||||
resolve: (passThrough) => {
|
resolve: (passThrough) => {
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: CommandLogType.ExportDone,
|
type: 'export-done',
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
resolve(passThrough)
|
resolve(passThrough)
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, PathToNode } from './wasm'
|
import {
|
||||||
|
Program,
|
||||||
|
PathToNode,
|
||||||
|
CallExpression,
|
||||||
|
Literal,
|
||||||
|
ArrayExpression,
|
||||||
|
BinaryExpression,
|
||||||
|
} from './wasm'
|
||||||
import { getNodeFromPath } from './queryAst'
|
import { getNodeFromPath } from './queryAst'
|
||||||
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
|
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
@ -84,3 +91,19 @@ export function isCursorInSketchCommandRange(
|
|||||||
([, artifact]) => artifact.type === 'path'
|
([, artifact]) => artifact.type === 'path'
|
||||||
)?.[0] || false
|
)?.[0] || false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCallExpression(e: any): e is CallExpression {
|
||||||
|
return e && e.type === 'CallExpression'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isArrayExpression(e: any): e is ArrayExpression {
|
||||||
|
return e && e.type === 'ArrayExpression'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLiteral(e: any): e is Literal {
|
||||||
|
return e && e.type === 'Literal'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBinaryExpression(e: any): e is BinaryExpression {
|
||||||
|
return e && e.type === 'BinaryExpression'
|
||||||
|
}
|
||||||
|
@ -32,7 +32,7 @@ import { CoreDumpManager } from 'lib/coredump'
|
|||||||
import openWindow from 'lib/openWindow'
|
import openWindow from 'lib/openWindow'
|
||||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||||
import { TEST } from 'env'
|
import { TEST } from 'env'
|
||||||
import { err } from 'lib/trap'
|
import { err, Reason } from 'lib/trap'
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
import { DeepPartial } from 'lib/types'
|
import { DeepPartial } from 'lib/types'
|
||||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||||
@ -62,6 +62,7 @@ export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
|
|||||||
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
|
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
|
||||||
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
|
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
|
||||||
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
|
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
|
||||||
|
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
|
||||||
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
||||||
|
|
||||||
export type SyntaxType =
|
export type SyntaxType =
|
||||||
@ -81,6 +82,7 @@ export type SyntaxType =
|
|||||||
| 'PipeExpression'
|
| 'PipeExpression'
|
||||||
| 'PipeSubstitution'
|
| 'PipeSubstitution'
|
||||||
| 'Literal'
|
| 'Literal'
|
||||||
|
| 'LiteralValue'
|
||||||
| 'NonCodeNode'
|
| 'NonCodeNode'
|
||||||
| 'UnaryExpression'
|
| 'UnaryExpression'
|
||||||
|
|
||||||
@ -142,6 +144,12 @@ export const parse = (code: string | Error): Node<Program> | Error => {
|
|||||||
|
|
||||||
export type PathToNode = [string | number, string][]
|
export type PathToNode = [string | number, string][]
|
||||||
|
|
||||||
|
export const isPathToNodeNumber = (
|
||||||
|
pathToNode: string | number
|
||||||
|
): pathToNode is number => {
|
||||||
|
return typeof pathToNode === 'number'
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExecState {
|
export interface ExecState {
|
||||||
memory: ProgramMemory
|
memory: ProgramMemory
|
||||||
idGenerator: IdGenerator
|
idGenerator: IdGenerator
|
||||||
@ -357,10 +365,10 @@ export class ProgramMemory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: In the future, make the parameter be a KclValue.
|
// TODO: In the future, make the parameter be a KclValue.
|
||||||
export function sketchFromKclValue(
|
export function sketchFromKclValueOptional(
|
||||||
obj: any,
|
obj: any,
|
||||||
varName: string | null
|
varName: string | null
|
||||||
): Sketch | Error {
|
): Sketch | Reason {
|
||||||
if (obj?.value?.type === 'Sketch') return obj.value
|
if (obj?.value?.type === 'Sketch') return obj.value
|
||||||
if (obj?.value?.type === 'Solid') return obj.value.sketch
|
if (obj?.value?.type === 'Solid') return obj.value.sketch
|
||||||
if (obj?.type === 'Solid') return obj.sketch
|
if (obj?.type === 'Solid') return obj.sketch
|
||||||
@ -369,15 +377,26 @@ export function sketchFromKclValue(
|
|||||||
}
|
}
|
||||||
const actualType = obj?.value?.type ?? obj?.type
|
const actualType = obj?.value?.type ?? obj?.type
|
||||||
if (actualType) {
|
if (actualType) {
|
||||||
console.log(obj)
|
return new Reason(
|
||||||
return new Error(
|
|
||||||
`Expected ${varName} to be a sketch or solid, but it was ${actualType} instead.`
|
`Expected ${varName} to be a sketch or solid, but it was ${actualType} instead.`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return new Error(`Expected ${varName} to be a sketch, but it wasn't.`)
|
return new Reason(`Expected ${varName} to be a sketch, but it wasn't.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: In the future, make the parameter be a KclValue.
|
||||||
|
export function sketchFromKclValue(
|
||||||
|
obj: any,
|
||||||
|
varName: string | null
|
||||||
|
): Sketch | Error {
|
||||||
|
const result = sketchFromKclValueOptional(obj, varName)
|
||||||
|
if (result instanceof Reason) {
|
||||||
|
return result.toError()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export const executor = async (
|
export const executor = async (
|
||||||
node: Node<Program>,
|
node: Node<Program>,
|
||||||
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
programMemory: ProgramMemory | Error = ProgramMemory.empty(),
|
||||||
|
@ -68,7 +68,16 @@ const save_ = async (file: ModelingAppFile, toastId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Saves files locally from an export call.
|
// Saves files locally from an export call.
|
||||||
export async function exportSave(data: ArrayBuffer, toastId: string) {
|
// We override the file's name with one passed in from the client side.
|
||||||
|
export async function exportSave({
|
||||||
|
data,
|
||||||
|
fileName,
|
||||||
|
toastId,
|
||||||
|
}: {
|
||||||
|
data: ArrayBuffer
|
||||||
|
fileName: string
|
||||||
|
toastId: string
|
||||||
|
}) {
|
||||||
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
|
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
|
||||||
let uintArray = new Uint8Array(data)
|
let uintArray = new Uint8Array(data)
|
||||||
|
|
||||||
@ -80,9 +89,10 @@ export async function exportSave(data: ArrayBuffer, toastId: string) {
|
|||||||
zip.file(file.name, new Uint8Array(file.contents), { binary: true })
|
zip.file(file.name, new Uint8Array(file.contents), { binary: true })
|
||||||
}
|
}
|
||||||
return zip.generateAsync({ type: 'array' }).then((contents) => {
|
return zip.generateAsync({ type: 'array' }).then((contents) => {
|
||||||
return save_({ name: 'output.zip', contents }, toastId)
|
return save_({ name: `${fileName || 'output'}.zip`, contents }, toastId)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
files[0].name = fileName || files[0].name
|
||||||
return save_(files[0], toastId)
|
return save_(files[0], toastId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,16 @@ import {
|
|||||||
createUnaryExpression,
|
createUnaryExpression,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
|
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
|
||||||
|
import { roundOff } from 'lib/utils'
|
||||||
|
import {
|
||||||
|
isCallExpression,
|
||||||
|
isArrayExpression,
|
||||||
|
isLiteral,
|
||||||
|
isBinaryExpression,
|
||||||
|
} from 'lang/util'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* It does not create the startSketchOn and it does not create the startProfileAt.
|
||||||
* Returns AST expressions for this KCL code:
|
* Returns AST expressions for this KCL code:
|
||||||
* const yo = startSketchOn('XY')
|
* const yo = startSketchOn('XY')
|
||||||
* |> startProfileAt([0, 0], %)
|
* |> startProfileAt([0, 0], %)
|
||||||
@ -92,3 +100,69 @@ export function updateRectangleSketch(
|
|||||||
createLiteral(Math.abs(y)), // This will be the height of the rectangle
|
createLiteral(Math.abs(y)), // This will be the height of the rectangle
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutates the pipeExpression to update the center rectangle sketch
|
||||||
|
* @param pipeExpression
|
||||||
|
* @param x
|
||||||
|
* @param y
|
||||||
|
* @param tag
|
||||||
|
*/
|
||||||
|
export function updateCenterRectangleSketch(
|
||||||
|
pipeExpression: PipeExpression,
|
||||||
|
deltaX: number,
|
||||||
|
deltaY: number,
|
||||||
|
tag: string,
|
||||||
|
originX: number,
|
||||||
|
originY: number
|
||||||
|
) {
|
||||||
|
let startX = originX - Math.abs(deltaX)
|
||||||
|
let startY = originY - Math.abs(deltaY)
|
||||||
|
|
||||||
|
// pipeExpression.body[1] is startProfileAt
|
||||||
|
let callExpression = pipeExpression.body[1]
|
||||||
|
if (isCallExpression(callExpression)) {
|
||||||
|
const arrayExpression = callExpression.arguments[0]
|
||||||
|
if (isArrayExpression(arrayExpression)) {
|
||||||
|
callExpression.arguments[0] = createArrayExpression([
|
||||||
|
createLiteral(roundOff(startX)),
|
||||||
|
createLiteral(roundOff(startY)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const twoX = deltaX * 2
|
||||||
|
const twoY = deltaY * 2
|
||||||
|
|
||||||
|
callExpression = pipeExpression.body[2]
|
||||||
|
if (isCallExpression(callExpression)) {
|
||||||
|
const arrayExpression = callExpression.arguments[0]
|
||||||
|
if (isArrayExpression(arrayExpression)) {
|
||||||
|
const literal = arrayExpression.elements[0]
|
||||||
|
if (isLiteral(literal)) {
|
||||||
|
callExpression.arguments[0] = createArrayExpression([
|
||||||
|
createLiteral(literal.value),
|
||||||
|
createLiteral(Math.abs(twoX)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callExpression = pipeExpression.body[3]
|
||||||
|
if (isCallExpression(callExpression)) {
|
||||||
|
const arrayExpression = callExpression.arguments[0]
|
||||||
|
if (isArrayExpression(arrayExpression)) {
|
||||||
|
const binaryExpression = arrayExpression.elements[0]
|
||||||
|
if (isBinaryExpression(binaryExpression)) {
|
||||||
|
callExpression.arguments[0] = createArrayExpression([
|
||||||
|
createBinaryExpression([
|
||||||
|
createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
|
||||||
|
binaryExpression.operator,
|
||||||
|
createLiteral(90),
|
||||||
|
]), // 90 offset from the previous line
|
||||||
|
createLiteral(Math.abs(twoY)), // This will be the height of the rectangle
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -124,7 +124,9 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
// We explicitly do not write to the file here since we are loading from
|
// We explicitly do not write to the file here since we are loading from
|
||||||
// the file system and not the editor.
|
// the file system and not the editor.
|
||||||
codeManager.updateCurrentFilePath(currentFilePath)
|
codeManager.updateCurrentFilePath(currentFilePath)
|
||||||
codeManager.updateCodeStateEditor(code)
|
// We pass true on the end here to clear the code editor history.
|
||||||
|
// This way undo and redo are not super weird when opening new files.
|
||||||
|
codeManager.updateCodeStateEditor(code, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the file system manager to the project path
|
// Set the file system manager to the project path
|
||||||
@ -145,6 +147,12 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
? await getProjectInfo(projectPath)
|
? await getProjectInfo(projectPath)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
console.log('maybeProjectInfo', {
|
||||||
|
maybeProjectInfo,
|
||||||
|
defaultProjectData,
|
||||||
|
projectPathData,
|
||||||
|
})
|
||||||
|
|
||||||
const projectData: IndexLoaderData = {
|
const projectData: IndexLoaderData = {
|
||||||
code,
|
code,
|
||||||
project: maybeProjectInfo ?? defaultProjectData,
|
project: maybeProjectInfo ?? defaultProjectData,
|
||||||
|
@ -118,8 +118,6 @@ export class Setting<T = unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MS_IN_MINUTE = 1000 * 60
|
|
||||||
|
|
||||||
export function createSettings() {
|
export function createSettings() {
|
||||||
return {
|
return {
|
||||||
/** Settings that affect the behavior of the entire app,
|
/** Settings that affect the behavior of the entire app,
|
||||||
@ -183,58 +181,13 @@ export function createSettings() {
|
|||||||
/**
|
/**
|
||||||
* Stream resource saving behavior toggle
|
* Stream resource saving behavior toggle
|
||||||
*/
|
*/
|
||||||
streamIdleMode: new Setting<number | undefined>({
|
streamIdleMode: new Setting<boolean>({
|
||||||
defaultValue: undefined,
|
defaultValue: false,
|
||||||
description: 'Toggle stream idling, saving bandwidth and battery',
|
description: 'Toggle stream idling, saving bandwidth and battery',
|
||||||
validate: (v) =>
|
validate: (v) => typeof v === 'boolean',
|
||||||
v === undefined ||
|
commandConfig: {
|
||||||
(typeof v === 'number' &&
|
inputType: 'boolean',
|
||||||
v >= 1 * MS_IN_MINUTE &&
|
},
|
||||||
v <= 60 * MS_IN_MINUTE),
|
|
||||||
Component: ({ value, updateValue }) => (
|
|
||||||
<div className="flex item-center gap-4 px-2 m-0 py-0">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={value !== undefined}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateValue(
|
|
||||||
!e.currentTarget.checked ? undefined : 5 * MS_IN_MINUTE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="block w-4 h-4"
|
|
||||||
/>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col grow">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
onChange={(e) =>
|
|
||||||
updateValue(Number(e.currentTarget.value) * MS_IN_MINUTE)
|
|
||||||
}
|
|
||||||
disabled={value === undefined}
|
|
||||||
value={
|
|
||||||
value !== null && value !== undefined
|
|
||||||
? value / MS_IN_MINUTE
|
|
||||||
: 5
|
|
||||||
}
|
|
||||||
min={1}
|
|
||||||
max={60}
|
|
||||||
step={1}
|
|
||||||
className="block flex-1"
|
|
||||||
/>
|
|
||||||
{value !== undefined && value !== null && (
|
|
||||||
<div>
|
|
||||||
{value / MS_IN_MINUTE === 60
|
|
||||||
? '1 hour'
|
|
||||||
: value / MS_IN_MINUTE === 1
|
|
||||||
? '1 minute'
|
|
||||||
: value / MS_IN_MINUTE + ' minutes'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
onboardingStatus: new Setting<string>({
|
onboardingStatus: new Setting<string>({
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
|
@ -24,10 +24,6 @@ import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration
|
|||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
import { DeepPartial } from 'lib/types'
|
import { DeepPartial } from 'lib/types'
|
||||||
|
|
||||||
type OmitNull<T> = T extends null ? undefined : T
|
|
||||||
const toUndefinedIfNull = (a: any): OmitNull<any> =>
|
|
||||||
a === null ? undefined : a
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert from a rust settings struct into the JS settings struct.
|
* Convert from a rust settings struct into the JS settings struct.
|
||||||
* We do this because the JS settings type has all the fancy shit
|
* We do this because the JS settings type has all the fancy shit
|
||||||
@ -44,9 +40,7 @@ export function configurationToSettingsPayload(
|
|||||||
: undefined,
|
: undefined,
|
||||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||||
streamIdleMode: toUndefinedIfNull(
|
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
|
||||||
configuration?.settings?.app?.stream_idle_mode
|
|
||||||
),
|
|
||||||
projectDirectory: configuration?.settings?.project?.directory,
|
projectDirectory: configuration?.settings?.project?.directory,
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
},
|
},
|
||||||
@ -85,9 +79,7 @@ export function projectConfigurationToSettingsPayload(
|
|||||||
: undefined,
|
: undefined,
|
||||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||||
streamIdleMode: toUndefinedIfNull(
|
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
|
||||||
configuration?.settings?.app?.stream_idle_mode
|
|
||||||
),
|
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
},
|
},
|
||||||
modeling: {
|
modeling: {
|
||||||
|
@ -10,14 +10,8 @@ export const codeManager = new CodeManager()
|
|||||||
|
|
||||||
export const engineCommandManager = new EngineCommandManager()
|
export const engineCommandManager = new EngineCommandManager()
|
||||||
|
|
||||||
declare global {
|
// Accessible for tests mostly
|
||||||
interface Window {
|
// @ts-ignore
|
||||||
tearDown: typeof engineCommandManager.tearDown
|
|
||||||
sceneInfra: typeof sceneInfra
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accessible for tests
|
|
||||||
window.tearDown = engineCommandManager.tearDown
|
window.tearDown = engineCommandManager.tearDown
|
||||||
|
|
||||||
// This needs to be after codeManager is created.
|
// This needs to be after codeManager is created.
|
||||||
@ -27,9 +21,7 @@ engineCommandManager.kclManager = kclManager
|
|||||||
engineCommandManager.getAstCb = () => kclManager.ast
|
engineCommandManager.getAstCb = () => kclManager.ast
|
||||||
|
|
||||||
export const sceneInfra = new SceneInfra(engineCommandManager)
|
export const sceneInfra = new SceneInfra(engineCommandManager)
|
||||||
|
engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange
|
||||||
// Accessible for tests
|
|
||||||
window.sceneInfra = sceneInfra
|
|
||||||
|
|
||||||
export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
|
export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
|
||||||
|
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
// 0.25s is the average visual reaction time for humans so we'll go a bit less
|
|
||||||
// so those exception people don't see.
|
|
||||||
export const REASONABLE_TIME_TO_REFRESH_STREAM_SIZE = 100
|
|
@ -407,8 +407,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Center circle',
|
title: 'Center circle',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!canRectangleOrCircleTool(state.context) &&
|
state.matches('Sketch no face') ||
|
||||||
!state.matches({ Sketch: 'Circle tool' }),
|
(!canRectangleOrCircleTool(state.context) &&
|
||||||
|
!state.matches({ Sketch: 'Circle tool' })),
|
||||||
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
|
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
|
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
|
||||||
@ -448,8 +449,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
icon: 'rectangle',
|
icon: 'rectangle',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
!canRectangleOrCircleTool(state.context) &&
|
state.matches('Sketch no face') ||
|
||||||
!state.matches({ Sketch: 'Rectangle tool' }),
|
(!canRectangleOrCircleTool(state.context) &&
|
||||||
|
!state.matches({ Sketch: 'Rectangle tool' })),
|
||||||
title: 'Corner rectangle',
|
title: 'Corner rectangle',
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
|
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
|
||||||
@ -459,13 +461,33 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'center-rectangle',
|
id: 'center-rectangle',
|
||||||
onClick: () => console.error('Center rectangle not yet implemented'),
|
onClick: ({ modelingState, modelingSend }) =>
|
||||||
icon: 'rectangle',
|
modelingSend({
|
||||||
status: 'unavailable',
|
type: 'change tool',
|
||||||
|
data: {
|
||||||
|
tool: !modelingState.matches({
|
||||||
|
Sketch: 'Center Rectangle tool',
|
||||||
|
})
|
||||||
|
? 'center rectangle'
|
||||||
|
: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
icon: 'arc',
|
||||||
|
status: 'available',
|
||||||
|
disabled: (state) =>
|
||||||
|
state.matches('Sketch no face') ||
|
||||||
|
(!canRectangleOrCircleTool(state.context) &&
|
||||||
|
!state.matches({ Sketch: 'Center Rectangle tool' })),
|
||||||
title: 'Center rectangle',
|
title: 'Center rectangle',
|
||||||
showTitle: false,
|
hotkey: (state) =>
|
||||||
|
state.matches({ Sketch: 'Center Rectangle tool' })
|
||||||
|
? ['Esc', 'C']
|
||||||
|
: 'C',
|
||||||
description: 'Start drawing a rectangle from its center',
|
description: 'Start drawing a rectangle from its center',
|
||||||
links: [],
|
links: [],
|
||||||
|
isActive: (state) => {
|
||||||
|
return state.matches({ Sketch: 'Center Rectangle tool' })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,23 @@ import toast from 'react-hot-toast'
|
|||||||
|
|
||||||
type ExcludeErr<T> = Exclude<T, Error>
|
type ExcludeErr<T> = Exclude<T, Error>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to Error, but more lightweight, without the stack trace. It can also
|
||||||
|
* be used to represent a reason for not being able to provide an alternative,
|
||||||
|
* which isn't necessarily an error.
|
||||||
|
*/
|
||||||
|
export class Reason {
|
||||||
|
message: string
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
this.message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
toError() {
|
||||||
|
return new Error(this.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is intentionally *not* exported due to misuse. We'd like to add a lint.
|
* This is intentionally *not* exported due to misuse. We'd like to add a lint.
|
||||||
*/
|
*/
|
||||||
|
@ -159,6 +159,15 @@ export type DefaultPlane = {
|
|||||||
yAxis: [number, number, number]
|
yAxis: [number, number, number]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OffsetPlane = {
|
||||||
|
type: 'offsetPlane'
|
||||||
|
position: [number, number, number]
|
||||||
|
planeId: string
|
||||||
|
pathToNode: PathToNode
|
||||||
|
zAxis: [number, number, number]
|
||||||
|
yAxis: [number, number, number]
|
||||||
|
}
|
||||||
|
|
||||||
export type SegmentOverlayPayload =
|
export type SegmentOverlayPayload =
|
||||||
| {
|
| {
|
||||||
type: 'set-one'
|
type: 'set-one'
|
||||||
@ -184,6 +193,7 @@ export type SketchTool =
|
|||||||
| 'line'
|
| 'line'
|
||||||
| 'tangentialArc'
|
| 'tangentialArc'
|
||||||
| 'rectangle'
|
| 'rectangle'
|
||||||
|
| 'center rectangle'
|
||||||
| 'circle'
|
| 'circle'
|
||||||
| 'none'
|
| 'none'
|
||||||
|
|
||||||
@ -197,7 +207,7 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Sketch On Face' }
|
| { type: 'Sketch On Face' }
|
||||||
| {
|
| {
|
||||||
type: 'Select default plane'
|
type: 'Select default plane'
|
||||||
data: DefaultPlane | ExtrudeFacePlane
|
data: DefaultPlane | ExtrudeFacePlane | OffsetPlane
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'Set selection'
|
type: 'Set selection'
|
||||||
@ -238,6 +248,10 @@ export type ModelingMachineEvent =
|
|||||||
type: 'Add rectangle origin'
|
type: 'Add rectangle origin'
|
||||||
data: [x: number, y: number]
|
data: [x: number, y: number]
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'Add center rectangle origin'
|
||||||
|
data: [x: number, y: number]
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'Add circle origin'
|
type: 'Add circle origin'
|
||||||
data: [x: number, y: number]
|
data: [x: number, y: number]
|
||||||
@ -278,6 +292,7 @@ export type ModelingMachineEvent =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
| { type: 'Finish rectangle' }
|
| { type: 'Finish rectangle' }
|
||||||
|
| { type: 'Finish center rectangle' }
|
||||||
| { type: 'Finish circle' }
|
| { type: 'Finish circle' }
|
||||||
| { type: 'Artifact graph populated' }
|
| { type: 'Artifact graph populated' }
|
||||||
| { type: 'Artifact graph emptied' }
|
| { type: 'Artifact graph emptied' }
|
||||||
@ -506,6 +521,9 @@ export const modelingMachine = setup({
|
|||||||
'next is rectangle': ({ context: { sketchDetails, currentTool } }) =>
|
'next is rectangle': ({ context: { sketchDetails, currentTool } }) =>
|
||||||
currentTool === 'rectangle' &&
|
currentTool === 'rectangle' &&
|
||||||
canRectangleOrCircleTool({ sketchDetails }),
|
canRectangleOrCircleTool({ sketchDetails }),
|
||||||
|
'next is center rectangle': ({ context: { sketchDetails, currentTool } }) =>
|
||||||
|
currentTool === 'center rectangle' &&
|
||||||
|
canRectangleOrCircleTool({ sketchDetails }),
|
||||||
'next is circle': ({ context: { sketchDetails, currentTool } }) =>
|
'next is circle': ({ context: { sketchDetails, currentTool } }) =>
|
||||||
currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }),
|
currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }),
|
||||||
'next is line': ({ context }) => context.currentTool === 'line',
|
'next is line': ({ context }) => context.currentTool === 'line',
|
||||||
@ -806,6 +824,26 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'listen for center rectangle origin': ({ context: { sketchDetails } }) => {
|
||||||
|
if (!sketchDetails) return
|
||||||
|
// setupNoPointsListener has the code for startProfileAt onClick
|
||||||
|
sceneEntitiesManager.setupNoPointsListener({
|
||||||
|
sketchDetails,
|
||||||
|
afterClick: (args) => {
|
||||||
|
const twoD = args.intersectionPoint?.twoD
|
||||||
|
if (twoD) {
|
||||||
|
sceneInfra.modelingSend({
|
||||||
|
type: 'Add center rectangle origin',
|
||||||
|
data: [twoD.x, twoD.y],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.error('No intersection point found')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
'listen for circle origin': ({ context: { sketchDetails } }) => {
|
'listen for circle origin': ({ context: { sketchDetails } }) => {
|
||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
sceneEntitiesManager.createIntersectionPlane()
|
sceneEntitiesManager.createIntersectionPlane()
|
||||||
@ -859,6 +897,21 @@ export const modelingMachine = setup({
|
|||||||
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
'set up draft center rectangle': ({
|
||||||
|
context: { sketchDetails },
|
||||||
|
event,
|
||||||
|
}) => {
|
||||||
|
if (event.type !== 'Add center rectangle origin') return
|
||||||
|
if (!sketchDetails || !event.data) return
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
sceneEntitiesManager.setupDraftCenterRectangle(
|
||||||
|
sketchDetails.sketchPathToNode,
|
||||||
|
sketchDetails.zAxis,
|
||||||
|
sketchDetails.yAxis,
|
||||||
|
sketchDetails.origin,
|
||||||
|
event.data
|
||||||
|
)
|
||||||
|
},
|
||||||
'set up draft circle': ({ context: { sketchDetails }, event }) => {
|
'set up draft circle': ({ context: { sketchDetails }, event }) => {
|
||||||
if (event.type !== 'Add circle origin') return
|
if (event.type !== 'Add circle origin') return
|
||||||
if (!sketchDetails || !event.data) return
|
if (!sketchDetails || !event.data) return
|
||||||
@ -1350,7 +1403,7 @@ export const modelingMachine = setup({
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
'animate-to-face': fromPromise(
|
'animate-to-face': fromPromise(
|
||||||
async (_: { input?: ExtrudeFacePlane | DefaultPlane }) => {
|
async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => {
|
||||||
return {} as
|
return {} as
|
||||||
| undefined
|
| undefined
|
||||||
| {
|
| {
|
||||||
@ -1822,6 +1875,40 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Center Rectangle tool': {
|
||||||
|
entry: ['listen for center rectangle origin'],
|
||||||
|
|
||||||
|
states: {
|
||||||
|
'Awaiting corner': {
|
||||||
|
on: {
|
||||||
|
'Finish center rectangle': 'Finished Center Rectangle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Awaiting origin': {
|
||||||
|
on: {
|
||||||
|
'Add center rectangle origin': {
|
||||||
|
target: 'Awaiting corner',
|
||||||
|
// TODO
|
||||||
|
actions: 'set up draft center rectangle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Finished Center Rectangle': {
|
||||||
|
always: '#Modeling.Sketch.SketchIdle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initial: 'Awaiting origin',
|
||||||
|
|
||||||
|
on: {
|
||||||
|
'change tool': {
|
||||||
|
target: 'Change Tool',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
'clean slate': {
|
'clean slate': {
|
||||||
always: 'SketchIdle',
|
always: 'SketchIdle',
|
||||||
},
|
},
|
||||||
@ -2015,6 +2102,10 @@ export const modelingMachine = setup({
|
|||||||
target: 'Circle tool',
|
target: 'Circle tool',
|
||||||
guard: 'next is circle',
|
guard: 'next is circle',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
target: 'Center Rectangle tool',
|
||||||
|
guard: 'next is center rectangle',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
entry: 'assign tool in context',
|
entry: 'assign tool in context',
|
||||||
|
15
src/wasm-lib/Cargo.lock
generated
@ -1589,6 +1589,8 @@ dependencies = [
|
|||||||
"console",
|
"console",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"similar",
|
"similar",
|
||||||
@ -1689,6 +1691,7 @@ dependencies = [
|
|||||||
"databake",
|
"databake",
|
||||||
"derive-docs",
|
"derive-docs",
|
||||||
"expectorate",
|
"expectorate",
|
||||||
|
"fnv",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures",
|
"futures",
|
||||||
"git_rev",
|
"git_rev",
|
||||||
@ -1734,18 +1737,6 @@ dependencies = [
|
|||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kcl-macros"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"databake",
|
|
||||||
"kcl-lib",
|
|
||||||
"pretty_assertions",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.87",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-test-server"
|
name = "kcl-test-server"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
|
@ -68,7 +68,6 @@ debug = "line-tables-only"
|
|||||||
members = [
|
members = [
|
||||||
"derive-docs",
|
"derive-docs",
|
||||||
"kcl",
|
"kcl",
|
||||||
"kcl-macros",
|
|
||||||
"kcl-test-server",
|
"kcl-test-server",
|
||||||
"kcl-to-core",
|
"kcl-to-core",
|
||||||
]
|
]
|
||||||
|
@ -173,11 +173,11 @@ fn do_stdlib_inner(
|
|||||||
quote! {
|
quote! {
|
||||||
let code_blocks = vec![#(#cb),*];
|
let code_blocks = vec![#(#cb),*];
|
||||||
code_blocks.iter().map(|cb| {
|
code_blocks.iter().map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
|
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
}).collect::<Vec<String>>()
|
}).collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -748,8 +748,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
|
|||||||
quote! {
|
quote! {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn #test_name_mock() {
|
async fn #test_name_mock() {
|
||||||
let program = crate::parser::top_level_parse(#code_block).unwrap();
|
let program = crate::Program::parse(#code_block).unwrap();
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
|
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
|
||||||
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
|
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
|
||||||
@ -758,7 +757,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
|
|||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default()).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
mod test_examples_someFn {
|
mod test_examples_someFn {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_someFn0() {
|
async fn test_mock_example_someFn0() {
|
||||||
let program = crate::parser::top_level_parse("someFn()").unwrap();
|
let program = crate::Program::parse("someFn()").unwrap();
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -15,7 +14,9 @@ mod test_examples_someFn {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -111,10 +112,10 @@ impl crate::docs::StdLibFn for SomeFn {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
mod test_examples_someFn {
|
mod test_examples_someFn {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_someFn0() {
|
async fn test_mock_example_someFn0() {
|
||||||
let program = crate::parser::top_level_parse("someFn()").unwrap();
|
let program = crate::Program::parse("someFn()").unwrap();
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -15,7 +14,9 @@ mod test_examples_someFn {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -111,10 +112,10 @@ impl crate::docs::StdLibFn for SomeFn {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,7 @@ mod test_examples_show {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_show0() {
|
async fn test_mock_example_show0() {
|
||||||
let program =
|
let program =
|
||||||
crate::parser::top_level_parse("This is another code block.\nyes sirrr.\nshow")
|
crate::Program::parse("This is another code block.\nyes sirrr.\nshow").unwrap();
|
||||||
.unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -17,7 +15,9 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -36,9 +36,7 @@ mod test_examples_show {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_show1() {
|
async fn test_mock_example_show1() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nshow").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -50,7 +48,9 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -149,10 +149,10 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
mod test_examples_show {
|
mod test_examples_show {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_show0() {
|
async fn test_mock_example_show0() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nshow").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -16,7 +14,9 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,7 @@ mod test_examples_my_func {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_my_func0() {
|
async fn test_mock_example_my_func0() {
|
||||||
let program =
|
let program =
|
||||||
crate::parser::top_level_parse("This is another code block.\nyes sirrr.\nmyFunc")
|
crate::Program::parse("This is another code block.\nyes sirrr.\nmyFunc").unwrap();
|
||||||
.unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -17,7 +15,9 @@ mod test_examples_my_func {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -36,9 +36,7 @@ mod test_examples_my_func {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_my_func1() {
|
async fn test_mock_example_my_func1() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nmyFunc").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nmyFunc").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -50,7 +48,9 @@ mod test_examples_my_func {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -149,10 +149,10 @@ impl crate::docs::StdLibFn for MyFunc {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,7 @@ mod test_examples_line_to {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_line_to0() {
|
async fn test_mock_example_line_to0() {
|
||||||
let program =
|
let program =
|
||||||
crate::parser::top_level_parse("This is another code block.\nyes sirrr.\nlineTo")
|
crate::Program::parse("This is another code block.\nyes sirrr.\nlineTo").unwrap();
|
||||||
.unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -17,7 +15,9 @@ mod test_examples_line_to {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -36,9 +36,7 @@ mod test_examples_line_to {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_line_to1() {
|
async fn test_mock_example_line_to1() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nlineTo").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nlineTo").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -50,7 +48,9 @@ mod test_examples_line_to {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -157,10 +157,10 @@ impl crate::docs::StdLibFn for LineTo {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@ mod test_examples_min {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_min0() {
|
async fn test_mock_example_min0() {
|
||||||
let program =
|
let program =
|
||||||
crate::parser::top_level_parse("This is another code block.\nyes sirrr.\nmin").unwrap();
|
crate::Program::parse("This is another code block.\nyes sirrr.\nmin").unwrap();
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -16,7 +15,9 @@ mod test_examples_min {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -35,9 +36,7 @@ mod test_examples_min {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_min1() {
|
async fn test_mock_example_min1() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nmin").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nmin").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -49,7 +48,9 @@ mod test_examples_min {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -148,10 +149,10 @@ impl crate::docs::StdLibFn for Min {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
mod test_examples_show {
|
mod test_examples_show {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_show0() {
|
async fn test_mock_example_show0() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nshow").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -16,7 +14,9 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
mod test_examples_import {
|
mod test_examples_import {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_import0() {
|
async fn test_mock_example_import0() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nimport").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -16,7 +14,9 @@ mod test_examples_import {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
mod test_examples_import {
|
mod test_examples_import {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_import0() {
|
async fn test_mock_example_import0() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nimport").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -16,7 +14,9 @@ mod test_examples_import {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
mod test_examples_import {
|
mod test_examples_import {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_import0() {
|
async fn test_mock_example_import0() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nimport").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nimport").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -16,7 +14,9 @@ mod test_examples_import {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
mod test_examples_show {
|
mod test_examples_show {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_show0() {
|
async fn test_mock_example_show0() {
|
||||||
let program =
|
let program = crate::Program::parse("This is code.\nIt does other shit.\nshow").unwrap();
|
||||||
crate::parser::top_level_parse("This is code.\nIt does other shit.\nshow").unwrap();
|
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -16,7 +14,9 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -112,10 +112,10 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
mod test_examples_some_function {
|
mod test_examples_some_function {
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_mock_example_some_function0() {
|
async fn test_mock_example_some_function0() {
|
||||||
let program = crate::parser::top_level_parse("someFunction()").unwrap();
|
let program = crate::Program::parse("someFunction()").unwrap();
|
||||||
let id_generator = crate::executor::IdGenerator::default();
|
|
||||||
let ctx = crate::executor::ExecutorContext {
|
let ctx = crate::executor::ExecutorContext {
|
||||||
engine: std::sync::Arc::new(Box::new(
|
engine: std::sync::Arc::new(Box::new(
|
||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
@ -15,7 +14,9 @@ mod test_examples_some_function {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
context_type: crate::executor::ContextType::Mock,
|
||||||
};
|
};
|
||||||
ctx.run(&program, None, id_generator, None).await.unwrap();
|
ctx.run(&program, &mut crate::ExecState::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -106,10 +107,10 @@ impl crate::docs::StdLibFn for SomeFunction {
|
|||||||
code_blocks
|
code_blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cb| {
|
.map(|cb| {
|
||||||
let program = crate::parser::top_level_parse(cb).unwrap();
|
let program = crate::Program::parse(cb).unwrap();
|
||||||
let mut options: crate::ast::types::FormatOptions = Default::default();
|
let mut options: crate::ast::types::FormatOptions = Default::default();
|
||||||
options.insert_final_newline = false;
|
options.insert_final_newline = false;
|
||||||
program.recast(&options, 0)
|
program.ast.recast(&options, 0)
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,20 @@
|
|||||||
cnr := "cargo nextest run"
|
cnr := "cargo nextest run"
|
||||||
cita := "cargo insta test --accept"
|
cita := "cargo insta test --accept"
|
||||||
|
|
||||||
# Create a new KCL snapshot test from `tests/inputs/my-test.kcl`.
|
# Run the same lint checks we run in CI.
|
||||||
new-test name:
|
|
||||||
echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs
|
|
||||||
TWENTY_TWENTY=overwrite {{cnr}} --test executor -E 'test(=visuals::{{name}})'
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
cargo clippy --workspace --all-targets -- -D warnings
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
|
||||||
|
# Generate the stdlib image artifacts
|
||||||
|
# Then run the stdlib docs generation
|
||||||
redo-kcl-stdlib-docs:
|
redo-kcl-stdlib-docs:
|
||||||
|
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example
|
||||||
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
|
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
|
||||||
|
|
||||||
# Create a new KCL deterministic simulation test case.
|
# Create a new KCL deterministic simulation test case.
|
||||||
new-sim-test test_name kcl_program render_to_png="true":
|
new-sim-test test_name render_to_png="true":
|
||||||
# Each test file gets its own directory. This will contain the KCL program, and its
|
|
||||||
# snapshotted artifacts (e.g. serialized tokens, serialized ASTs, program memory,
|
|
||||||
# PNG snapshots, etc).
|
|
||||||
mkdir kcl/tests/{{test_name}}
|
|
||||||
echo "{{kcl_program}}" > kcl/tests/{{test_name}}/input.kcl
|
|
||||||
# Add the various tests for this new test case.
|
|
||||||
cat kcl/tests/simtest.tmpl | sed "s/TEST_NAME_HERE/{{test_name}}/" | sed "s/RENDER_TO_PNG/{{render_to_png}}/" >> kcl/src/simulation_tests.rs
|
|
||||||
# Run all the tests for the first time, in the right order.
|
|
||||||
{{cita}} -p kcl-lib -- tests::{{test_name}}::tokenize
|
{{cita}} -p kcl-lib -- tests::{{test_name}}::tokenize
|
||||||
{{cita}} -p kcl-lib -- tests::{{test_name}}::parse
|
{{cita}} -p kcl-lib -- tests::{{test_name}}::parse
|
||||||
{{cita}} -p kcl-lib -- tests::{{test_name}}::unparse
|
{{cita}} -p kcl-lib -- tests::{{test_name}}::unparse
|
||||||
TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib -- tests::{{test_name}}::kcl_test_execute
|
TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib -- tests::{{test_name}}::kcl_test_execute
|
||||||
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "kcl-macros"
|
|
||||||
description = "Macro for compiling KCL to its AST during Rust compile-time"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
databake = "0.1.8"
|
|
||||||
kcl-lib = { path = "../kcl" }
|
|
||||||
proc-macro2 = "1"
|
|
||||||
quote = "1"
|
|
||||||
syn = { version = "2.0.87", features = ["full"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
pretty_assertions = "1.4.1"
|
|
@ -1,22 +0,0 @@
|
|||||||
//! This crate contains macros for parsing KCL at Rust compile-time.
|
|
||||||
use databake::*;
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
use syn::{parse_macro_input, LitStr};
|
|
||||||
|
|
||||||
/// Parses KCL into its AST at compile-time.
|
|
||||||
/// This macro takes exactly one argument: A string literal containing KCL.
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// extern crate alloc;
|
|
||||||
/// use kcl_compile_macro::parse_kcl;
|
|
||||||
/// let ast: kcl_lib::ast::types::Program = parse_kcl!("const y = 4");
|
|
||||||
/// ```
|
|
||||||
#[proc_macro]
|
|
||||||
pub fn parse(input: TokenStream) -> TokenStream {
|
|
||||||
let input = parse_macro_input!(input as LitStr);
|
|
||||||
let kcl_src = input.value();
|
|
||||||
let ast = kcl_lib::parser::top_level_parse(&kcl_src).unwrap();
|
|
||||||
let ast_struct = ast.bake(&Default::default());
|
|
||||||
quote!(#ast_struct).into()
|
|
||||||
}
|
|