Compare commits
25 Commits
achalmers/
...
kurt-reduc
Author | SHA1 | Date | |
---|---|---|---|
68957ad7d8 | |||
adcf80331a | |||
4fbd7ace98 | |||
0df858b9ca | |||
c6f080c440 | |||
c1a14a107a | |||
3c721f2b29 | |||
61e2a1eddc | |||
6406e27794 | |||
1e382a76dd | |||
06cdaa9ae8 | |||
85c30be333 | |||
4d4a1d66e8 | |||
223b5952aa | |||
fedffbb384 | |||
ed4e3df3b2 | |||
18d200e790 | |||
0c50a5996d | |||
73bca2dcfc | |||
c6a50a3cdf | |||
b81c9d04cc | |||
9d8a7064da | |||
b0e6140e9f | |||
f9df7ff885 | |||
aec9637d7a |
1
.github/workflows/playwright.yml
vendored
@ -85,7 +85,6 @@ jobs:
|
|||||||
playwright-macos:
|
playwright-macos:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
needs: playwright-ubuntu
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
1
.gitignore
vendored
@ -33,6 +33,7 @@ src/wasm-lib/bindings
|
|||||||
src/wasm-lib/kcl/bindings
|
src/wasm-lib/kcl/bindings
|
||||||
public/wasm_lib_bg.wasm
|
public/wasm_lib_bg.wasm
|
||||||
src/wasm-lib/lcov.info
|
src/wasm-lib/lcov.info
|
||||||
|
src/wasm-lib/grackle/test_json_output
|
||||||
|
|
||||||
e2e/playwright/playwright-secrets.env
|
e2e/playwright/playwright-secrets.env
|
||||||
e2e/playwright/temp1.png
|
e2e/playwright/temp1.png
|
||||||
|
2431
docs/kcl/std.json
466
docs/kcl/std.md
@ -20,6 +20,7 @@
|
|||||||
* [`atan`](#atan)
|
* [`atan`](#atan)
|
||||||
* [`bezierCurve`](#bezierCurve)
|
* [`bezierCurve`](#bezierCurve)
|
||||||
* [`ceil`](#ceil)
|
* [`ceil`](#ceil)
|
||||||
|
* [`circle`](#circle)
|
||||||
* [`close`](#close)
|
* [`close`](#close)
|
||||||
* [`cos`](#cos)
|
* [`cos`](#cos)
|
||||||
* [`e`](#e)
|
* [`e`](#e)
|
||||||
@ -49,7 +50,6 @@
|
|||||||
* [`segEndX`](#segEndX)
|
* [`segEndX`](#segEndX)
|
||||||
* [`segEndY`](#segEndY)
|
* [`segEndY`](#segEndY)
|
||||||
* [`segLen`](#segLen)
|
* [`segLen`](#segLen)
|
||||||
* [`show`](#show)
|
|
||||||
* [`sin`](#sin)
|
* [`sin`](#sin)
|
||||||
* [`sqrt`](#sqrt)
|
* [`sqrt`](#sqrt)
|
||||||
* [`startProfileAt`](#startProfileAt)
|
* [`startProfileAt`](#startProfileAt)
|
||||||
@ -3438,6 +3438,290 @@ ceil(num: number) -> number
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### circle
|
||||||
|
|
||||||
|
Sketch a circle on the given plane
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
circle(plane: SketchData, center: [number, number], radius: number) -> SketchGroup
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
|
||||||
|
* `plane`: `SketchData` - Data for start sketch on. You can start a sketch on a plane or an extrude group.
|
||||||
|
```
|
||||||
|
"XY" |
|
||||||
|
"-XY" |
|
||||||
|
"XZ" |
|
||||||
|
"-XZ" |
|
||||||
|
"YZ" |
|
||||||
|
"-YZ" |
|
||||||
|
{
|
||||||
|
plane: {
|
||||||
|
// Origin of the plane.
|
||||||
|
origin: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// What should the plane’s X axis be?
|
||||||
|
x_axis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// What should the plane’s Y axis be?
|
||||||
|
y_axis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// The z-axis (normal).
|
||||||
|
z_axis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
// The id of the extrusion end cap
|
||||||
|
endCapId: uuid,
|
||||||
|
// The height of the extrude group.
|
||||||
|
height: number,
|
||||||
|
// The id of the extrude group.
|
||||||
|
id: uuid,
|
||||||
|
// The position of the extrude group.
|
||||||
|
position: [number, number, number],
|
||||||
|
// The rotation of the extrude group.
|
||||||
|
rotation: [number, number, number, number],
|
||||||
|
// The id of the extrusion start cap
|
||||||
|
startCapId: uuid,
|
||||||
|
// The extrude surfaces.
|
||||||
|
value: [{
|
||||||
|
// The face id for the extrude plane.
|
||||||
|
faceId: uuid,
|
||||||
|
// The id of the geometry.
|
||||||
|
id: uuid,
|
||||||
|
// The name.
|
||||||
|
name: string,
|
||||||
|
// The position.
|
||||||
|
position: [number, number, number],
|
||||||
|
// The rotation.
|
||||||
|
rotation: [number, number, number, number],
|
||||||
|
// The source range.
|
||||||
|
sourceRange: [number, number],
|
||||||
|
type: "extrudePlane",
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
// The face id for the extrude plane.
|
||||||
|
faceId: uuid,
|
||||||
|
// The id of the geometry.
|
||||||
|
id: uuid,
|
||||||
|
// The name.
|
||||||
|
name: string,
|
||||||
|
// The position.
|
||||||
|
position: [number, number, number],
|
||||||
|
// The rotation.
|
||||||
|
rotation: [number, number, number, number],
|
||||||
|
// The source range.
|
||||||
|
sourceRange: [number, number],
|
||||||
|
type: "extrudeArc",
|
||||||
|
}],
|
||||||
|
// The x-axis of the extrude group base plane in the 3D space
|
||||||
|
xAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// The y-axis of the extrude group base plane in the 3D space
|
||||||
|
yAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// The z-axis of the extrude group base plane in the 3D space
|
||||||
|
zAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* `center`: `[number, number]`
|
||||||
|
* `radius`: `number`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
* `SketchGroup` - A sketch group is a collection of paths.
|
||||||
|
```
|
||||||
|
{
|
||||||
|
// The plane id or face id of the sketch group.
|
||||||
|
entityId: uuid,
|
||||||
|
// The id of the sketch group.
|
||||||
|
id: uuid,
|
||||||
|
// What the sketch is on (can be a plane or a face).
|
||||||
|
on: {
|
||||||
|
// The id of the plane.
|
||||||
|
id: uuid,
|
||||||
|
// Origin of the plane.
|
||||||
|
origin: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
type: "plane",
|
||||||
|
// Type for a plane.
|
||||||
|
value: "XY" | "XZ" | "YZ" | "Custom",
|
||||||
|
// What should the plane’s X axis be?
|
||||||
|
xAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// What should the plane’s Y axis be?
|
||||||
|
yAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// The z-axis (normal).
|
||||||
|
zAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
// The id of the face.
|
||||||
|
id: uuid,
|
||||||
|
// The original sketch group id of the object we are sketching on.
|
||||||
|
sketchGroupId: uuid,
|
||||||
|
type: "face",
|
||||||
|
// The tag of the face.
|
||||||
|
value: string,
|
||||||
|
// What should the face’s X axis be?
|
||||||
|
xAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// What should the face’s Y axis be?
|
||||||
|
yAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// The z-axis (normal).
|
||||||
|
zAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// The position of the sketch group.
|
||||||
|
position: [number, number, number],
|
||||||
|
// The rotation of the sketch group base plane.
|
||||||
|
rotation: [number, number, number, number],
|
||||||
|
// The starting path.
|
||||||
|
start: {
|
||||||
|
// The from point.
|
||||||
|
from: [number, number],
|
||||||
|
// The name of the path.
|
||||||
|
name: string,
|
||||||
|
// The to point.
|
||||||
|
to: [number, number],
|
||||||
|
},
|
||||||
|
// The paths in the sketch group.
|
||||||
|
value: [{
|
||||||
|
// The from point.
|
||||||
|
from: [number, number],
|
||||||
|
// The name of the path.
|
||||||
|
name: string,
|
||||||
|
// The to point.
|
||||||
|
to: [number, number],
|
||||||
|
type: "ToPoint",
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
// arc's direction
|
||||||
|
ccw: string,
|
||||||
|
// the arc's center
|
||||||
|
center: [number, number],
|
||||||
|
// The from point.
|
||||||
|
from: [number, number],
|
||||||
|
// The name of the path.
|
||||||
|
name: string,
|
||||||
|
// The to point.
|
||||||
|
to: [number, number],
|
||||||
|
type: "TangentialArcTo",
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
// The from point.
|
||||||
|
from: [number, number],
|
||||||
|
// The name of the path.
|
||||||
|
name: string,
|
||||||
|
// The to point.
|
||||||
|
to: [number, number],
|
||||||
|
type: "TangentialArc",
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
// The from point.
|
||||||
|
from: [number, number],
|
||||||
|
// The name of the path.
|
||||||
|
name: string,
|
||||||
|
// The to point.
|
||||||
|
to: [number, number],
|
||||||
|
type: "Horizontal",
|
||||||
|
// The x coordinate.
|
||||||
|
x: number,
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
// The from point.
|
||||||
|
from: [number, number],
|
||||||
|
// The name of the path.
|
||||||
|
name: string,
|
||||||
|
// The to point.
|
||||||
|
to: [number, number],
|
||||||
|
type: "AngledLineTo",
|
||||||
|
// The x coordinate.
|
||||||
|
x: number,
|
||||||
|
// The y coordinate.
|
||||||
|
y: number,
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
// The from point.
|
||||||
|
from: [number, number],
|
||||||
|
// The name of the path.
|
||||||
|
name: string,
|
||||||
|
// The to point.
|
||||||
|
to: [number, number],
|
||||||
|
type: "Base",
|
||||||
|
}],
|
||||||
|
// The x-axis of the sketch group base plane in the 3D space
|
||||||
|
xAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// The y-axis of the sketch group base plane in the 3D space
|
||||||
|
yAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
// The z-axis of the sketch group base plane in the 3D space
|
||||||
|
zAxis: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### close
|
### close
|
||||||
|
|
||||||
Close the current sketch.
|
Close the current sketch.
|
||||||
@ -4703,6 +4987,7 @@ hole(hole_sketch_group: SketchGroupSet, sketch_group: SketchGroup) -> SketchGrou
|
|||||||
Import a CAD file.
|
Import a CAD file.
|
||||||
|
|
||||||
For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters. Otherwise you can specify the unit by passing in the options parameter. If you import a gltf file, we will try to find the bin file and import it as well.
|
For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters. Otherwise you can specify the unit by passing in the options parameter. If you import a gltf file, we will try to find the bin file and import it as well.
|
||||||
|
Import paths are relative to the current project directory. This only works in the desktop app not in browser.
|
||||||
|
|
||||||
```
|
```
|
||||||
import(file_path: String, options: ImportFormat) -> ImportedGeometry
|
import(file_path: String, options: ImportFormat) -> ImportedGeometry
|
||||||
@ -7383,185 +7668,6 @@ segLen(segment_name: string, sketch_group: SketchGroup) -> number
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### show
|
|
||||||
|
|
||||||
Render a model.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
show(sketch: SketchGroup)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Arguments
|
|
||||||
|
|
||||||
* `sketch`: `SketchGroup` - A sketch group is a collection of paths.
|
|
||||||
```
|
|
||||||
{
|
|
||||||
// The plane id or face id of the sketch group.
|
|
||||||
entityId: uuid,
|
|
||||||
// The id of the sketch group.
|
|
||||||
id: uuid,
|
|
||||||
// What the sketch is on (can be a plane or a face).
|
|
||||||
on: {
|
|
||||||
// The id of the plane.
|
|
||||||
id: uuid,
|
|
||||||
// Origin of the plane.
|
|
||||||
origin: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
type: "plane",
|
|
||||||
// Type for a plane.
|
|
||||||
value: "XY" | "XZ" | "YZ" | "Custom",
|
|
||||||
// What should the plane’s X axis be?
|
|
||||||
xAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
// What should the plane’s Y axis be?
|
|
||||||
yAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
// The z-axis (normal).
|
|
||||||
zAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
} |
|
|
||||||
{
|
|
||||||
// The id of the face.
|
|
||||||
id: uuid,
|
|
||||||
// The original sketch group id of the object we are sketching on.
|
|
||||||
sketchGroupId: uuid,
|
|
||||||
type: "face",
|
|
||||||
// The tag of the face.
|
|
||||||
value: string,
|
|
||||||
// What should the face’s X axis be?
|
|
||||||
xAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
// What should the face’s Y axis be?
|
|
||||||
yAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
// The z-axis (normal).
|
|
||||||
zAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// The position of the sketch group.
|
|
||||||
position: [number, number, number],
|
|
||||||
// The rotation of the sketch group base plane.
|
|
||||||
rotation: [number, number, number, number],
|
|
||||||
// The starting path.
|
|
||||||
start: {
|
|
||||||
// The from point.
|
|
||||||
from: [number, number],
|
|
||||||
// The name of the path.
|
|
||||||
name: string,
|
|
||||||
// The to point.
|
|
||||||
to: [number, number],
|
|
||||||
},
|
|
||||||
// The paths in the sketch group.
|
|
||||||
value: [{
|
|
||||||
// The from point.
|
|
||||||
from: [number, number],
|
|
||||||
// The name of the path.
|
|
||||||
name: string,
|
|
||||||
// The to point.
|
|
||||||
to: [number, number],
|
|
||||||
type: "ToPoint",
|
|
||||||
} |
|
|
||||||
{
|
|
||||||
// arc's direction
|
|
||||||
ccw: string,
|
|
||||||
// the arc's center
|
|
||||||
center: [number, number],
|
|
||||||
// The from point.
|
|
||||||
from: [number, number],
|
|
||||||
// The name of the path.
|
|
||||||
name: string,
|
|
||||||
// The to point.
|
|
||||||
to: [number, number],
|
|
||||||
type: "TangentialArcTo",
|
|
||||||
} |
|
|
||||||
{
|
|
||||||
// The from point.
|
|
||||||
from: [number, number],
|
|
||||||
// The name of the path.
|
|
||||||
name: string,
|
|
||||||
// The to point.
|
|
||||||
to: [number, number],
|
|
||||||
type: "TangentialArc",
|
|
||||||
} |
|
|
||||||
{
|
|
||||||
// The from point.
|
|
||||||
from: [number, number],
|
|
||||||
// The name of the path.
|
|
||||||
name: string,
|
|
||||||
// The to point.
|
|
||||||
to: [number, number],
|
|
||||||
type: "Horizontal",
|
|
||||||
// The x coordinate.
|
|
||||||
x: number,
|
|
||||||
} |
|
|
||||||
{
|
|
||||||
// The from point.
|
|
||||||
from: [number, number],
|
|
||||||
// The name of the path.
|
|
||||||
name: string,
|
|
||||||
// The to point.
|
|
||||||
to: [number, number],
|
|
||||||
type: "AngledLineTo",
|
|
||||||
// The x coordinate.
|
|
||||||
x: number,
|
|
||||||
// The y coordinate.
|
|
||||||
y: number,
|
|
||||||
} |
|
|
||||||
{
|
|
||||||
// The from point.
|
|
||||||
from: [number, number],
|
|
||||||
// The name of the path.
|
|
||||||
name: string,
|
|
||||||
// The to point.
|
|
||||||
to: [number, number],
|
|
||||||
type: "Base",
|
|
||||||
}],
|
|
||||||
// The x-axis of the sketch group base plane in the 3D space
|
|
||||||
xAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
// The y-axis of the sketch group base plane in the 3D space
|
|
||||||
yAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
// The z-axis of the sketch group base plane in the 3D space
|
|
||||||
zAxis: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### sin
|
### sin
|
||||||
|
|
||||||
Computes the sine of a number (in radians).
|
Computes the sine of a number (in radians).
|
||||||
|
@ -4,6 +4,7 @@ import { getUtils } from './test-utils'
|
|||||||
import waitOn from 'wait-on'
|
import waitOn from 'wait-on'
|
||||||
import { Themes } from '../../src/lib/theme'
|
import { Themes } from '../../src/lib/theme'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
|
import { platform } from 'node:os'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||||
@ -16,9 +17,9 @@ document.addEventListener('mousemove', (e) =>
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const commonPoints = {
|
const commonPoints = {
|
||||||
startAt: '[0.93, -1.26]',
|
startAt: '[9.06, -12.22]',
|
||||||
num1: 0.95,
|
num1: 9.14,
|
||||||
num2: 1.88,
|
num2: 18.2,
|
||||||
}
|
}
|
||||||
|
|
||||||
test.beforeEach(async ({ context, page }) => {
|
test.beforeEach(async ({ context, page }) => {
|
||||||
@ -66,10 +67,8 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
|
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await u.doAndWaitForImageDiff(
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
await page.waitForTimeout(100)
|
||||||
200
|
|
||||||
)
|
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
@ -91,7 +90,6 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const num = 26.63
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
@ -102,13 +100,13 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1 - 0.01}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1 - 0.01}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
|> line([-${commonPoints.num2}, 0], %)`)
|
|> line([-${commonPoints.num2}, 0], %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
@ -133,10 +131,130 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %)
|
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %)
|
||||||
|> line([0, ${commonPoints.num1 - 0.01}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can moving camera', async ({ page, context }) => {
|
||||||
|
test.skip(process.platform === 'darwin', 'Can moving camera')
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
|
const camPos: [number, number, number] = [0, 85, 85]
|
||||||
|
const bakeInRetries = async (
|
||||||
|
mouseActions: any,
|
||||||
|
xyz: [number, number, number],
|
||||||
|
cnt = 0
|
||||||
|
) => {
|
||||||
|
// hack that we're implemented our own retry instead of using retries built into playwright.
|
||||||
|
// however each of these camera drags can be flaky, because of udp
|
||||||
|
// and so putting them together means only one needs to fail to make this test extra flaky.
|
||||||
|
// this way we can retry within the test
|
||||||
|
// We could break them out into separate tests, but the longest past of the test is waiting
|
||||||
|
// for the stream to start, so it can be good to bundle related things together.
|
||||||
|
|
||||||
|
await u.updateCamPosition(camPos)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
// rotate
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
// const yo = page.getByTestId('cam-x-position').inputValue()
|
||||||
|
|
||||||
|
await u.doAndWaitForImageDiff(async () => {
|
||||||
|
await mouseActions()
|
||||||
|
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
const vals = await Promise.all([
|
||||||
|
page.getByTestId('cam-x-position').inputValue(),
|
||||||
|
page.getByTestId('cam-y-position').inputValue(),
|
||||||
|
page.getByTestId('cam-z-position').inputValue(),
|
||||||
|
])
|
||||||
|
const xError = Math.abs(Number(vals[0]) + xyz[0])
|
||||||
|
const yError = Math.abs(Number(vals[1]) + xyz[1])
|
||||||
|
const zError = Math.abs(Number(vals[2]) + xyz[2])
|
||||||
|
|
||||||
|
let shouldRetry = false
|
||||||
|
|
||||||
|
if (xError > 5 || yError > 5 || zError > 5) {
|
||||||
|
if (cnt > 2) {
|
||||||
|
console.log('xVal', vals[0], 'xError', xError)
|
||||||
|
console.log('yVal', vals[1], 'yError', yError)
|
||||||
|
console.log('zVal', vals[2], 'zError', zError)
|
||||||
|
|
||||||
|
throw new Error('Camera position not as expected')
|
||||||
|
}
|
||||||
|
shouldRetry = true
|
||||||
|
}
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
|
||||||
|
}
|
||||||
|
await bakeInRetries(async () => {
|
||||||
|
await page.mouse.move(700, 200)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.move(600, 303)
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
}, [4, -10.5, -120])
|
||||||
|
|
||||||
|
await bakeInRetries(async () => {
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.mouse.move(600, 200)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.move(700, 200, { steps: 2 })
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
}, [-10, -85, -85])
|
||||||
|
|
||||||
|
await u.updateCamPosition(camPos)
|
||||||
|
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
|
||||||
|
// zoom
|
||||||
|
await u.doAndWaitForImageDiff(async () => {
|
||||||
|
await page.keyboard.down('Control')
|
||||||
|
await page.mouse.move(700, 400)
|
||||||
|
await page.mouse.down({ button: 'right' })
|
||||||
|
await page.mouse.move(700, 300)
|
||||||
|
await page.mouse.up({ button: 'right' })
|
||||||
|
await page.keyboard.up('Control')
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
// zoom with scroll
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
// TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom
|
||||||
|
// await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5)
|
||||||
|
// await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5)
|
||||||
|
// await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5)
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
|
||||||
|
await bakeInRetries(async () => {
|
||||||
|
await page.mouse.move(700, 400)
|
||||||
|
await page.mouse.wheel(0, -100)
|
||||||
|
}, [1, -94, -94])
|
||||||
|
})
|
||||||
|
|
||||||
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1000, height: 500 })
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
@ -492,13 +610,13 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1 - 0.01}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1 - 0.01}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
|> line([-${commonPoints.num2}, 0], %)`)
|
|> line([-${commonPoints.num2}, 0], %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
@ -625,12 +743,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
|||||||
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
||||||
await expect(themeOption).toBeVisible()
|
await expect(themeOption).toBeVisible()
|
||||||
await themeOption.click()
|
await themeOption.click()
|
||||||
const themeInput = page.getByPlaceholder('Select an option')
|
const themeInput = page.getByPlaceholder('system')
|
||||||
await expect(themeInput).toBeVisible()
|
await expect(themeInput).toBeVisible()
|
||||||
await expect(themeInput).toBeFocused()
|
await expect(themeInput).toBeFocused()
|
||||||
// Select dark theme
|
// Select dark theme
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowUp')
|
||||||
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
||||||
'data-headlessui-state',
|
'data-headlessui-state',
|
||||||
'active'
|
'active'
|
||||||
@ -771,12 +889,12 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1 - 0.01}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1 - 0.01}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
|> line([-${commonPoints.num2}, 0], %)`
|
|> line([-${commonPoints.num2}, 0], %)`
|
||||||
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
|
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
|
||||||
|
|
||||||
@ -998,7 +1116,6 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
@ -1056,3 +1173,160 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
|||||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can edit segments by dragging their handles', async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([4.61, -14.01], %)
|
||||||
|
|> line([12.73, -0.09], %)
|
||||||
|
|> tangentialArcTo([24.95, -5.38], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
|
||||||
|
const startPX = [652, 418]
|
||||||
|
const lineEndPX = [794, 416]
|
||||||
|
const arcEndPX = [893, 318]
|
||||||
|
|
||||||
|
const dragPX = 30
|
||||||
|
|
||||||
|
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
|
||||||
|
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
let prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
const step5 = { steps: 5 }
|
||||||
|
|
||||||
|
// drag startProfieAt handle
|
||||||
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
|
await page.mouse.up()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
// drag line handle
|
||||||
|
await page.mouse.move(lineEndPX[0] + dragPX, lineEndPX[1] - dragPX)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(
|
||||||
|
lineEndPX[0] + dragPX * 2,
|
||||||
|
lineEndPX[1] - dragPX * 2,
|
||||||
|
step5
|
||||||
|
)
|
||||||
|
await page.mouse.up()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
// drag tangentialArcTo handle
|
||||||
|
await page.mouse.move(arcEndPX[0], arcEndPX[1])
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(arcEndPX[0] + dragPX, arcEndPX[1] - dragPX, step5)
|
||||||
|
await page.mouse.up()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
|
||||||
|
// expect the code to have changed
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([7.01, -11.79], %)
|
||||||
|
|> line([14.69, 2.73], %)
|
||||||
|
|> tangentialArcTo([27.6, -3.25], %)`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Snap to close works (at any scale)', async ({ page }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
|
const doSnapAtDifferentScales = async (
|
||||||
|
camPos: [number, number, number],
|
||||||
|
expectedCode: string
|
||||||
|
) => {
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await u.updateCamPosition(camPos)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// select a plane
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
`const part001 = startSketchOn('XZ')`
|
||||||
|
)
|
||||||
|
|
||||||
|
let prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
const pointA = [700, 200]
|
||||||
|
const pointB = [900, 200]
|
||||||
|
const pointC = [900, 400]
|
||||||
|
|
||||||
|
// draw three lines
|
||||||
|
await page.mouse.click(pointA[0], pointA[1])
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.mouse.click(pointB[0], pointB[1])
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.mouse.click(pointC[0], pointC[1])
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.mouse.move(pointA[0] - 12, pointA[1] + 12)
|
||||||
|
const pointNotQuiteA = [pointA[0] - 7, pointA[1] + 7]
|
||||||
|
await page.mouse.move(pointNotQuiteA[0], pointNotQuiteA[1], { steps: 10 })
|
||||||
|
|
||||||
|
await page.mouse.click(pointNotQuiteA[0], pointNotQuiteA[1])
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
|
||||||
|
|
||||||
|
// exit sketch
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.removeCurrentCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeTemplate = (
|
||||||
|
scale = 1,
|
||||||
|
fudge = 0
|
||||||
|
) => `const part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|
||||||
|
|> line([${roundOff(scale * 175.36)}, 0], %)
|
||||||
|
|> line([0, -${roundOff(scale * 175.37) + fudge}], %)
|
||||||
|
|> close(%)`
|
||||||
|
|
||||||
|
await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01))
|
||||||
|
|
||||||
|
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate())
|
||||||
|
})
|
||||||
|
@ -29,98 +29,10 @@ test.beforeEach(async ({ context, page }) => {
|
|||||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test.setTimeout(60000)
|
test.setTimeout(60_000)
|
||||||
|
|
||||||
const commonPoints = {
|
|
||||||
startAt: '[26.38, -35.59]',
|
|
||||||
num1: 26.63,
|
|
||||||
num2: 53.01,
|
|
||||||
}
|
|
||||||
|
|
||||||
test('change camera, show planes', async ({ page, context }) => {
|
|
||||||
const u = getUtils(page)
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
|
||||||
await u.openAndClearDebugPanel()
|
|
||||||
|
|
||||||
const camPos: [number, number, number] = [0, 85, 85]
|
|
||||||
await u.updateCamPosition(camPos)
|
|
||||||
|
|
||||||
// rotate
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
await page.mouse.move(700, 200)
|
|
||||||
await page.mouse.down({ button: 'right' })
|
|
||||||
await page.mouse.move(600, 300)
|
|
||||||
await page.mouse.up({ button: 'right' })
|
|
||||||
|
|
||||||
await u.openDebugPanel()
|
|
||||||
await page.waitForTimeout(500)
|
|
||||||
await u.clearCommandLogs()
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
await expect(page).toHaveScreenshot({
|
|
||||||
maxDiffPixels: 100,
|
|
||||||
})
|
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
|
||||||
|
|
||||||
await u.updateCamPosition(camPos)
|
|
||||||
|
|
||||||
await u.clearCommandLogs()
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
// pan
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
await page.mouse.move(600, 200)
|
|
||||||
await page.mouse.down({ button: 'right' })
|
|
||||||
await page.mouse.move(700, 200)
|
|
||||||
await page.mouse.up({ button: 'right' })
|
|
||||||
await page.keyboard.up('Shift')
|
|
||||||
|
|
||||||
await u.openDebugPanel()
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
await u.clearCommandLogs()
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
await expect(page).toHaveScreenshot({
|
|
||||||
maxDiffPixels: 100,
|
|
||||||
})
|
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
|
||||||
|
|
||||||
await u.updateCamPosition(camPos)
|
|
||||||
|
|
||||||
await u.clearCommandLogs()
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
// zoom
|
|
||||||
await page.keyboard.down('Control')
|
|
||||||
await page.mouse.move(700, 400)
|
|
||||||
await page.mouse.down({ button: 'right' })
|
|
||||||
await page.mouse.move(700, 300)
|
|
||||||
await page.mouse.up({ button: 'right' })
|
|
||||||
await page.keyboard.up('Control')
|
|
||||||
|
|
||||||
await u.openDebugPanel()
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
await u.clearCommandLogs()
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
await expect(page).toHaveScreenshot({
|
|
||||||
maxDiffPixels: 100,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('exports of each format should work', async ({ page, context }) => {
|
test('exports of each format should work', async ({ page, context }) => {
|
||||||
|
test.setTimeout(120_000)
|
||||||
// FYI this test doesn't work with only engine running locally
|
// FYI this test doesn't work with only engine running locally
|
||||||
// And you will need to have the KittyCAD CLI installed
|
// And you will need to have the KittyCAD CLI installed
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
@ -179,8 +91,6 @@ const part001 = startSketchOn('-XZ')
|
|||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
await u.clearAndCloseDebugPanel()
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
await page.getByRole('button', { name: APP_NAME }).click()
|
|
||||||
|
|
||||||
interface Paths {
|
interface Paths {
|
||||||
modelPath: string
|
modelPath: string
|
||||||
imagePath: string
|
imagePath: string
|
||||||
@ -189,19 +99,21 @@ const part001 = startSketchOn('-XZ')
|
|||||||
const doExport = async (
|
const doExport = async (
|
||||||
output: Models['OutputFormat_type']
|
output: Models['OutputFormat_type']
|
||||||
): Promise<Paths> => {
|
): Promise<Paths> => {
|
||||||
await page.getByRole('button', { name: 'Export Model' }).click()
|
await page.getByRole('button', { name: APP_NAME }).click()
|
||||||
|
await page.getByRole('button', { name: 'Export Part' }).click()
|
||||||
const exportSelect = page.getByTestId('export-type')
|
|
||||||
await exportSelect.selectOption({ label: output.type })
|
|
||||||
|
|
||||||
|
// Go through export via command bar
|
||||||
|
await page.getByRole('option', { name: output.type, exact: false }).click()
|
||||||
if ('storage' in output) {
|
if ('storage' in output) {
|
||||||
const storageSelect = page.getByTestId('export-storage')
|
await page.getByRole('button', { name: 'storage', exact: false }).click()
|
||||||
await storageSelect.selectOption({ label: output.storage })
|
await page
|
||||||
|
.getByRole('option', { name: output.storage, exact: false })
|
||||||
|
.click()
|
||||||
}
|
}
|
||||||
|
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent('download')
|
// Handle download
|
||||||
await page.getByRole('button', { name: 'Export', exact: true }).click()
|
const download = await page.waitForEvent('download')
|
||||||
const download = await downloadPromise
|
|
||||||
const downloadLocationer = (extra = '', isImage = false) =>
|
const downloadLocationer = (extra = '', isImage = false) =>
|
||||||
`./e2e/playwright/export-snapshots/${output.type}-${
|
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||||
'storage' in output ? output.storage : ''
|
'storage' in output ? output.storage : ''
|
||||||
@ -384,13 +296,13 @@ test('extrude on each default plane should be stable', async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
||||||
|> startProfileAt([0.70, 0.44], %)
|
|> startProfileAt([7.00, 4.40], %)
|
||||||
|> line([0.66, -0.02], %)
|
|> line([6.60, -0.20], %)
|
||||||
|> line([0.28, 0.50], %)
|
|> line([2.80, 5.00], %)
|
||||||
|> line([-0.56, 0.44], %)
|
|> line([-5.60, 4.40], %)
|
||||||
|> line([-0.54, -0.38], %)
|
|> line([-5.40, -3.80], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(1.00, %)
|
|> extrude(10.00, %)
|
||||||
`
|
`
|
||||||
await context.addInitScript(async (code) => {
|
await context.addInitScript(async (code) => {
|
||||||
localStorage.setItem('persistCode', code)
|
localStorage.setItem('persistCode', code)
|
||||||
@ -484,7 +396,7 @@ test('Draft segments should look right', async ({ page, context }) => {
|
|||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([0.93, -1.26], %)`)
|
|> startProfileAt([9.06, -12.22], %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -498,8 +410,8 @@ test('Draft segments should look right', async ({ page, context }) => {
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([0.93, -1.26], %)
|
|> startProfileAt([9.06, -12.22], %)
|
||||||
|> line([0.95, 0], %)`)
|
|> line([9.14, 0], %)`)
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
|
|
||||||
@ -562,7 +474,7 @@ test('Client side scene scale should match engine scale inch', async ({
|
|||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([0.93, -1.26], %)`)
|
|> startProfileAt([9.06, -12.22], %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -572,8 +484,8 @@ test('Client side scene scale should match engine scale inch', async ({
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([0.93, -1.26], %)
|
|> startProfileAt([9.06, -12.22], %)
|
||||||
|> line([0.95, 0], %)`)
|
|> line([9.14, 0], %)`)
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
@ -582,9 +494,13 @@ test('Client side scene scale should match engine scale inch', async ({
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([0.93, -1.26], %)
|
|> startProfileAt([9.06, -12.22], %)
|
||||||
|> line([0.95, 0], %)
|
|> line([9.14, 0], %)
|
||||||
|> tangentialArcTo([2.82, -0.32], %)`)
|
|> tangentialArcTo([27.34, -3.08], %)`)
|
||||||
|
|
||||||
|
// click tangential arc tool again to unequip it
|
||||||
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// screen shot should show the sketch
|
// screen shot should show the sketch
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
@ -658,7 +574,7 @@ test('Client side scene scale should match engine scale mm', async ({
|
|||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([0.93, -1.26], %)`)
|
|> startProfileAt([230.03, -310.33], %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -668,8 +584,8 @@ test('Client side scene scale should match engine scale mm', async ({
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([0.93, -1.26], %)
|
|> startProfileAt([230.03, -310.33], %)
|
||||||
|> line([0.95, 0], %)`)
|
|> line([232.2, 0], %)`)
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
@ -678,9 +594,12 @@ test('Client side scene scale should match engine scale mm', async ({
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([0.93, -1.26], %)
|
|> startProfileAt([230.03, -310.33], %)
|
||||||
|> line([0.95, 0], %)
|
|> line([232.2, 0], %)
|
||||||
|> tangentialArcTo([2.82, -0.32], %)`)
|
|> tangentialArcTo([694.43, -78.12], %)`)
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// screen shot should show the sketch
|
// screen shot should show the sketch
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
@ -1,16 +1,23 @@
|
|||||||
import { expect, Page } from '@playwright/test'
|
import { expect, Page, errors } from '@playwright/test'
|
||||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import pixelMatch from 'pixelmatch'
|
import pixelMatch from 'pixelmatch'
|
||||||
import { PNG } from 'pngjs'
|
import { PNG } from 'pngjs'
|
||||||
|
|
||||||
async function waitForPageLoad(page: Page) {
|
async function waitForPageLoad(page: Page) {
|
||||||
// wait for 'Loading stream...' spinner
|
try {
|
||||||
await page.getByTestId('loading-stream').waitFor()
|
// wait for 'Loading stream...' spinner
|
||||||
// wait for all spinners to be gone
|
await page.getByTestId('loading-stream').waitFor()
|
||||||
await page.getByTestId('loading').waitFor({ state: 'detached' })
|
// wait for all spinners to be gone
|
||||||
|
await page.getByTestId('loading').waitFor({ state: 'detached' })
|
||||||
await page.getByTestId('start-sketch').waitFor()
|
await page.getByTestId('start-sketch').waitFor()
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof errors.TimeoutError) {
|
||||||
|
console.log('Timeout while waiting for page load.')
|
||||||
|
} else {
|
||||||
|
throw e // re-throw the error if it is not a TimeoutError
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeCurrentCode(page: Page) {
|
async function removeCurrentCode(page: Page) {
|
||||||
|
@ -1,16 +1,34 @@
|
|||||||
import requests
|
import re
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
|
webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
|
||||||
release_version = os.getenv('RELEASE_VERSION')
|
release_version = os.getenv('RELEASE_VERSION')
|
||||||
release_body = os.getenv('RELEASE_BODY')
|
release_body = os.getenv('RELEASE_BODY')
|
||||||
|
|
||||||
# message to send to Discord
|
# Regular expression to match URLs
|
||||||
|
url_pattern = r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)'
|
||||||
|
|
||||||
|
# Function to encase URLs in <>
|
||||||
|
def encase_urls_with_angle_brackets(match):
|
||||||
|
url = match.group(0)
|
||||||
|
return f'<{url}>'
|
||||||
|
|
||||||
|
# Replace all URLs in the release_body with their <> enclosed version
|
||||||
|
modified_release_body = re.sub(url_pattern, encase_urls_with_angle_brackets, release_body)
|
||||||
|
|
||||||
|
# Ensure the modified_release_body does not exceed Discord's character limit
|
||||||
|
max_length = 500 # Adjust as needed
|
||||||
|
if len(modified_release_body) > max_length:
|
||||||
|
modified_release_body = modified_release_body[:max_length].rsplit(' ', 1)[0] # Avoid cutting off in the middle of a word
|
||||||
|
modified_release_body += "... for full changelog, check out the link above."
|
||||||
|
|
||||||
|
# Message to send to Discord
|
||||||
data = {
|
data = {
|
||||||
"content":
|
"content":
|
||||||
f'''
|
f'''
|
||||||
**{release_version}** is now available! Check out the latest features and improvements here: https://zoo.dev/modeling-app/download
|
**{release_version}** is now available! Check out the latest features and improvements here: <https://zoo.dev/modeling-app/download>
|
||||||
{release_body}
|
{modified_release_body}
|
||||||
''',
|
''',
|
||||||
"username": "Modeling App Release Updates",
|
"username": "Modeling App Release Updates",
|
||||||
"avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png"
|
"avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png"
|
||||||
@ -23,4 +41,7 @@ response = requests.post(webhook_url, json=data)
|
|||||||
if response.status_code == 204:
|
if response.status_code == 204:
|
||||||
print("Successfully sent the message to Discord.")
|
print("Successfully sent the message to Discord.")
|
||||||
else:
|
else:
|
||||||
print("Failed to send the message to Discord.")
|
print(f"Failed to send the message to Discord. Status code: {response.status_code}, Response: {response.text}")
|
||||||
|
|
||||||
|
print(modified_release_body)
|
||||||
|
print(data["content"])
|
||||||
|
@ -7,7 +7,6 @@ use std::io::Read;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use oauth2::TokenResponse;
|
use oauth2::TokenResponse;
|
||||||
use std::process::Command;
|
|
||||||
use tauri::{InvokeError, Manager};
|
use tauri::{InvokeError, Manager};
|
||||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||||
|
|
||||||
|
@ -3,15 +3,8 @@ import {
|
|||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
Outlet,
|
Outlet,
|
||||||
redirect,
|
redirect,
|
||||||
useLocation,
|
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import {
|
|
||||||
matchRoutes,
|
|
||||||
createRoutesFromChildren,
|
|
||||||
useNavigationType,
|
|
||||||
} from 'react-router'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { ErrorPage } from './components/ErrorPage'
|
import { ErrorPage } from './components/ErrorPage'
|
||||||
import { Settings } from './routes/Settings'
|
import { Settings } from './routes/Settings'
|
||||||
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||||
|
@ -16,7 +16,11 @@ import {
|
|||||||
SKETCH_LAYER,
|
SKETCH_LAYER,
|
||||||
ZOOM_MAGIC_NUMBER,
|
ZOOM_MAGIC_NUMBER,
|
||||||
} from './sceneInfra'
|
} from './sceneInfra'
|
||||||
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
|
import {
|
||||||
|
EngineCommand,
|
||||||
|
Subscription,
|
||||||
|
engineCommandManager,
|
||||||
|
} from 'lang/std/engineConnection'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { deg2Rad } from 'lib/utils2d'
|
import { deg2Rad } from 'lib/utils2d'
|
||||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||||
@ -28,6 +32,12 @@ const FRAMES_TO_ANIMATE_IN = 30
|
|||||||
|
|
||||||
const tempQuaternion = new Quaternion() // just used for maths
|
const tempQuaternion = new Quaternion() // just used for maths
|
||||||
|
|
||||||
|
type interactionType = 'pan' | 'rotate' | 'zoom'
|
||||||
|
|
||||||
|
const throttledEngCmd = throttle((cmd: EngineCommand) => {
|
||||||
|
engineCommandManager.sendSceneCommand(cmd)
|
||||||
|
}, 1000 / 15)
|
||||||
|
|
||||||
interface ThreeCamValues {
|
interface ThreeCamValues {
|
||||||
position: Vector3
|
position: Vector3
|
||||||
quaternion: Quaternion
|
quaternion: Quaternion
|
||||||
@ -110,10 +120,11 @@ const throttledUpdateEngineFov = throttle(
|
|||||||
lastCmdDelay
|
lastCmdDelay
|
||||||
) as any as number
|
) as any as number
|
||||||
},
|
},
|
||||||
1000 / 15
|
1000 / 30
|
||||||
)
|
)
|
||||||
|
|
||||||
export class CameraControls {
|
export class CameraControls {
|
||||||
|
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
||||||
camera: PerspectiveCamera | OrthographicCamera
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
target: Vector3
|
target: Vector3
|
||||||
domElement: HTMLCanvasElement
|
domElement: HTMLCanvasElement
|
||||||
@ -198,6 +209,7 @@ export class CameraControls {
|
|||||||
this.camera.zoom = camProps.zoom || 1
|
this.camera.zoom = camProps.zoom || 1
|
||||||
}
|
}
|
||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
|
console.log('doing this thing', camProps)
|
||||||
this.update(true)
|
this.update(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +233,45 @@ export class CameraControls {
|
|||||||
|
|
||||||
this.update()
|
this.update()
|
||||||
this._usePerspectiveCamera()
|
this._usePerspectiveCamera()
|
||||||
|
|
||||||
|
const cb: Subscription<
|
||||||
|
'default_camera_zoom' | 'camera_drag_end' | 'default_camera_get_settings'
|
||||||
|
>['callback'] = ({ data, type }) => {
|
||||||
|
const camSettings = data.settings
|
||||||
|
this.camera.position.set(
|
||||||
|
camSettings.pos.x,
|
||||||
|
camSettings.pos.y,
|
||||||
|
camSettings.pos.z
|
||||||
|
)
|
||||||
|
this.target.set(
|
||||||
|
camSettings.center.x,
|
||||||
|
camSettings.center.y,
|
||||||
|
camSettings.center.z
|
||||||
|
)
|
||||||
|
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
|
||||||
|
this.camera.fov = camSettings.fov_y
|
||||||
|
} else if (
|
||||||
|
this.camera instanceof OrthographicCamera &&
|
||||||
|
camSettings.ortho_scale
|
||||||
|
) {
|
||||||
|
this.camera.zoom = camSettings.ortho_scale
|
||||||
|
}
|
||||||
|
this.onCameraChange()
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
engineCommandManager.subscribeTo({
|
||||||
|
event: 'camera_drag_end',
|
||||||
|
callback: cb,
|
||||||
|
})
|
||||||
|
engineCommandManager.subscribeTo({
|
||||||
|
event: 'default_camera_zoom',
|
||||||
|
callback: cb,
|
||||||
|
})
|
||||||
|
engineCommandManager.subscribeTo({
|
||||||
|
event: 'default_camera_get_settings',
|
||||||
|
callback: cb,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
|
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
|
||||||
@ -254,7 +305,21 @@ export class CameraControls {
|
|||||||
onMouseDown = (event: MouseEvent) => {
|
onMouseDown = (event: MouseEvent) => {
|
||||||
this.isDragging = true
|
this.isDragging = true
|
||||||
this.mouseDownPosition.set(event.clientX, event.clientY)
|
this.mouseDownPosition.set(event.clientX, event.clientY)
|
||||||
|
let interaction = this.getInteractionType(event)
|
||||||
|
if (interaction === 'none') return
|
||||||
this.handleStart()
|
this.handleStart()
|
||||||
|
|
||||||
|
if (this.syncDirection === 'engineToClient') {
|
||||||
|
void engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_start',
|
||||||
|
interaction,
|
||||||
|
window: { x: event.clientX, y: event.clientY },
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove = (event: MouseEvent) => {
|
onMouseMove = (event: MouseEvent) => {
|
||||||
@ -265,36 +330,34 @@ export class CameraControls {
|
|||||||
.sub(this.mouseDownPosition)
|
.sub(this.mouseDownPosition)
|
||||||
this.mouseDownPosition.copy(this.mouseNewPosition)
|
this.mouseDownPosition.copy(this.mouseNewPosition)
|
||||||
|
|
||||||
let state: 'pan' | 'rotate' | 'zoom' = 'pan'
|
const interaction = this.getInteractionType(event)
|
||||||
|
if (interaction === 'none') return
|
||||||
|
|
||||||
if (this.interactionGuards.pan.callback(event as any)) {
|
if (this.syncDirection === 'engineToClient') {
|
||||||
if (this.enablePan === false) return
|
throttledEngCmd({
|
||||||
// handleMouseDownPan(event)
|
type: 'modeling_cmd_req',
|
||||||
state = 'pan'
|
cmd: {
|
||||||
} else if (this.interactionGuards.rotate.callback(event as any)) {
|
type: 'camera_drag_move',
|
||||||
if (this.enableRotate === false) return
|
interaction,
|
||||||
// handleMouseDownRotate(event)
|
window: { x: event.clientX, y: event.clientY },
|
||||||
state = 'rotate'
|
},
|
||||||
} else if (this.interactionGuards.zoom.dragCallback(event as any)) {
|
cmd_id: uuidv4(),
|
||||||
if (this.enableZoom === false) return
|
})
|
||||||
// handleMouseDownDolly(event)
|
|
||||||
state = 'zoom'
|
|
||||||
} else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement camera movement logic here based on deltaMove
|
// Implement camera movement logic here based on deltaMove
|
||||||
// For example, for rotating the camera around the target:
|
// For example, for rotating the camera around the target:
|
||||||
if (state === 'rotate') {
|
if (interaction === 'rotate') {
|
||||||
this.pendingRotation = this.pendingRotation
|
this.pendingRotation = this.pendingRotation
|
||||||
? this.pendingRotation
|
? this.pendingRotation
|
||||||
: new Vector2()
|
: new Vector2()
|
||||||
this.pendingRotation.x += deltaMove.x
|
this.pendingRotation.x += deltaMove.x
|
||||||
this.pendingRotation.y += deltaMove.y
|
this.pendingRotation.y += deltaMove.y
|
||||||
} else if (state === 'zoom') {
|
} else if (interaction === 'zoom') {
|
||||||
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||||
this.pendingZoom *= 1 + deltaMove.y * 0.01
|
this.pendingZoom *= 1 + deltaMove.y * 0.01
|
||||||
} else if (state === 'pan') {
|
} else if (interaction === 'pan') {
|
||||||
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
|
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
|
||||||
let distance = this.camera.position.distanceTo(this.target)
|
let distance = this.camera.position.distanceTo(this.target)
|
||||||
if (this.camera instanceof OrthographicCamera) {
|
if (this.camera instanceof OrthographicCamera) {
|
||||||
@ -311,11 +374,45 @@ export class CameraControls {
|
|||||||
onMouseUp = (event: MouseEvent) => {
|
onMouseUp = (event: MouseEvent) => {
|
||||||
this.isDragging = false
|
this.isDragging = false
|
||||||
this.handleEnd()
|
this.handleEnd()
|
||||||
|
if (this.syncDirection === 'engineToClient') {
|
||||||
|
const interaction = this.getInteractionType(event)
|
||||||
|
if (interaction === 'none') return
|
||||||
|
void engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_end',
|
||||||
|
interaction,
|
||||||
|
window: { x: event.clientX, y: event.clientY },
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseWheel = (event: WheelEvent) => {
|
onMouseWheel = (event: WheelEvent) => {
|
||||||
// Assume trackpad if the deltas are small and integers
|
// Assume trackpad if the deltas are small and integers
|
||||||
this.handleStart()
|
this.handleStart()
|
||||||
|
|
||||||
|
if (this.syncDirection === 'engineToClient') {
|
||||||
|
const interactions = this.interactionGuards.zoom.scrollCallback(
|
||||||
|
event as any
|
||||||
|
)
|
||||||
|
if (!interactions) {
|
||||||
|
this.handleEnd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throttledEngCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_zoom',
|
||||||
|
magnitude: -event.deltaY * 0.4,
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
this.handleEnd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
|
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
|
||||||
|
|
||||||
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
|
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
|
||||||
@ -473,7 +570,7 @@ export class CameraControls {
|
|||||||
update = (forceUpdate = false) => {
|
update = (forceUpdate = false) => {
|
||||||
// If there are any changes that need to be applied to the camera, apply them here.
|
// If there are any changes that need to be applied to the camera, apply them here.
|
||||||
|
|
||||||
let didChange = forceUpdate
|
let didChange = false
|
||||||
if (this.pendingRotation) {
|
if (this.pendingRotation) {
|
||||||
this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y)
|
this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y)
|
||||||
this.pendingRotation = null // Clear the pending rotation after applying it
|
this.pendingRotation = null // Clear the pending rotation after applying it
|
||||||
@ -525,8 +622,8 @@ export class CameraControls {
|
|||||||
|
|
||||||
// Update the camera's matrices
|
// Update the camera's matrices
|
||||||
this.camera.updateMatrixWorld()
|
this.camera.updateMatrixWorld()
|
||||||
if (didChange) {
|
if (didChange || forceUpdate) {
|
||||||
this.onCameraChange()
|
this.onCameraChange(forceUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// damping would be implemented here in update if we choose to add it.
|
// damping would be implemented here in update if we choose to add it.
|
||||||
@ -637,6 +734,10 @@ export class CameraControls {
|
|||||||
duration = 500,
|
duration = 500,
|
||||||
toOrthographic = true
|
toOrthographic = true
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (this.syncDirection === 'engineToClient')
|
||||||
|
console.warn(
|
||||||
|
'tweenCameraToQuaternion not design to work with engineToClient syncDirection.'
|
||||||
|
)
|
||||||
const isVertical = isQuaternionVertical(targetQuaternion)
|
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||||
let remainingDuration = duration
|
let remainingDuration = duration
|
||||||
if (isVertical) {
|
if (isVertical) {
|
||||||
@ -719,6 +820,10 @@ export class CameraControls {
|
|||||||
|
|
||||||
animateToOrthographic = () =>
|
animateToOrthographic = () =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
|
if (this.syncDirection === 'engineToClient')
|
||||||
|
console.warn(
|
||||||
|
'animate To Orthographic not design to work with engineToClient syncDirection.'
|
||||||
|
)
|
||||||
this.isFovAnimationInProgress = true
|
this.isFovAnimationInProgress = true
|
||||||
let currentFov = this.lastPerspectiveFov
|
let currentFov = this.lastPerspectiveFov
|
||||||
this.fovBeforeOrtho = currentFov
|
this.fovBeforeOrtho = currentFov
|
||||||
@ -752,6 +857,10 @@ export class CameraControls {
|
|||||||
})
|
})
|
||||||
animateToPerspective = () =>
|
animateToPerspective = () =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
|
if (this.syncDirection === 'engineToClient')
|
||||||
|
console.warn(
|
||||||
|
'animate To Perspective not design to work with engineToClient syncDirection.'
|
||||||
|
)
|
||||||
this.isFovAnimationInProgress = true
|
this.isFovAnimationInProgress = true
|
||||||
// Immediately set the camera to perspective with a very low FOV
|
// Immediately set the camera to perspective with a very low FOV
|
||||||
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
|
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
|
||||||
@ -790,7 +899,7 @@ export class CameraControls {
|
|||||||
this.reactCameraPropertiesCallback(a)
|
this.reactCameraPropertiesCallback(a)
|
||||||
}, 200)
|
}, 200)
|
||||||
|
|
||||||
onCameraChange = () => {
|
onCameraChange = (forceUpdate = false) => {
|
||||||
const distance = this.target.distanceTo(this.camera.position)
|
const distance = this.target.distanceTo(this.camera.position)
|
||||||
if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) {
|
if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) {
|
||||||
this.camera.far = distance * 2
|
this.camera.far = distance * 2
|
||||||
@ -798,13 +907,14 @@ export class CameraControls {
|
|||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
}
|
}
|
||||||
|
|
||||||
throttledUpdateEngineCamera({
|
if (this.syncDirection === 'clientToEngine' || forceUpdate)
|
||||||
quaternion: this.camera.quaternion,
|
throttledUpdateEngineCamera({
|
||||||
position: this.camera.position,
|
quaternion: this.camera.quaternion,
|
||||||
zoom: this.camera.zoom,
|
position: this.camera.position,
|
||||||
isPerspective: this.isPerspective,
|
zoom: this.camera.zoom,
|
||||||
target: this.target,
|
isPerspective: this.isPerspective,
|
||||||
})
|
target: this.target,
|
||||||
|
})
|
||||||
this.deferReactUpdate({
|
this.deferReactUpdate({
|
||||||
type: this.isPerspective ? 'perspective' : 'orthographic',
|
type: this.isPerspective ? 'perspective' : 'orthographic',
|
||||||
[this.isPerspective ? 'fov' : 'zoom']:
|
[this.isPerspective ? 'fov' : 'zoom']:
|
||||||
@ -825,9 +935,18 @@ export class CameraControls {
|
|||||||
})
|
})
|
||||||
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
||||||
}
|
}
|
||||||
|
getInteractionType = (event: any) =>
|
||||||
|
_getInteractionType(
|
||||||
|
this.interactionGuards,
|
||||||
|
event,
|
||||||
|
this.enablePan,
|
||||||
|
this.enableRotate,
|
||||||
|
this.enableZoom
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently duplicated, delete one
|
// Pure function helpers
|
||||||
|
|
||||||
function calculateNearFarFromFOV(fov: number) {
|
function calculateNearFarFromFOV(fov: number) {
|
||||||
const nearFarRatio = (fov - 3) / (45 - 3)
|
const nearFarRatio = (fov - 3) / (45 - 3)
|
||||||
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
||||||
@ -835,7 +954,6 @@ function calculateNearFarFromFOV(fov: number) {
|
|||||||
return { z_near: 0.1, z_far }
|
return { z_near: 0.1, z_far }
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently duplicated, delete one
|
|
||||||
function convertThreeCamValuesToEngineCam({
|
function convertThreeCamValuesToEngineCam({
|
||||||
target,
|
target,
|
||||||
position,
|
position,
|
||||||
@ -876,8 +994,6 @@ function convertThreeCamValuesToEngineCam({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pure function helpers
|
|
||||||
|
|
||||||
function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
|
function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
|
||||||
// Direction from position to target, normalized.
|
// Direction from position to target, normalized.
|
||||||
let direction = new Vector3().subVectors(target, position).normalize()
|
let direction = new Vector3().subVectors(target, position).normalize()
|
||||||
@ -896,3 +1012,17 @@ function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
|
|||||||
|
|
||||||
return quaternion
|
return quaternion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _getInteractionType(
|
||||||
|
interactionGuards: MouseGuard,
|
||||||
|
event: any,
|
||||||
|
enablePan: boolean,
|
||||||
|
enableRotate: boolean,
|
||||||
|
enableZoom: boolean
|
||||||
|
): interactionType | 'none' {
|
||||||
|
let state: interactionType | 'none' = 'none'
|
||||||
|
if (enablePan && interactionGuards.pan.callback(event)) return 'pan'
|
||||||
|
if (enableRotate && interactionGuards.rotate.callback(event)) return 'rotate'
|
||||||
|
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
@ -3,10 +3,13 @@ import {
|
|||||||
DoubleSide,
|
DoubleSide,
|
||||||
ExtrudeGeometry,
|
ExtrudeGeometry,
|
||||||
Group,
|
Group,
|
||||||
|
Intersection,
|
||||||
LineCurve3,
|
LineCurve3,
|
||||||
Matrix4,
|
Matrix4,
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
|
Object3D,
|
||||||
|
Object3DEventMap,
|
||||||
OrthographicCamera,
|
OrthographicCamera,
|
||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
PlaneGeometry,
|
PlaneGeometry,
|
||||||
@ -24,6 +27,7 @@ import {
|
|||||||
defaultPlaneColor,
|
defaultPlaneColor,
|
||||||
getSceneScale,
|
getSceneScale,
|
||||||
INTERSECTION_PLANE_LAYER,
|
INTERSECTION_PLANE_LAYER,
|
||||||
|
OnMouseEnterLeaveArgs,
|
||||||
RAYCASTABLE_PLANE,
|
RAYCASTABLE_PLANE,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
SKETCH_GROUP_SEGMENTS,
|
SKETCH_GROUP_SEGMENTS,
|
||||||
@ -56,6 +60,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
|
|||||||
import {
|
import {
|
||||||
createArcGeometry,
|
createArcGeometry,
|
||||||
dashedStraight,
|
dashedStraight,
|
||||||
|
profileStart,
|
||||||
straightSegment,
|
straightSegment,
|
||||||
tangentialArcToSegment,
|
tangentialArcToSegment,
|
||||||
} from './segments'
|
} from './segments'
|
||||||
@ -63,7 +68,7 @@ import {
|
|||||||
addCloseToPipe,
|
addCloseToPipe,
|
||||||
addNewSketchLn,
|
addNewSketchLn,
|
||||||
changeSketchArguments,
|
changeSketchArguments,
|
||||||
compareVec2Epsilon2,
|
updateStartProfileAtArgs,
|
||||||
} from 'lang/std/sketch'
|
} from 'lang/std/sketch'
|
||||||
import { isReducedMotion, throttle } from 'lib/utils'
|
import { isReducedMotion, throttle } from 'lib/utils'
|
||||||
import {
|
import {
|
||||||
@ -85,6 +90,7 @@ export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
|||||||
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
||||||
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
||||||
'tangential-arc-to-segment-body-dashed'
|
'tangential-arc-to-segment-body-dashed'
|
||||||
|
export const PROFILE_START = 'profile-start'
|
||||||
|
|
||||||
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
||||||
// That mostly mean sketch elements.
|
// That mostly mean sketch elements.
|
||||||
@ -137,6 +143,9 @@ class SceneEntities {
|
|||||||
scale: factor,
|
scale: factor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (segment.name === PROFILE_START) {
|
||||||
|
segment.scale.set(factor, factor, factor)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if (this.axisGroup) {
|
if (this.axisGroup) {
|
||||||
const factor =
|
const factor =
|
||||||
@ -293,14 +302,51 @@ class SceneEntities {
|
|||||||
? orthoFactor
|
? orthoFactor
|
||||||
: perspScale(sceneInfra.camControls.camera, dummy)) /
|
: perspScale(sceneInfra.camControls.camera, dummy)) /
|
||||||
sceneInfra._baseUnitMultiplier
|
sceneInfra._baseUnitMultiplier
|
||||||
|
|
||||||
|
const segPathToNode = getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
sketchGroup.start.__geoMeta.sourceRange
|
||||||
|
)
|
||||||
|
const _profileStart = profileStart({
|
||||||
|
from: sketchGroup.start.from,
|
||||||
|
id: sketchGroup.start.__geoMeta.id,
|
||||||
|
pathToNode: segPathToNode,
|
||||||
|
scale: factor,
|
||||||
|
})
|
||||||
|
_profileStart.layers.set(SKETCH_LAYER)
|
||||||
|
_profileStart.traverse((child) => {
|
||||||
|
child.layers.set(SKETCH_LAYER)
|
||||||
|
})
|
||||||
|
group.add(_profileStart)
|
||||||
|
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
||||||
|
|
||||||
sketchGroup.value.forEach((segment, index) => {
|
sketchGroup.value.forEach((segment, index) => {
|
||||||
let segPathToNode = getNodePathFromSourceRange(
|
let segPathToNode = getNodePathFromSourceRange(
|
||||||
draftSegment ? truncatedAst : kclManager.ast,
|
kclManager.ast,
|
||||||
segment.__geoMeta.sourceRange
|
segment.__geoMeta.sourceRange
|
||||||
)
|
)
|
||||||
|
if (draftSegment && (sketchGroup.value[index - 1] || sketchGroup.start)) {
|
||||||
|
const previousSegment =
|
||||||
|
sketchGroup.value[index - 1] || sketchGroup.start
|
||||||
|
const previousSegmentPathToNode = getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
previousSegment.__geoMeta.sourceRange
|
||||||
|
)
|
||||||
|
const bodyIndex = previousSegmentPathToNode[1][0]
|
||||||
|
segPathToNode = getNodePathFromSourceRange(
|
||||||
|
truncatedAst,
|
||||||
|
segment.__geoMeta.sourceRange
|
||||||
|
)
|
||||||
|
segPathToNode[1][0] = bodyIndex
|
||||||
|
}
|
||||||
const isDraftSegment =
|
const isDraftSegment =
|
||||||
draftSegment && index === sketchGroup.value.length - 1
|
draftSegment && index === sketchGroup.value.length - 1
|
||||||
let seg
|
let seg
|
||||||
|
const callExpName = getNodeFromPath<CallExpression>(
|
||||||
|
kclManager.ast,
|
||||||
|
segPathToNode,
|
||||||
|
'CallExpression'
|
||||||
|
)?.node?.callee?.name
|
||||||
if (segment.type === 'TangentialArcTo') {
|
if (segment.type === 'TangentialArcTo') {
|
||||||
seg = tangentialArcToSegment({
|
seg = tangentialArcToSegment({
|
||||||
prevSegment: sketchGroup.value[index - 1],
|
prevSegment: sketchGroup.value[index - 1],
|
||||||
@ -319,6 +365,7 @@ class SceneEntities {
|
|||||||
pathToNode: segPathToNode,
|
pathToNode: segPathToNode,
|
||||||
isDraftSegment,
|
isDraftSegment,
|
||||||
scale: factor,
|
scale: factor,
|
||||||
|
callExpName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
seg.layers.set(SKETCH_LAYER)
|
seg.layers.set(SKETCH_LAYER)
|
||||||
@ -340,17 +387,19 @@ class SceneEntities {
|
|||||||
this.scene.add(group)
|
this.scene.add(group)
|
||||||
if (!draftSegment) {
|
if (!draftSegment) {
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onDrag: (args) => {
|
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
|
||||||
if (args.event.which !== 1) return
|
if (mouseEvent.which !== 1) return
|
||||||
this.onDragSegment({
|
this.onDragSegment({
|
||||||
...args,
|
object: selected,
|
||||||
|
intersection2d: intersectionPoint.twoD,
|
||||||
|
intersects,
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onMove: () => {},
|
onMove: () => {},
|
||||||
onClick: (args) => {
|
onClick: (args) => {
|
||||||
if (args?.event.which !== 1) return
|
if (args?.mouseEvent.which !== 1) return
|
||||||
if (!args || !args.object) {
|
if (!args || !args.selected) {
|
||||||
sceneInfra.modelingSend({
|
sceneInfra.modelingSend({
|
||||||
type: 'Set selection',
|
type: 'Set selection',
|
||||||
data: {
|
data: {
|
||||||
@ -359,73 +408,32 @@ class SceneEntities {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { object } = args
|
const { selected } = args
|
||||||
const event = getEventForSegmentSelection(object)
|
const event = getEventForSegmentSelection(selected)
|
||||||
if (!event) return
|
if (!event) return
|
||||||
sceneInfra.modelingSend(event)
|
sceneInfra.modelingSend(event)
|
||||||
},
|
},
|
||||||
onMouseEnter: ({ object }) => {
|
...mouseEnterLeaveCallbacks(),
|
||||||
// TODO change the color of the segment to yellow?
|
|
||||||
// Give a few pixels grace around each of the segments
|
|
||||||
// for hover.
|
|
||||||
if ([X_AXIS, Y_AXIS].includes(object?.userData?.type)) {
|
|
||||||
const obj = object as Mesh
|
|
||||||
const mat = obj.material as MeshBasicMaterial
|
|
||||||
mat.color.set(obj.userData.baseColor)
|
|
||||||
mat.color.offsetHSL(0, 0, 0.5)
|
|
||||||
}
|
|
||||||
const parent = getParentGroup(object)
|
|
||||||
if (parent?.userData?.pathToNode) {
|
|
||||||
const updatedAst = parse(recast(kclManager.ast))
|
|
||||||
const node = getNodeFromPath<CallExpression>(
|
|
||||||
updatedAst,
|
|
||||||
parent.userData.pathToNode,
|
|
||||||
'CallExpression'
|
|
||||||
).node
|
|
||||||
sceneInfra.highlightCallback([node.start, node.end])
|
|
||||||
const yellow = 0xffff00
|
|
||||||
colorSegment(object, yellow)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sceneInfra.highlightCallback([0, 0])
|
|
||||||
},
|
|
||||||
onMouseLeave: ({ object }) => {
|
|
||||||
sceneInfra.highlightCallback([0, 0])
|
|
||||||
const parent = getParentGroup(object)
|
|
||||||
const isSelected = parent?.userData?.isSelected
|
|
||||||
colorSegment(object, isSelected ? 0x0000ff : 0xffffff)
|
|
||||||
if ([X_AXIS, Y_AXIS].includes(object?.userData?.type)) {
|
|
||||||
const obj = object as Mesh
|
|
||||||
const mat = obj.material as MeshBasicMaterial
|
|
||||||
mat.color.set(obj.userData.baseColor)
|
|
||||||
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onDrag: () => {},
|
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
if (!args) return
|
if (!args) return
|
||||||
if (args.event.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
const { intersection2d } = args
|
const { intersectionPoint } = args
|
||||||
if (!intersection2d) return
|
let intersection2d = intersectionPoint?.twoD
|
||||||
|
const profileStart = args.intersects
|
||||||
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
||||||
|
.find((a) => a?.name === PROFILE_START)
|
||||||
|
|
||||||
const firstSeg = sketchGroup.value[0]
|
|
||||||
const isClosingSketch = compareVec2Epsilon2(
|
|
||||||
firstSeg.from,
|
|
||||||
[intersection2d.x, intersection2d.y],
|
|
||||||
0.5
|
|
||||||
)
|
|
||||||
let modifiedAst
|
let modifiedAst
|
||||||
if (isClosingSketch) {
|
if (profileStart) {
|
||||||
// TODO close needs a better UX
|
|
||||||
modifiedAst = addCloseToPipe({
|
modifiedAst = addCloseToPipe({
|
||||||
node: kclManager.ast,
|
node: kclManager.ast,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
})
|
})
|
||||||
} else {
|
} else if (intersection2d) {
|
||||||
const lastSegment = sketchGroup.value.slice(-1)[0]
|
const lastSegment = sketchGroup.value.slice(-1)[0]
|
||||||
modifiedAst = addNewSketchLn({
|
modifiedAst = addNewSketchLn({
|
||||||
node: kclManager.ast,
|
node: kclManager.ast,
|
||||||
@ -438,6 +446,9 @@ class SceneEntities {
|
|||||||
: 'line',
|
: 'line',
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
}).modifiedAst
|
}).modifiedAst
|
||||||
|
} else {
|
||||||
|
// return early as we didn't modify the ast
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
kclManager.executeAstMock(modifiedAst, { updates: 'code' })
|
kclManager.executeAstMock(modifiedAst, { updates: 'code' })
|
||||||
@ -446,8 +457,9 @@ class SceneEntities {
|
|||||||
},
|
},
|
||||||
onMove: (args) => {
|
onMove: (args) => {
|
||||||
this.onDragSegment({
|
this.onDragSegment({
|
||||||
...args,
|
intersection2d: args.intersectionPoint.twoD,
|
||||||
object: Object.values(this.activeSegments).slice(-1)[0],
|
object: Object.values(this.activeSegments).slice(-1)[0],
|
||||||
|
intersects: args.intersects,
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
draftInfo: {
|
draftInfo: {
|
||||||
draftSegment,
|
draftSegment,
|
||||||
@ -457,6 +469,7 @@ class SceneEntities {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
...mouseEnterLeaveCallbacks(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sceneInfra.camControls.enableRotate = false
|
sceneInfra.camControls.enableRotate = false
|
||||||
@ -493,17 +506,15 @@ class SceneEntities {
|
|||||||
)
|
)
|
||||||
onDragSegment({
|
onDragSegment({
|
||||||
object,
|
object,
|
||||||
event,
|
intersection2d: _intersection2d,
|
||||||
intersectPoint,
|
|
||||||
intersection2d,
|
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
draftInfo,
|
draftInfo,
|
||||||
|
intersects,
|
||||||
}: {
|
}: {
|
||||||
object: any
|
object: any
|
||||||
event: any
|
|
||||||
intersectPoint: Vector3
|
|
||||||
intersection2d: Vector2
|
intersection2d: Vector2
|
||||||
sketchPathToNode: PathToNode
|
sketchPathToNode: PathToNode
|
||||||
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
||||||
draftInfo?: {
|
draftInfo?: {
|
||||||
draftSegment: DraftSegment
|
draftSegment: DraftSegment
|
||||||
truncatedAst: Program
|
truncatedAst: Program
|
||||||
@ -511,7 +522,20 @@ class SceneEntities {
|
|||||||
variableDeclarationName: string
|
variableDeclarationName: string
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const group = getParentGroup(object)
|
const profileStart =
|
||||||
|
draftInfo &&
|
||||||
|
intersects
|
||||||
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
||||||
|
.find((a) => a?.name === PROFILE_START)
|
||||||
|
const intersection2d = profileStart
|
||||||
|
? new Vector2(profileStart.position.x, profileStart.position.y)
|
||||||
|
: _intersection2d
|
||||||
|
|
||||||
|
const group = getParentGroup(object, [
|
||||||
|
STRAIGHT_SEGMENT,
|
||||||
|
TANGENTIAL_ARC_TO_SEGMENT,
|
||||||
|
PROFILE_START,
|
||||||
|
])
|
||||||
if (!group) return
|
if (!group) return
|
||||||
const pathToNode: PathToNode = JSON.parse(
|
const pathToNode: PathToNode = JSON.parse(
|
||||||
JSON.stringify(group.userData.pathToNode)
|
JSON.stringify(group.userData.pathToNode)
|
||||||
@ -535,13 +559,28 @@ class SceneEntities {
|
|||||||
).node
|
).node
|
||||||
if (node.type !== 'CallExpression') return
|
if (node.type !== 'CallExpression') return
|
||||||
|
|
||||||
const modded = changeSketchArguments(
|
let modded: {
|
||||||
modifiedAst,
|
modifiedAst: Program
|
||||||
kclManager.programMemory,
|
pathToNode: PathToNode
|
||||||
[node.start, node.end],
|
}
|
||||||
to,
|
if (group.name === PROFILE_START) {
|
||||||
from
|
modded = updateStartProfileAtArgs({
|
||||||
)
|
node: modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
previousProgramMemory: kclManager.programMemory,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
modded = changeSketchArguments(
|
||||||
|
modifiedAst,
|
||||||
|
kclManager.programMemory,
|
||||||
|
[node.start, node.end],
|
||||||
|
to,
|
||||||
|
from
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
modifiedAst = modded.modifiedAst
|
modifiedAst = modded.modifiedAst
|
||||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||||
draftInfo
|
draftInfo
|
||||||
@ -560,10 +599,16 @@ class SceneEntities {
|
|||||||
programMemoryOverride,
|
programMemoryOverride,
|
||||||
})
|
})
|
||||||
this.sceneProgramMemory = programMemory
|
this.sceneProgramMemory = programMemory
|
||||||
const sketchGroup = programMemory.root[variableDeclarationName]
|
const sketchGroup = programMemory.root[
|
||||||
.value as Path[]
|
variableDeclarationName
|
||||||
|
] as SketchGroup
|
||||||
|
const sgPaths = sketchGroup.value
|
||||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||||
sketchGroup.forEach((segment, index) => {
|
|
||||||
|
const updateSegment = (
|
||||||
|
segment: Path | SketchGroup['start'],
|
||||||
|
index: number
|
||||||
|
) => {
|
||||||
const segPathToNode = getNodePathFromSourceRange(
|
const segPathToNode = getNodePathFromSourceRange(
|
||||||
modifiedAst,
|
modifiedAst,
|
||||||
segment.__geoMeta.sourceRange
|
segment.__geoMeta.sourceRange
|
||||||
@ -584,7 +629,7 @@ class SceneEntities {
|
|||||||
sceneInfra._baseUnitMultiplier
|
sceneInfra._baseUnitMultiplier
|
||||||
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
||||||
this.updateTangentialArcToSegment({
|
this.updateTangentialArcToSegment({
|
||||||
prevSegment: sketchGroup[index - 1],
|
prevSegment: sgPaths[index - 1],
|
||||||
from: segment.from,
|
from: segment.from,
|
||||||
to: segment.to,
|
to: segment.to,
|
||||||
group: group,
|
group: group,
|
||||||
@ -597,8 +642,13 @@ class SceneEntities {
|
|||||||
group: group,
|
group: group,
|
||||||
scale: factor,
|
scale: factor,
|
||||||
})
|
})
|
||||||
|
} else if (type === PROFILE_START) {
|
||||||
|
group.position.set(segment.from[0], segment.from[1], 0)
|
||||||
|
group.scale.set(factor, factor, factor)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
updateSegment(sketchGroup.start, 0)
|
||||||
|
sgPaths.forEach(updateSegment)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,9 +668,7 @@ class SceneEntities {
|
|||||||
group.userData.from = from
|
group.userData.from = from
|
||||||
group.userData.to = to
|
group.userData.to = to
|
||||||
group.userData.prevSegment = prevSegment
|
group.userData.prevSegment = prevSegment
|
||||||
const arrowGroup = group.children.find(
|
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||||
(child) => child.userData.type === ARROWHEAD
|
|
||||||
) as Group
|
|
||||||
|
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
|
||||||
@ -695,20 +743,20 @@ class SceneEntities {
|
|||||||
const shape = new Shape()
|
const shape = new Shape()
|
||||||
shape.moveTo(0, -0.08 * scale)
|
shape.moveTo(0, -0.08 * scale)
|
||||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||||
const arrowGroup = group.children.find(
|
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||||
(child) => child.userData.type === ARROWHEAD
|
|
||||||
) as Group
|
|
||||||
|
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
if (arrowGroup) {
|
||||||
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
|
||||||
const dir = new Vector3()
|
const dir = new Vector3()
|
||||||
.subVectors(
|
.subVectors(
|
||||||
new Vector3(to[0], to[1], 0),
|
new Vector3(to[0], to[1], 0),
|
||||||
new Vector3(from[0], from[1], 0)
|
new Vector3(from[0], from[1], 0)
|
||||||
)
|
)
|
||||||
.normalize()
|
.normalize()
|
||||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||||
arrowGroup.scale.set(scale, scale, scale)
|
arrowGroup.scale.set(scale, scale, scale)
|
||||||
|
}
|
||||||
|
|
||||||
const straightSegmentBody = group.children.find(
|
const straightSegmentBody = group.children.find(
|
||||||
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
|
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
|
||||||
@ -793,22 +841,24 @@ class SceneEntities {
|
|||||||
}
|
}
|
||||||
setupDefaultPlaneHover() {
|
setupDefaultPlaneHover() {
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onMouseEnter: ({ object }) => {
|
onMouseEnter: ({ selected }) => {
|
||||||
if (object.parent.userData.type !== DEFAULT_PLANES) return
|
if (!(selected instanceof Mesh && selected.parent)) return
|
||||||
const type: DefaultPlane = object.userData.type
|
if (selected.parent.userData.type !== DEFAULT_PLANES) return
|
||||||
object.material.color = defaultPlaneColor(type, 0.5, 1)
|
const type: DefaultPlane = selected.userData.type
|
||||||
|
selected.material.color = defaultPlaneColor(type, 0.5, 1)
|
||||||
},
|
},
|
||||||
onMouseLeave: ({ object }) => {
|
onMouseLeave: ({ selected }) => {
|
||||||
if (object.parent.userData.type !== DEFAULT_PLANES) return
|
if (!(selected instanceof Mesh && selected.parent)) return
|
||||||
const type: DefaultPlane = object.userData.type
|
if (selected.parent.userData.type !== DEFAULT_PLANES) return
|
||||||
object.material.color = defaultPlaneColor(type)
|
const type: DefaultPlane = selected.userData.type
|
||||||
|
selected.material.color = defaultPlaneColor(type)
|
||||||
},
|
},
|
||||||
onClick: (args) => {
|
onClick: (args) => {
|
||||||
if (!args || !args.object) return
|
if (!args || !args.intersects?.[0]) return
|
||||||
if (args.event.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
const { intersection } = args
|
const { intersects } = args
|
||||||
const type = intersection.object.name || ''
|
const type = intersects?.[0].object.name || ''
|
||||||
const posNorm = Number(intersection.normal?.z) > 0
|
const posNorm = Number(intersects?.[0]?.normal?.z) > 0
|
||||||
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
|
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
|
||||||
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
|
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
|
||||||
if (type === YZ_PLANE) {
|
if (type === YZ_PLANE) {
|
||||||
@ -980,9 +1030,9 @@ export function quaternionFromSketchGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function colorSegment(object: any, color: number) {
|
function colorSegment(object: any, color: number) {
|
||||||
const arrowHead = getParentGroup(object, [ARROWHEAD])
|
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
|
||||||
if (arrowHead) {
|
if (segmentHead) {
|
||||||
arrowHead.traverse((child) => {
|
segmentHead.traverse((child) => {
|
||||||
if (child instanceof Mesh) {
|
if (child instanceof Mesh) {
|
||||||
child.material.color.set(color)
|
child.material.color.set(color)
|
||||||
}
|
}
|
||||||
@ -1038,3 +1088,53 @@ function massageFormats(a: any): Vector3 {
|
|||||||
? new Vector3(a[0], a[1], a[2])
|
? new Vector3(a[0], a[1], a[2])
|
||||||
: new Vector3(a.x, a.y, a.z)
|
: new Vector3(a.x, a.y, a.z)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mouseEnterLeaveCallbacks() {
|
||||||
|
return {
|
||||||
|
onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => {
|
||||||
|
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
|
||||||
|
const obj = selected as Mesh
|
||||||
|
const mat = obj.material as MeshBasicMaterial
|
||||||
|
mat.color.set(obj.userData.baseColor)
|
||||||
|
mat.color.offsetHSL(0, 0, 0.5)
|
||||||
|
}
|
||||||
|
const parent = getParentGroup(selected, [
|
||||||
|
STRAIGHT_SEGMENT,
|
||||||
|
TANGENTIAL_ARC_TO_SEGMENT,
|
||||||
|
PROFILE_START,
|
||||||
|
])
|
||||||
|
if (parent?.userData?.pathToNode) {
|
||||||
|
const updatedAst = parse(recast(kclManager.ast))
|
||||||
|
const node = getNodeFromPath<CallExpression>(
|
||||||
|
updatedAst,
|
||||||
|
parent.userData.pathToNode,
|
||||||
|
'CallExpression'
|
||||||
|
).node
|
||||||
|
sceneInfra.highlightCallback([node.start, node.end])
|
||||||
|
const yellow = 0xffff00
|
||||||
|
colorSegment(selected, yellow)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sceneInfra.highlightCallback([0, 0])
|
||||||
|
},
|
||||||
|
onMouseLeave: ({ selected }: OnMouseEnterLeaveArgs) => {
|
||||||
|
sceneInfra.highlightCallback([0, 0])
|
||||||
|
const parent = getParentGroup(selected, [
|
||||||
|
STRAIGHT_SEGMENT,
|
||||||
|
TANGENTIAL_ARC_TO_SEGMENT,
|
||||||
|
PROFILE_START,
|
||||||
|
])
|
||||||
|
const isSelected = parent?.userData?.isSelected
|
||||||
|
colorSegment(
|
||||||
|
selected,
|
||||||
|
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
|
||||||
|
)
|
||||||
|
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
|
||||||
|
const obj = selected as Mesh
|
||||||
|
const mat = obj.material as MeshBasicMaterial
|
||||||
|
mat.color.set(obj.userData.baseColor)
|
||||||
|
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
Object3D,
|
Object3D,
|
||||||
Object3DEventMap,
|
Object3DEventMap,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
|
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import * as TWEEN from '@tweenjs/tween.js'
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import { SourceRange } from 'lang/wasm'
|
import { SourceRange } from 'lang/wasm'
|
||||||
@ -48,31 +48,36 @@ export const AXIS_GROUP = 'axisGroup'
|
|||||||
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||||
export const ARROWHEAD = 'arrowhead'
|
export const ARROWHEAD = 'arrowhead'
|
||||||
|
|
||||||
interface BaseCallbackArgs2 {
|
export interface OnMouseEnterLeaveArgs {
|
||||||
object: any
|
selected: Object3D<Object3DEventMap>
|
||||||
event: any
|
mouseEvent: MouseEvent
|
||||||
}
|
|
||||||
interface BaseCallbackArgs {
|
|
||||||
event: any
|
|
||||||
}
|
|
||||||
interface OnDragCallbackArgs extends BaseCallbackArgs {
|
|
||||||
object: any
|
|
||||||
intersection2d: Vector2
|
|
||||||
intersectPoint: Vector3
|
|
||||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
|
||||||
}
|
|
||||||
interface OnClickCallbackArgs extends BaseCallbackArgs {
|
|
||||||
intersection2d?: Vector2
|
|
||||||
intersectPoint: Vector3
|
|
||||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
|
||||||
object?: any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface onMoveCallbackArgs {
|
interface OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
|
||||||
event: any
|
intersectionPoint: {
|
||||||
intersection2d: Vector2
|
twoD: Vector2
|
||||||
intersectPoint: Vector3
|
threeD: Vector3
|
||||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
}
|
||||||
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
||||||
|
}
|
||||||
|
interface OnClickCallbackArgs {
|
||||||
|
mouseEvent: MouseEvent
|
||||||
|
intersectionPoint?: {
|
||||||
|
twoD: Vector2
|
||||||
|
threeD: Vector3
|
||||||
|
}
|
||||||
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
||||||
|
selected?: Object3D<Object3DEventMap>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnMoveCallbackArgs {
|
||||||
|
mouseEvent: MouseEvent
|
||||||
|
intersectionPoint: {
|
||||||
|
twoD: Vector2
|
||||||
|
threeD: Vector3
|
||||||
|
}
|
||||||
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
||||||
|
selected?: Object3D<Object3DEventMap>
|
||||||
}
|
}
|
||||||
|
|
||||||
// This singleton class is responsible for all of the under the hood setup for the client side scene.
|
// This singleton class is responsible for all of the under the hood setup for the client side scene.
|
||||||
@ -90,16 +95,16 @@ class SceneInfra {
|
|||||||
_baseUnit: BaseUnit = 'mm'
|
_baseUnit: BaseUnit = 'mm'
|
||||||
_baseUnitMultiplier = 1
|
_baseUnitMultiplier = 1
|
||||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
|
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
|
||||||
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
|
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
|
||||||
onMouseEnter: (arg: BaseCallbackArgs2) => void = () => {}
|
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||||
onMouseLeave: (arg: BaseCallbackArgs2) => void = () => {}
|
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||||
setCallbacks = (callbacks: {
|
setCallbacks = (callbacks: {
|
||||||
onDrag?: (arg: OnDragCallbackArgs) => void
|
onDrag?: (arg: OnDragCallbackArgs) => void
|
||||||
onMove?: (arg: onMoveCallbackArgs) => void
|
onMove?: (arg: OnMoveCallbackArgs) => void
|
||||||
onClick?: (arg?: OnClickCallbackArgs) => void
|
onClick?: (arg?: OnClickCallbackArgs) => void
|
||||||
onMouseEnter?: (arg: BaseCallbackArgs2) => void
|
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
|
||||||
onMouseLeave?: (arg: BaseCallbackArgs2) => void
|
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
|
||||||
}) => {
|
}) => {
|
||||||
this.onDragCallback = callbacks.onDrag || this.onDragCallback
|
this.onDragCallback = callbacks.onDrag || this.onDragCallback
|
||||||
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
||||||
@ -142,10 +147,9 @@ class SceneInfra {
|
|||||||
currentMouseVector = new Vector2()
|
currentMouseVector = new Vector2()
|
||||||
selected: {
|
selected: {
|
||||||
mouseDownVector: Vector2
|
mouseDownVector: Vector2
|
||||||
object: any
|
object: Object3D<Object3DEventMap>
|
||||||
hasBeenDragged: boolean
|
hasBeenDragged: boolean
|
||||||
} | null = null
|
} | null = null
|
||||||
selectedObject: null | any = null
|
|
||||||
mouseDownVector: null | Vector2 = null
|
mouseDownVector: null | Vector2 = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -242,8 +246,8 @@ class SceneInfra {
|
|||||||
// Dispose of any other resources like geometries, materials, textures
|
// Dispose of any other resources like geometries, materials, textures
|
||||||
}
|
}
|
||||||
getPlaneIntersectPoint = (): {
|
getPlaneIntersectPoint = (): {
|
||||||
intersection2d?: Vector2
|
twoD?: Vector2
|
||||||
intersectPoint: Vector3
|
threeD?: Vector3
|
||||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||||
} | null => {
|
} | null => {
|
||||||
this.planeRaycaster.setFromCamera(
|
this.planeRaycaster.setFromCamera(
|
||||||
@ -254,23 +258,11 @@ class SceneInfra {
|
|||||||
this.scene.children,
|
this.scene.children,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
if (
|
const recastablePlaneIntersect = planeIntersects.find(
|
||||||
planeIntersects.length > 0 &&
|
(intersect) => intersect.object.name === RAYCASTABLE_PLANE
|
||||||
planeIntersects[0].object.userData.type !== RAYCASTABLE_PLANE
|
|
||||||
) {
|
|
||||||
const intersect = planeIntersects[0]
|
|
||||||
return {
|
|
||||||
intersectPoint: intersect.point,
|
|
||||||
intersection: intersect,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
planeIntersects.length > 0 &&
|
|
||||||
planeIntersects[0].object.userData.type === RAYCASTABLE_PLANE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return null
|
if (!planeIntersects.length) return null
|
||||||
|
if (!recastablePlaneIntersect) return { intersection: planeIntersects[0] }
|
||||||
const planePosition = planeIntersects[0].object.position
|
const planePosition = planeIntersects[0].object.position
|
||||||
const inversePlaneQuaternion = planeIntersects[0].object.quaternion
|
const inversePlaneQuaternion = planeIntersects[0].object.quaternion
|
||||||
.clone()
|
.clone()
|
||||||
@ -285,19 +277,21 @@ class SceneInfra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
intersection2d: new Vector2(
|
twoD: new Vector2(
|
||||||
transformedPoint.x / this._baseUnitMultiplier,
|
transformedPoint.x / this._baseUnitMultiplier,
|
||||||
transformedPoint.y / this._baseUnitMultiplier
|
transformedPoint.y / this._baseUnitMultiplier
|
||||||
), // z should be 0
|
), // z should be 0
|
||||||
intersectPoint: intersectPoint.divideScalar(this._baseUnitMultiplier),
|
threeD: intersectPoint.divideScalar(this._baseUnitMultiplier),
|
||||||
intersection: planeIntersects[0],
|
intersection: planeIntersects[0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMouseMove = (event: MouseEvent) => {
|
onMouseMove = (mouseEvent: MouseEvent) => {
|
||||||
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
this.currentMouseVector.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
|
||||||
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
this.currentMouseVector.y =
|
||||||
|
-(mouseEvent.clientY / window.innerHeight) * 2 + 1
|
||||||
|
|
||||||
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
||||||
|
const intersects = this.raycastRing()
|
||||||
|
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
const hasBeenDragged = !compareVec2Epsilon2(
|
const hasBeenDragged = !compareVec2Epsilon2(
|
||||||
@ -313,47 +307,56 @@ class SceneInfra {
|
|||||||
if (
|
if (
|
||||||
hasBeenDragged &&
|
hasBeenDragged &&
|
||||||
planeIntersectPoint &&
|
planeIntersectPoint &&
|
||||||
planeIntersectPoint.intersection2d
|
planeIntersectPoint.twoD &&
|
||||||
|
planeIntersectPoint.threeD
|
||||||
) {
|
) {
|
||||||
// // console.log('onDrag', this.selected)
|
// // console.log('onDrag', this.selected)
|
||||||
|
|
||||||
this.onDragCallback({
|
this.onDragCallback({
|
||||||
object: this.selected.object,
|
mouseEvent,
|
||||||
event,
|
intersectionPoint: {
|
||||||
intersection2d: planeIntersectPoint.intersection2d,
|
twoD: planeIntersectPoint.twoD,
|
||||||
...planeIntersectPoint,
|
threeD: planeIntersectPoint.threeD,
|
||||||
|
},
|
||||||
|
intersects,
|
||||||
|
selected: this.selected.object,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (planeIntersectPoint && planeIntersectPoint.intersection2d) {
|
} else if (
|
||||||
|
planeIntersectPoint &&
|
||||||
|
planeIntersectPoint.twoD &&
|
||||||
|
planeIntersectPoint.threeD
|
||||||
|
) {
|
||||||
this.onMoveCallback({
|
this.onMoveCallback({
|
||||||
event,
|
mouseEvent,
|
||||||
intersection2d: planeIntersectPoint.intersection2d,
|
intersectionPoint: {
|
||||||
...planeIntersectPoint,
|
twoD: planeIntersectPoint.twoD,
|
||||||
|
threeD: planeIntersectPoint.threeD,
|
||||||
|
},
|
||||||
|
intersects,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const intersect = this.raycastRing()
|
if (intersects[0]) {
|
||||||
|
const firstIntersectObject = intersects[0].object
|
||||||
if (intersect) {
|
|
||||||
const firstIntersectObject = intersect.object
|
|
||||||
if (this.hoveredObject !== firstIntersectObject) {
|
if (this.hoveredObject !== firstIntersectObject) {
|
||||||
if (this.hoveredObject) {
|
if (this.hoveredObject) {
|
||||||
this.onMouseLeave({
|
this.onMouseLeave({
|
||||||
object: this.hoveredObject,
|
selected: this.hoveredObject,
|
||||||
event,
|
mouseEvent: mouseEvent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.hoveredObject = firstIntersectObject
|
this.hoveredObject = firstIntersectObject
|
||||||
this.onMouseEnter({
|
this.onMouseEnter({
|
||||||
object: this.hoveredObject,
|
selected: this.hoveredObject,
|
||||||
event,
|
mouseEvent: mouseEvent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.hoveredObject) {
|
if (this.hoveredObject) {
|
||||||
this.onMouseLeave({
|
this.onMouseLeave({
|
||||||
object: this.hoveredObject,
|
selected: this.hoveredObject,
|
||||||
event,
|
mouseEvent: mouseEvent,
|
||||||
})
|
})
|
||||||
this.hoveredObject = null
|
this.hoveredObject = null
|
||||||
}
|
}
|
||||||
@ -363,41 +366,38 @@ class SceneInfra {
|
|||||||
raycastRing = (
|
raycastRing = (
|
||||||
pixelRadius = 8,
|
pixelRadius = 8,
|
||||||
rayRingCount = 32
|
rayRingCount = 32
|
||||||
): Intersection<Object3D<Object3DEventMap>> | undefined => {
|
): Intersection<Object3D<Object3DEventMap>>[] => {
|
||||||
const mouseDownVector = this.currentMouseVector.clone()
|
const mouseDownVector = this.currentMouseVector.clone()
|
||||||
let closestIntersection:
|
const intersectionsMap = new Map<
|
||||||
| Intersection<Object3D<Object3DEventMap>>
|
Object3D,
|
||||||
| undefined = undefined
|
Intersection<Object3D<Object3DEventMap>>
|
||||||
let closestDistance = Infinity
|
>()
|
||||||
|
|
||||||
const updateClosestIntersection = (
|
const updateIntersectionsMap = (
|
||||||
intersections: Intersection<Object3D<Object3DEventMap>>[]
|
intersections: Intersection<Object3D<Object3DEventMap>>[]
|
||||||
) => {
|
) => {
|
||||||
let intersection = null
|
intersections.forEach((intersection) => {
|
||||||
for (let i = 0; i < intersections.length; i++) {
|
if (intersection.object.type !== 'GridHelper') {
|
||||||
if (intersections[i].object.type !== 'GridHelper') {
|
const existingIntersection = intersectionsMap.get(intersection.object)
|
||||||
intersection = intersections[i]
|
if (
|
||||||
break
|
!existingIntersection ||
|
||||||
|
existingIntersection.distance > intersection.distance
|
||||||
|
) {
|
||||||
|
intersectionsMap.set(intersection.object, intersection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
if (!intersection) return
|
|
||||||
|
|
||||||
if (intersection.distance < closestDistance) {
|
|
||||||
closestDistance = intersection.distance
|
|
||||||
closestIntersection = intersection
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the center point
|
// Check the center point
|
||||||
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
|
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
|
||||||
updateClosestIntersection(
|
updateIntersectionsMap(
|
||||||
this.raycaster.intersectObjects(this.scene.children, true)
|
this.raycaster.intersectObjects(this.scene.children, true)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check the ring points
|
// Check the ring points
|
||||||
for (let i = 0; i < rayRingCount; i++) {
|
for (let i = 0; i < rayRingCount; i++) {
|
||||||
const angle = (i / rayRingCount) * Math.PI * 2
|
const angle = (i / rayRingCount) * Math.PI * 2
|
||||||
|
|
||||||
const offsetX = ((pixelRadius * Math.cos(angle)) / window.innerWidth) * 2
|
const offsetX = ((pixelRadius * Math.cos(angle)) / window.innerWidth) * 2
|
||||||
const offsetY = ((pixelRadius * Math.sin(angle)) / window.innerHeight) * 2
|
const offsetY = ((pixelRadius * Math.sin(angle)) / window.innerHeight) * 2
|
||||||
const ringVector = new Vector2(
|
const ringVector = new Vector2(
|
||||||
@ -405,11 +405,15 @@ class SceneInfra {
|
|||||||
mouseDownVector.y - offsetY
|
mouseDownVector.y - offsetY
|
||||||
)
|
)
|
||||||
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
|
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
|
||||||
updateClosestIntersection(
|
updateIntersectionsMap(
|
||||||
this.raycaster.intersectObjects(this.scene.children, true)
|
this.raycaster.intersectObjects(this.scene.children, true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return closestIntersection
|
|
||||||
|
// Convert the map values to an array and sort by distance
|
||||||
|
return Array.from(intersectionsMap.values()).sort(
|
||||||
|
(a, b) => a.distance - b.distance
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown = (event: MouseEvent) => {
|
onMouseDown = (event: MouseEvent) => {
|
||||||
@ -417,45 +421,60 @@ class SceneInfra {
|
|||||||
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||||
|
|
||||||
const mouseDownVector = this.currentMouseVector.clone()
|
const mouseDownVector = this.currentMouseVector.clone()
|
||||||
const intersect = this.raycastRing()
|
const intersect = this.raycastRing()[0]
|
||||||
|
|
||||||
if (intersect) {
|
if (intersect) {
|
||||||
const intersectParent = intersect?.object?.parent as Group
|
const intersectParent = intersect?.object?.parent as Group
|
||||||
this.selected = intersectParent.isGroup
|
this.selected = intersectParent.isGroup
|
||||||
? {
|
? {
|
||||||
mouseDownVector,
|
mouseDownVector,
|
||||||
object: intersect?.object,
|
object: intersect.object,
|
||||||
hasBeenDragged: false,
|
hasBeenDragged: false,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseUp = (event: MouseEvent) => {
|
onMouseUp = (mouseEvent: MouseEvent) => {
|
||||||
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
this.currentMouseVector.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
|
||||||
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
this.currentMouseVector.y =
|
||||||
|
-(mouseEvent.clientY / window.innerHeight) * 2 + 1
|
||||||
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
||||||
|
const intersects = this.raycastRing()
|
||||||
|
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
if (this.selected.hasBeenDragged) {
|
if (this.selected.hasBeenDragged) {
|
||||||
// this is where we could fire a onDragEnd event
|
// this is where we could fire a onDragEnd event
|
||||||
// console.log('onDragEnd', this.selected)
|
// console.log('onDragEnd', this.selected)
|
||||||
} else if (planeIntersectPoint) {
|
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
|
||||||
// fire onClick event as there was no drags
|
// fire onClick event as there was no drags
|
||||||
this.onClickCallback({
|
this.onClickCallback({
|
||||||
object: this.selected?.object,
|
mouseEvent,
|
||||||
event,
|
intersectionPoint: {
|
||||||
...planeIntersectPoint,
|
twoD: planeIntersectPoint.twoD,
|
||||||
|
threeD: planeIntersectPoint.threeD,
|
||||||
|
},
|
||||||
|
intersects,
|
||||||
|
selected: this.selected.object,
|
||||||
|
})
|
||||||
|
} else if (planeIntersectPoint) {
|
||||||
|
this.onClickCallback({
|
||||||
|
mouseEvent,
|
||||||
|
intersects,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.onClickCallback()
|
this.onClickCallback()
|
||||||
}
|
}
|
||||||
// Clear the selected state whether it was dragged or not
|
// Clear the selected state whether it was dragged or not
|
||||||
this.selected = null
|
this.selected = null
|
||||||
} else if (planeIntersectPoint) {
|
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
|
||||||
this.onClickCallback({
|
this.onClickCallback({
|
||||||
event,
|
mouseEvent,
|
||||||
...planeIntersectPoint,
|
intersectionPoint: {
|
||||||
|
twoD: planeIntersectPoint.twoD,
|
||||||
|
threeD: planeIntersectPoint.threeD,
|
||||||
|
},
|
||||||
|
intersects,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.onClickCallback()
|
this.onClickCallback()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Coords2d } from 'lang/std/sketch'
|
import { Coords2d } from 'lang/std/sketch'
|
||||||
import {
|
import {
|
||||||
|
BoxGeometry,
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
CatmullRomCurve3,
|
CatmullRomCurve3,
|
||||||
ConeGeometry,
|
ConeGeometry,
|
||||||
@ -19,6 +20,7 @@ import {
|
|||||||
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||||
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
PROFILE_START,
|
||||||
STRAIGHT_SEGMENT,
|
STRAIGHT_SEGMENT,
|
||||||
STRAIGHT_SEGMENT_BODY,
|
STRAIGHT_SEGMENT_BODY,
|
||||||
STRAIGHT_SEGMENT_DASH,
|
STRAIGHT_SEGMENT_DASH,
|
||||||
@ -29,6 +31,38 @@ import {
|
|||||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||||
import { ARROWHEAD } from './sceneInfra'
|
import { ARROWHEAD } from './sceneInfra'
|
||||||
|
|
||||||
|
export function profileStart({
|
||||||
|
from,
|
||||||
|
id,
|
||||||
|
pathToNode,
|
||||||
|
scale = 1,
|
||||||
|
}: {
|
||||||
|
from: Coords2d
|
||||||
|
id: string
|
||||||
|
pathToNode: PathToNode
|
||||||
|
scale?: number
|
||||||
|
}) {
|
||||||
|
const group = new Group()
|
||||||
|
|
||||||
|
const geometry = new BoxGeometry(0.8, 0.8, 0.8)
|
||||||
|
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||||
|
const mesh = new Mesh(geometry, body)
|
||||||
|
|
||||||
|
group.add(mesh)
|
||||||
|
|
||||||
|
group.userData = {
|
||||||
|
type: PROFILE_START,
|
||||||
|
id,
|
||||||
|
from,
|
||||||
|
pathToNode,
|
||||||
|
isSelected: false,
|
||||||
|
}
|
||||||
|
group.name = PROFILE_START
|
||||||
|
group.position.set(from[0], from[1], 0)
|
||||||
|
group.scale.set(scale, scale, scale)
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
export function straightSegment({
|
export function straightSegment({
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
@ -36,6 +70,7 @@ export function straightSegment({
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
isDraftSegment,
|
isDraftSegment,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
|
callExpName,
|
||||||
}: {
|
}: {
|
||||||
from: Coords2d
|
from: Coords2d
|
||||||
to: Coords2d
|
to: Coords2d
|
||||||
@ -43,6 +78,7 @@ export function straightSegment({
|
|||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
isDraftSegment?: boolean
|
isDraftSegment?: boolean
|
||||||
scale?: number
|
scale?: number
|
||||||
|
callExpName: string
|
||||||
}): Group {
|
}): Group {
|
||||||
const group = new Group()
|
const group = new Group()
|
||||||
|
|
||||||
@ -66,7 +102,8 @@ export function straightSegment({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = new MeshBasicMaterial({ color: 0xffffff })
|
const baseColor = callExpName === 'close' ? 0x444444 : 0xffffff
|
||||||
|
const body = new MeshBasicMaterial({ color: baseColor })
|
||||||
const mesh = new Mesh(geometry, body)
|
const mesh = new Mesh(geometry, body)
|
||||||
mesh.userData.type = isDraftSegment
|
mesh.userData.type = isDraftSegment
|
||||||
? STRAIGHT_SEGMENT_DASH
|
? STRAIGHT_SEGMENT_DASH
|
||||||
@ -80,7 +117,10 @@ export function straightSegment({
|
|||||||
to,
|
to,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
|
callExpName,
|
||||||
|
baseColor,
|
||||||
}
|
}
|
||||||
|
group.name = STRAIGHT_SEGMENT
|
||||||
|
|
||||||
const arrowGroup = createArrowhead(scale)
|
const arrowGroup = createArrowhead(scale)
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
@ -89,7 +129,8 @@ export function straightSegment({
|
|||||||
.normalize()
|
.normalize()
|
||||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||||
|
|
||||||
group.add(mesh, arrowGroup)
|
group.add(mesh)
|
||||||
|
if (callExpName !== 'close') group.add(arrowGroup)
|
||||||
|
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
@ -169,6 +210,7 @@ export function tangentialArcToSegment({
|
|||||||
pathToNode,
|
pathToNode,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
}
|
}
|
||||||
|
group.name = TANGENTIAL_ARC_TO_SEGMENT
|
||||||
|
|
||||||
const arrowGroup = createArrowhead(scale)
|
const arrowGroup = createArrowhead(scale)
|
||||||
arrowGroup.position.set(to[0], to[1], 0)
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Combobox } from '@headlessui/react'
|
import { Combobox } from '@headlessui/react'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { CommandArgumentOption } from 'lib/commandTypes'
|
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
function CommandArgOptionInput({
|
function CommandArgOptionInput({
|
||||||
options,
|
options,
|
||||||
@ -11,51 +11,89 @@ function CommandArgOptionInput({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: {
|
}: {
|
||||||
options: CommandArgumentOption<unknown>[]
|
options: (CommandArgument<unknown> & { inputType: 'options' })['options']
|
||||||
argName: string
|
argName: string
|
||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (data: unknown) => void
|
onSubmit: (data: unknown) => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
|
const resolvedOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
typeof options === 'function'
|
||||||
|
? options(commandBarState.context)
|
||||||
|
: options,
|
||||||
|
[argName, options, commandBarState.context]
|
||||||
|
)
|
||||||
|
// The initial current option is either an already-input value or the configured default
|
||||||
|
const currentOption = useMemo(
|
||||||
|
() =>
|
||||||
|
resolvedOptions.find(
|
||||||
|
(o) => o.value === commandBarState.context.argumentsToSubmit[argName]
|
||||||
|
) || resolvedOptions.find((o) => o.isCurrent),
|
||||||
|
[commandBarState.context.argumentsToSubmit, argName, resolvedOptions]
|
||||||
|
)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
|
const [selectedOption, setSelectedOption] = useState<
|
||||||
options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
|
CommandArgumentOption<unknown>
|
||||||
commandBarState.context.argumentsToSubmit[argName] ||
|
>(currentOption || resolvedOptions[0])
|
||||||
options[0].value
|
const initialQuery = useMemo(() => '', [options, argName])
|
||||||
|
const [query, setQuery] = useState(initialQuery)
|
||||||
|
const [filteredOptions, setFilteredOptions] =
|
||||||
|
useState<typeof resolvedOptions>()
|
||||||
|
|
||||||
|
// Create a new Fuse instance when the options change
|
||||||
|
const fuse = useMemo(
|
||||||
|
() =>
|
||||||
|
new Fuse(resolvedOptions, {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
}),
|
||||||
|
[argName, resolvedOptions]
|
||||||
)
|
)
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
|
||||||
|
|
||||||
const fuse = new Fuse(options, {
|
// Reset the query and selected option when the argName changes
|
||||||
keys: ['name', 'description'],
|
useEffect(() => {
|
||||||
threshold: 0.3,
|
setQuery(initialQuery)
|
||||||
})
|
setSelectedOption(currentOption || resolvedOptions[0])
|
||||||
|
}, [argName])
|
||||||
|
|
||||||
|
// Auto focus and select the input when the component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
inputRef.current?.select()
|
inputRef.current?.select()
|
||||||
}, [inputRef])
|
}, [inputRef])
|
||||||
|
|
||||||
|
// Filter the options based on the query,
|
||||||
|
// resetting the query when the options change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const results = fuse.search(query).map((result) => result.item)
|
const results = fuse.search(query).map((result) => result.item)
|
||||||
setFilteredOptions(query.length > 0 ? results : options)
|
setFilteredOptions(query.length > 0 ? results : resolvedOptions)
|
||||||
}, [query])
|
}, [query, resolvedOptions, fuse])
|
||||||
|
|
||||||
function handleSelectOption(option: CommandArgumentOption<unknown>) {
|
function handleSelectOption(option: CommandArgumentOption<unknown>) {
|
||||||
setArgValue(option)
|
// We deal with the whole option object internally
|
||||||
|
setSelectedOption(option)
|
||||||
|
|
||||||
|
// But we only submit the value
|
||||||
onSubmit(option.value)
|
onSubmit(option.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onSubmit(argValue)
|
|
||||||
|
// We submit the value of the selected option, not the whole object
|
||||||
|
onSubmit(selectedOption.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
|
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
|
||||||
<Combobox value={argValue} onChange={handleSelectOption} name="options">
|
<Combobox
|
||||||
|
value={selectedOption}
|
||||||
|
onChange={handleSelectOption}
|
||||||
|
name="options"
|
||||||
|
>
|
||||||
<div className="flex items-center mx-4 mt-4 mb-2">
|
<div className="flex items-center mx-4 mt-4 mb-2">
|
||||||
<label
|
<label
|
||||||
htmlFor="option-input"
|
htmlFor="option-input"
|
||||||
@ -75,10 +113,12 @@ function CommandArgOptionInput({
|
|||||||
stepBack()
|
stepBack()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
value={query}
|
||||||
placeholder={
|
placeholder={
|
||||||
(argValue as CommandArgumentOption<unknown>)?.name ||
|
currentOption?.name ||
|
||||||
placeholder ||
|
placeholder ||
|
||||||
'Select an option for ' + argName
|
argName ||
|
||||||
|
'Select an option'
|
||||||
}
|
}
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -98,7 +138,7 @@ function CommandArgOptionInput({
|
|||||||
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
||||||
>
|
>
|
||||||
<p className="flex-grow">{option.name} </p>
|
<p className="flex-grow">{option.name} </p>
|
||||||
{'isCurrent' in option && option.isCurrent && (
|
{option.value === currentOption?.value && (
|
||||||
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
||||||
current
|
current
|
||||||
</small>
|
</small>
|
||||||
|
@ -29,12 +29,6 @@ export const CommandBarProvider = ({
|
|||||||
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
||||||
devTools: true,
|
devTools: true,
|
||||||
guards: {
|
guards: {
|
||||||
'Arguments are ready': (context, _) => {
|
|
||||||
return context.selectedCommand?.args
|
|
||||||
? context.argumentsToSubmit.length ===
|
|
||||||
Object.keys(context.selectedCommand.args)?.length
|
|
||||||
: false
|
|
||||||
},
|
|
||||||
'Command has no arguments': (context, _event) => {
|
'Command has no arguments': (context, _event) => {
|
||||||
return (
|
return (
|
||||||
!context.selectedCommand?.args ||
|
!context.selectedCommand?.args ||
|
||||||
@ -81,7 +75,12 @@ export const CommandBar = () => {
|
|||||||
function stepBack() {
|
function stepBack() {
|
||||||
if (!currentArgument) {
|
if (!currentArgument) {
|
||||||
if (commandBarState.matches('Review')) {
|
if (commandBarState.matches('Review')) {
|
||||||
const entries = Object.entries(selectedCommand?.args || {})
|
const entries = Object.entries(selectedCommand?.args || {}).filter(
|
||||||
|
([_, argConfig]) =>
|
||||||
|
typeof argConfig.required === 'function'
|
||||||
|
? argConfig.required(commandBarState.context)
|
||||||
|
: argConfig.required
|
||||||
|
)
|
||||||
|
|
||||||
const currentArgName = entries[entries.length - 1][0]
|
const currentArgName = entries[entries.length - 1][0]
|
||||||
const currentArg = {
|
const currentArg = {
|
||||||
@ -89,19 +88,12 @@ export const CommandBar = () => {
|
|||||||
...entries[entries.length - 1][1],
|
...entries[entries.length - 1][1],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandBarState.matches('Review')) {
|
commandBarSend({
|
||||||
commandBarSend({
|
type: 'Edit argument',
|
||||||
type: 'Edit argument',
|
data: {
|
||||||
data: {
|
arg: currentArg,
|
||||||
arg: currentArg,
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
commandBarSend({
|
|
||||||
type: 'Remove argument',
|
|
||||||
data: { [currentArgName]: currentArg },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarSend({ type: 'Deselect command' })
|
||||||
}
|
}
|
||||||
@ -124,11 +116,6 @@ export const CommandBar = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => console.log(commandBarState.context.argumentsToSubmit),
|
|
||||||
[commandBarState.context.argumentsToSubmit]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root
|
<Transition.Root
|
||||||
show={!commandBarState.matches('Closed') || false}
|
show={!commandBarState.matches('Closed') || false}
|
||||||
|
@ -76,72 +76,82 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
)}
|
)}
|
||||||
{selectedCommand?.name}
|
{selectedCommand?.name}
|
||||||
</p>
|
</p>
|
||||||
{Object.entries(selectedCommand?.args || {}).map(
|
{Object.entries(selectedCommand?.args || {})
|
||||||
([argName, arg], i) => (
|
.filter(([_, argConfig]) =>
|
||||||
<button
|
typeof argConfig.required === 'function'
|
||||||
disabled={!isReviewing && currentArgument?.name === argName}
|
? argConfig.required(commandBarState.context)
|
||||||
onClick={() => {
|
: argConfig.required
|
||||||
commandBarSend({
|
|
||||||
type: isReviewing
|
|
||||||
? 'Edit argument'
|
|
||||||
: 'Change current argument',
|
|
||||||
data: { arg: { ...arg, name: argName } },
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
key={argName}
|
|
||||||
className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
|
||||||
argName === currentArgument?.name
|
|
||||||
? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
|
|
||||||
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="capitalize">{argName}</span>
|
|
||||||
{argumentsToSubmit[argName] ? (
|
|
||||||
arg.inputType === 'selection' ? (
|
|
||||||
getSelectionTypeDisplayText(
|
|
||||||
argumentsToSubmit[argName] as Selections
|
|
||||||
)
|
|
||||||
) : arg.inputType === 'kcl' ? (
|
|
||||||
roundOff(
|
|
||||||
Number(
|
|
||||||
(argumentsToSubmit[argName] as KclCommandValue)
|
|
||||||
.valueCalculated
|
|
||||||
),
|
|
||||||
4
|
|
||||||
)
|
|
||||||
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
|
||||||
JSON.stringify(argumentsToSubmit[argName])
|
|
||||||
) : (
|
|
||||||
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
{showShortcuts && (
|
|
||||||
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
|
||||||
<span className="sr-only">Hotkey: </span>
|
|
||||||
{i + 1}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{arg.inputType === 'kcl' &&
|
|
||||||
!!argumentsToSubmit[argName] &&
|
|
||||||
'variableName' in
|
|
||||||
(argumentsToSubmit[argName] as KclCommandValue) && (
|
|
||||||
<>
|
|
||||||
<CustomIcon name="make-variable" className="w-4 h-4" />
|
|
||||||
<Tooltip position="blockEnd">
|
|
||||||
New variable:{' '}
|
|
||||||
{
|
|
||||||
(
|
|
||||||
argumentsToSubmit[
|
|
||||||
argName
|
|
||||||
] as KclExpressionWithVariable
|
|
||||||
).variableName
|
|
||||||
}
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
)}
|
.map(([argName, arg], i) => {
|
||||||
|
const argValue =
|
||||||
|
(typeof argumentsToSubmit[argName] === 'function'
|
||||||
|
? (argumentsToSubmit[argName] as Function)(
|
||||||
|
commandBarState.context
|
||||||
|
)
|
||||||
|
: argumentsToSubmit[argName]) || ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={!isReviewing && currentArgument?.name === argName}
|
||||||
|
onClick={() => {
|
||||||
|
commandBarSend({
|
||||||
|
type: isReviewing
|
||||||
|
? 'Edit argument'
|
||||||
|
: 'Change current argument',
|
||||||
|
data: { arg: { ...arg, name: argName } },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
key={argName}
|
||||||
|
className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
||||||
|
argName === currentArgument?.name
|
||||||
|
? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
|
||||||
|
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="capitalize">{argName}</span>
|
||||||
|
{argValue ? (
|
||||||
|
arg.inputType === 'selection' ? (
|
||||||
|
getSelectionTypeDisplayText(argValue as Selections)
|
||||||
|
) : arg.inputType === 'kcl' ? (
|
||||||
|
roundOff(
|
||||||
|
Number((argValue as KclCommandValue).valueCalculated),
|
||||||
|
4
|
||||||
|
)
|
||||||
|
) : typeof argValue === 'object' ? (
|
||||||
|
JSON.stringify(argValue)
|
||||||
|
) : (
|
||||||
|
<em>{argValue}</em>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{showShortcuts && (
|
||||||
|
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
||||||
|
<span className="sr-only">Hotkey: </span>
|
||||||
|
{i + 1}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{arg.inputType === 'kcl' &&
|
||||||
|
!!argValue &&
|
||||||
|
'variableName' in (argValue as KclCommandValue) && (
|
||||||
|
<>
|
||||||
|
<CustomIcon
|
||||||
|
name="make-variable"
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<Tooltip position="blockEnd">
|
||||||
|
New variable:{' '}
|
||||||
|
{
|
||||||
|
(
|
||||||
|
argumentsToSubmit[
|
||||||
|
argName
|
||||||
|
] as KclExpressionWithVariable
|
||||||
|
).variableName
|
||||||
|
}
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
|
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +48,8 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
if (!arg) return
|
if (!arg) return
|
||||||
})
|
})
|
||||||
|
|
||||||
function submitCommand() {
|
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Submit command',
|
type: 'Submit command',
|
||||||
data: argumentsToSubmit,
|
data: argumentsToSubmit,
|
||||||
|
@ -29,7 +29,7 @@ function CommandBarSelectionInput({
|
|||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||||
const selection = useSelector(arg.actor, selectionSelector)
|
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||||
const [selectionsByType, setSelectionsByType] = useState<
|
const [selectionsByType, setSelectionsByType] = useState<
|
||||||
'none' | ResolvedSelectionType[]
|
'none' | ResolvedSelectionType[]
|
||||||
>(
|
>(
|
||||||
|
@ -9,6 +9,7 @@ export type CustomIconName =
|
|||||||
| 'clipboardCheckmark'
|
| 'clipboardCheckmark'
|
||||||
| 'close'
|
| 'close'
|
||||||
| 'equal'
|
| 'equal'
|
||||||
|
| 'exportFile'
|
||||||
| 'extrude'
|
| 'extrude'
|
||||||
| 'file'
|
| 'file'
|
||||||
| 'filePlus'
|
| 'filePlus'
|
||||||
@ -194,6 +195,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'exportFile':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM16.3904 14.1877L14.3904 11.6877L13.6096 12.3124L14.9597 14H11V15H14.9597L13.6096 16.6877L14.3904 17.3124L16.3904 14.8124L16.6403 14.5L16.3904 14.1877Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'extrude':
|
case 'extrude':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -1,238 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { ActionButton } from './ActionButton'
|
|
||||||
import Modal from 'react-modal'
|
|
||||||
import React from 'react'
|
|
||||||
import { useFormik } from 'formik'
|
|
||||||
import { Models } from '@kittycad/lib'
|
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
|
||||||
type OutputTypeKey = OutputFormat['type']
|
|
||||||
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
|
||||||
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
|
||||||
|
|
||||||
export interface ExportButtonProps extends React.PropsWithChildren {
|
|
||||||
className?: {
|
|
||||||
button?: string
|
|
||||||
icon?: string
|
|
||||||
bg?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|
||||||
const [modalIsOpen, setIsOpen] = React.useState(false)
|
|
||||||
const {
|
|
||||||
settings: {
|
|
||||||
state: {
|
|
||||||
context: { baseUnit },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useGlobalStateContext()
|
|
||||||
|
|
||||||
const defaultType = 'gltf'
|
|
||||||
const [type, setType] = React.useState<OutputTypeKey>(defaultType)
|
|
||||||
const defaultStorage = 'embedded'
|
|
||||||
const [storage, setStorage] = React.useState<StorageUnion>(defaultStorage)
|
|
||||||
|
|
||||||
function openModal() {
|
|
||||||
setIsOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
setIsOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to gltf and embedded.
|
|
||||||
const initialValues: OutputFormat = {
|
|
||||||
type: defaultType,
|
|
||||||
storage: defaultStorage,
|
|
||||||
presentation: 'pretty',
|
|
||||||
}
|
|
||||||
const formik = useFormik({
|
|
||||||
initialValues,
|
|
||||||
onSubmit: (values: OutputFormat) => {
|
|
||||||
// Set the default coords.
|
|
||||||
if (
|
|
||||||
values.type === 'obj' ||
|
|
||||||
values.type === 'ply' ||
|
|
||||||
values.type === 'step' ||
|
|
||||||
values.type === 'stl'
|
|
||||||
) {
|
|
||||||
// Set the default coords.
|
|
||||||
// In the future we can make this configurable.
|
|
||||||
// But for now, its probably best to keep it consistent with the
|
|
||||||
// UI.
|
|
||||||
values.coords = {
|
|
||||||
forward: {
|
|
||||||
axis: 'y',
|
|
||||||
direction: 'negative',
|
|
||||||
},
|
|
||||||
up: {
|
|
||||||
axis: 'z',
|
|
||||||
direction: 'positive',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
values.type === 'obj' ||
|
|
||||||
values.type === 'stl' ||
|
|
||||||
values.type === 'ply'
|
|
||||||
) {
|
|
||||||
values.units = baseUnit
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
values.type === 'ply' ||
|
|
||||||
values.type === 'stl' ||
|
|
||||||
values.type === 'gltf'
|
|
||||||
) {
|
|
||||||
// Set the storage type.
|
|
||||||
values.storage = storage
|
|
||||||
}
|
|
||||||
if (values.type === 'ply' || values.type === 'stl') {
|
|
||||||
values.selection = { type: 'default_scene' }
|
|
||||||
}
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'export',
|
|
||||||
// By default let's leave this blank to export the whole scene.
|
|
||||||
// In the future we might want to let the user choose which entities
|
|
||||||
// in the scene to export. In that case, you'd pass the IDs thru here.
|
|
||||||
entity_ids: [],
|
|
||||||
format: values,
|
|
||||||
source_unit: baseUnit,
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
|
|
||||||
closeModal()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionButton
|
|
||||||
onClick={openModal}
|
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: faFileExport,
|
|
||||||
className: 'p-1',
|
|
||||||
size: 'sm',
|
|
||||||
iconClassName: className?.icon,
|
|
||||||
bgClassName: className?.bg,
|
|
||||||
}}
|
|
||||||
className={className?.button}
|
|
||||||
>
|
|
||||||
{children || 'Export'}
|
|
||||||
</ActionButton>
|
|
||||||
<Modal
|
|
||||||
isOpen={modalIsOpen}
|
|
||||||
onRequestClose={closeModal}
|
|
||||||
contentLabel="Export"
|
|
||||||
overlayClassName="z-40 fixed inset-0 grid place-items-center"
|
|
||||||
className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border max-w-xl w-full"
|
|
||||||
>
|
|
||||||
<h1 className="text-2xl font-bold">Export your design</h1>
|
|
||||||
<form onSubmit={formik.handleSubmit}>
|
|
||||||
<div className="flex flex-wrap justify-between gap-8 items-center w-full my-8">
|
|
||||||
<label htmlFor="type" className="flex-1">
|
|
||||||
<p className="mb-2">Type</p>
|
|
||||||
<select
|
|
||||||
id="type"
|
|
||||||
name="type"
|
|
||||||
data-testid="export-type"
|
|
||||||
onChange={(e) => {
|
|
||||||
setType(e.target.value as OutputTypeKey)
|
|
||||||
if (e.target.value === 'gltf') {
|
|
||||||
// Set default to embedded.
|
|
||||||
setStorage('embedded')
|
|
||||||
} else if (e.target.value === 'ply') {
|
|
||||||
// Set default to ascii.
|
|
||||||
setStorage('ascii')
|
|
||||||
} else if (e.target.value === 'stl') {
|
|
||||||
// Set default to ascii.
|
|
||||||
setStorage('ascii')
|
|
||||||
}
|
|
||||||
formik.handleChange(e)
|
|
||||||
}}
|
|
||||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
|
||||||
>
|
|
||||||
<option value="gltf">gltf</option>
|
|
||||||
<option value="obj">obj</option>
|
|
||||||
<option value="ply">ply</option>
|
|
||||||
<option value="step">step</option>
|
|
||||||
<option value="stl">stl</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
|
|
||||||
<label htmlFor="storage" className="flex-1">
|
|
||||||
<p className="mb-2">Storage</p>
|
|
||||||
<select
|
|
||||||
id="storage"
|
|
||||||
name="storage"
|
|
||||||
data-testid="export-storage"
|
|
||||||
onChange={(e) => {
|
|
||||||
setStorage(e.target.value as StorageUnion)
|
|
||||||
formik.handleChange(e)
|
|
||||||
}}
|
|
||||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
|
||||||
>
|
|
||||||
{type === 'gltf' && (
|
|
||||||
<>
|
|
||||||
<option value="embedded">embedded</option>
|
|
||||||
<option value="binary">binary</option>
|
|
||||||
<option value="standard">standard</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{type === 'stl' && (
|
|
||||||
<>
|
|
||||||
<option value="ascii">ascii</option>
|
|
||||||
<option value="binary">binary</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{type === 'ply' && (
|
|
||||||
<>
|
|
||||||
<option value="ascii">ascii</option>
|
|
||||||
<option value="binary_little_endian">
|
|
||||||
binary_little_endian
|
|
||||||
</option>
|
|
||||||
<option value="binary_big_endian">
|
|
||||||
binary_big_endian
|
|
||||||
</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between mt-6">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
icon={{
|
|
||||||
icon: faXmark,
|
|
||||||
className: 'p-1',
|
|
||||||
bgClassName: 'bg-destroy-80',
|
|
||||||
iconClassName:
|
|
||||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
|
||||||
}}
|
|
||||||
className="hover:border-destroy-40"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
type="submit"
|
|
||||||
icon={{ icon: faFileExport, className: 'p-1' }}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -57,27 +57,30 @@ export const GlobalStateProvider = ({
|
|||||||
>
|
>
|
||||||
)
|
)
|
||||||
|
|
||||||
const [settingsState, settingsSend] = useMachine(settingsMachine, {
|
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||||
context: persistedSettings,
|
settingsMachine,
|
||||||
actions: {
|
{
|
||||||
toastSuccess: (context, event) => {
|
context: persistedSettings,
|
||||||
const truncatedNewValue =
|
actions: {
|
||||||
'data' in event && event.data instanceof Object
|
toastSuccess: (context, event) => {
|
||||||
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
const truncatedNewValue =
|
||||||
.toString()
|
'data' in event && event.data instanceof Object
|
||||||
.substring(0, 28) as any)
|
? (String(
|
||||||
: undefined
|
context[Object.keys(event.data)[0] as keyof typeof context]
|
||||||
toast.success(
|
).substring(0, 28) as any)
|
||||||
event.type +
|
: undefined
|
||||||
(truncatedNewValue
|
toast.success(
|
||||||
? ` to "${truncatedNewValue}${
|
event.type +
|
||||||
truncatedNewValue.length === 28 ? '...' : ''
|
(truncatedNewValue
|
||||||
}"`
|
? ` to "${truncatedNewValue}${
|
||||||
: '')
|
truncatedNewValue.length === 28 ? '...' : ''
|
||||||
)
|
}"`
|
||||||
|
: '')
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
})
|
)
|
||||||
settingsStateRef = settingsState.context
|
settingsStateRef = settingsState.context
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
@ -85,6 +88,7 @@ export const GlobalStateProvider = ({
|
|||||||
state: settingsState,
|
state: settingsState,
|
||||||
send: settingsSend,
|
send: settingsSend,
|
||||||
commandBarConfig: settingsCommandBarConfig,
|
commandBarConfig: settingsCommandBarConfig,
|
||||||
|
actor: settingsActor,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for changes to the system theme and update the app theme accordingly
|
// Listen for changes to the system theme and update the app theme accordingly
|
||||||
@ -105,7 +109,7 @@ export const GlobalStateProvider = ({
|
|||||||
}, [settingsState.context])
|
}, [settingsState.context])
|
||||||
|
|
||||||
// Auth machine setup
|
// Auth machine setup
|
||||||
const [authState, authSend] = useMachine(authMachine, {
|
const [authState, authSend, authActor] = useMachine(authMachine, {
|
||||||
actions: {
|
actions: {
|
||||||
goToSignInPage: () => {
|
goToSignInPage: () => {
|
||||||
navigate(paths.SIGN_IN)
|
navigate(paths.SIGN_IN)
|
||||||
@ -125,6 +129,7 @@ export const GlobalStateProvider = ({
|
|||||||
state: authState,
|
state: authState,
|
||||||
send: authSend,
|
send: authSend,
|
||||||
commandBarConfig: authCommandBarConfig,
|
commandBarConfig: authCommandBarConfig,
|
||||||
|
actor: authActor,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -25,8 +25,7 @@ describe('processMemory', () => {
|
|||||||
|> lineTo([-3.35, 0.17], %)
|
|> lineTo([-3.35, 0.17], %)
|
||||||
|> lineTo([0.98, 5.16], %)
|
|> lineTo([0.98, 5.16], %)
|
||||||
|> lineTo([2.15, 4.32], %)
|
|> lineTo([2.15, 4.32], %)
|
||||||
// |> rx(90, %)
|
// |> rx(90, %)`
|
||||||
show(theExtrude, theSketch)`
|
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
const programMemory = await enginelessExecutor(ast, {
|
const programMemory = await enginelessExecutor(ast, {
|
||||||
root: {},
|
root: {},
|
||||||
|
@ -38,6 +38,10 @@ import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
|
|||||||
import { startSketchOnDefault } from 'lang/modifyAst'
|
import { startSketchOnDefault } from 'lang/modifyAst'
|
||||||
import { Program } from 'lang/wasm'
|
import { Program } from 'lang/wasm'
|
||||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
|
import { TEST } from 'env'
|
||||||
|
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||||
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -54,7 +58,12 @@ export const ModelingMachineProvider = ({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const { auth } = useGlobalStateContext()
|
const {
|
||||||
|
auth,
|
||||||
|
settings: {
|
||||||
|
context: { baseUnit },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
const { code } = useKclContext()
|
const { code } = useKclContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
@ -170,6 +179,56 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
return { selectionRangeTypeMap }
|
return { selectionRangeTypeMap }
|
||||||
}),
|
}),
|
||||||
|
'Engine export': (_, event) => {
|
||||||
|
if (event.type !== 'Export' || TEST) return
|
||||||
|
const format = {
|
||||||
|
...event.data,
|
||||||
|
} as Partial<Models['OutputFormat_type']>
|
||||||
|
|
||||||
|
// Set all the un-configurable defaults here.
|
||||||
|
if (format.type === 'gltf') {
|
||||||
|
format.presentation = 'pretty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
format.type === 'obj' ||
|
||||||
|
format.type === 'ply' ||
|
||||||
|
format.type === 'step' ||
|
||||||
|
format.type === 'stl'
|
||||||
|
) {
|
||||||
|
// Set the default coords.
|
||||||
|
// In the future we can make this configurable.
|
||||||
|
// But for now, its probably best to keep it consistent with the
|
||||||
|
// UI.
|
||||||
|
format.coords = {
|
||||||
|
forward: {
|
||||||
|
axis: 'y',
|
||||||
|
direction: 'negative',
|
||||||
|
},
|
||||||
|
up: {
|
||||||
|
axis: 'z',
|
||||||
|
direction: 'positive',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
format.type === 'obj' ||
|
||||||
|
format.type === 'stl' ||
|
||||||
|
format.type === 'ply'
|
||||||
|
) {
|
||||||
|
format.units = baseUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format.type === 'ply' || format.type === 'stl') {
|
||||||
|
format.selection = { type: 'default_scene' }
|
||||||
|
}
|
||||||
|
|
||||||
|
exportFromEngine({
|
||||||
|
source_unit: baseUnit,
|
||||||
|
format: format as Models['OutputFormat_type'],
|
||||||
|
}).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager
|
||||||
|
},
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
'has valid extrude selection': ({ selectionRanges }) => {
|
'has valid extrude selection': ({ selectionRanges }) => {
|
||||||
@ -192,6 +251,8 @@ export const ModelingMachineProvider = ({
|
|||||||
selectionRanges
|
selectionRanges
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'Has exportable geometry': () =>
|
||||||
|
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
|
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
|
||||||
|
@ -5,7 +5,6 @@ import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
|||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { ExportButtonProps } from './ExportButton'
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
@ -38,15 +37,6 @@ const projectWellFormed = {
|
|||||||
},
|
},
|
||||||
} satisfies ProjectWithEntryPointMetadata
|
} satisfies ProjectWithEntryPointMetadata
|
||||||
|
|
||||||
const mockExportButton = vi.fn()
|
|
||||||
vi.mock('/src/components/ExportButton', () => ({
|
|
||||||
// engineCommandManager method call in ExportButton causes vitest to hang
|
|
||||||
ExportButton: (props: ExportButtonProps) => {
|
|
||||||
mockExportButton(props)
|
|
||||||
return <button>Fake export button</button>
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('ProjectSidebarMenu tests', () => {
|
describe('ProjectSidebarMenu tests', () => {
|
||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
render(
|
render(
|
||||||
|
@ -5,12 +5,12 @@ import { type IndexLoaderData } from 'lib/types'
|
|||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { isTauri } from '../lib/isTauri'
|
import { isTauri } from '../lib/isTauri'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ExportButton } from './ExportButton'
|
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { FileTree } from './FileTree'
|
import { FileTree } from './FileTree'
|
||||||
import { sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
import { Logo } from './Logo'
|
import { Logo } from './Logo'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
project,
|
project,
|
||||||
@ -21,6 +21,8 @@ const ProjectSidebarMenu = ({
|
|||||||
project?: IndexLoaderData['project']
|
project?: IndexLoaderData['project']
|
||||||
file?: IndexLoaderData['file']
|
file?: IndexLoaderData['file']
|
||||||
}) => {
|
}) => {
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
return renderAsLink ? (
|
return renderAsLink ? (
|
||||||
<Link
|
<Link
|
||||||
to={paths.HOME}
|
to={paths.HOME}
|
||||||
@ -112,13 +114,19 @@ const ProjectSidebarMenu = ({
|
|||||||
<div className="flex-1 overflow-hidden" />
|
<div className="flex-1 overflow-hidden" />
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
|
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
|
||||||
<ExportButton
|
<ActionButton
|
||||||
className={{
|
Element="button"
|
||||||
button: 'border-transparent dark:border-transparent',
|
icon={{ icon: 'exportFile', className: 'p-1' }}
|
||||||
}}
|
className="border-transparent dark:border-transparent"
|
||||||
|
onClick={() =>
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Export', ownerMachine: 'modeling' },
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Export Model
|
Export Part
|
||||||
</ExportButton>
|
</ActionButton>
|
||||||
{isTauri() && (
|
{isTauri() && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="link"
|
Element="link"
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
import {
|
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||||
MouseEventHandler,
|
|
||||||
WheelEventHandler,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { getNormalisedCoordinates, throttle } from '../lib/utils'
|
import { getNormalisedCoordinates } from '../lib/utils'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
@ -36,7 +29,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
}))
|
}))
|
||||||
const { settings } = useGlobalStateContext()
|
const { settings } = useGlobalStateContext()
|
||||||
const cameraControls = settings?.context?.cameraControls
|
|
||||||
const { state } = useModelingContext()
|
const { state } = useModelingContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { overallState } = useNetworkStatus()
|
const { overallState } = useNetworkStatus()
|
||||||
@ -68,19 +60,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
setClickCoords({ x, y })
|
setClickCoords({ x, y })
|
||||||
}
|
}
|
||||||
|
|
||||||
const fps = 60
|
|
||||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
|
|
||||||
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_zoom',
|
|
||||||
magnitude: e.deltaY * 0.4,
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
}, Math.round(1000 / fps))
|
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
|
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
|
||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
@ -159,7 +138,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
muted
|
muted
|
||||||
autoPlay
|
autoPlay
|
||||||
controls={false}
|
controls={false}
|
||||||
onWheel={handleScroll}
|
|
||||||
onPlay={() => setIsLoading(false)}
|
onPlay={() => setIsLoading(false)}
|
||||||
onMouseMoveCapture={handleMouseMove}
|
onMouseMoveCapture={handleMouseMove}
|
||||||
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
|
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
|
||||||
|
@ -28,7 +28,7 @@ interface UseStateMachineCommandsArgs<
|
|||||||
machineId: T['id']
|
machineId: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
actor?: InterpreterFrom<T>
|
actor: InterpreterFrom<T>
|
||||||
commandBarConfig?: CommandSetConfig<T, S>
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
allCommandsRequireNetwork?: boolean
|
allCommandsRequireNetwork?: boolean
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
|
@ -11,59 +11,53 @@ const mySketch001 = startSketchOn('XY')
|
|||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([0, 0], %)
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)`
|
||||||
show(mySketch001)`
|
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parse(code))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const shown = programMemory?.return?.map(
|
const sketch001 = programMemory?.root?.mySketch001
|
||||||
// @ts-ignore
|
expect(sketch001).toEqual({
|
||||||
(a) => programMemory?.root?.[a.name]
|
type: 'SketchGroup',
|
||||||
)
|
on: expect.any(Object),
|
||||||
expect(shown).toEqual([
|
start: {
|
||||||
{
|
to: [0, 0],
|
||||||
type: 'SketchGroup',
|
from: [0, 0],
|
||||||
on: expect.any(Object),
|
name: '',
|
||||||
start: {
|
__geoMeta: {
|
||||||
to: [0, 0],
|
id: expect.any(String),
|
||||||
from: [0, 0],
|
sourceRange: [46, 71],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
type: 'ToPoint',
|
||||||
name: '',
|
name: '',
|
||||||
|
to: [-1.59, -1.54],
|
||||||
|
from: [0, 0],
|
||||||
__geoMeta: {
|
__geoMeta: {
|
||||||
|
sourceRange: [77, 102],
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
sourceRange: [46, 71],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
value: [
|
{
|
||||||
{
|
type: 'ToPoint',
|
||||||
type: 'ToPoint',
|
to: [0.46, -5.82],
|
||||||
name: '',
|
from: [-1.59, -1.54],
|
||||||
to: [-1.59, -1.54],
|
name: '',
|
||||||
from: [0, 0],
|
__geoMeta: {
|
||||||
__geoMeta: {
|
sourceRange: [108, 132],
|
||||||
sourceRange: [77, 102],
|
id: expect.any(String),
|
||||||
id: expect.any(String),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
type: 'ToPoint',
|
],
|
||||||
to: [0.46, -5.82],
|
position: [0, 0, 0],
|
||||||
from: [-1.59, -1.54],
|
rotation: [0, 0, 0, 1],
|
||||||
name: '',
|
xAxis: { x: 1, y: 0, z: 0 },
|
||||||
__geoMeta: {
|
yAxis: { x: 0, y: 1, z: 0 },
|
||||||
sourceRange: [108, 132],
|
zAxis: { x: 0, y: 0, z: 1 },
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
},
|
entityId: expect.any(String),
|
||||||
},
|
__meta: [{ sourceRange: [46, 71] }],
|
||||||
],
|
})
|
||||||
position: [0, 0, 0],
|
|
||||||
rotation: [0, 0, 0, 1],
|
|
||||||
xAxis: { x: 1, y: 0, z: 0 },
|
|
||||||
yAxis: { x: 0, y: 1, z: 0 },
|
|
||||||
zAxis: { x: 0, y: 0, z: 1 },
|
|
||||||
id: expect.any(String),
|
|
||||||
entityId: expect.any(String),
|
|
||||||
__meta: [{ sourceRange: [46, 71] }],
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
test('extrude artifacts', async () => {
|
test('extrude artifacts', async () => {
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
@ -73,30 +67,24 @@ const mySketch001 = startSketchOn('XY')
|
|||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> extrude(2, %)
|
|> extrude(2, %)`
|
||||||
show(mySketch001)`
|
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parse(code))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const shown = programMemory?.return?.map(
|
const sketch001 = programMemory?.root?.mySketch001
|
||||||
// @ts-ignore
|
expect(sketch001).toEqual({
|
||||||
(a) => programMemory?.root?.[a.name]
|
type: 'ExtrudeGroup',
|
||||||
)
|
id: expect.any(String),
|
||||||
expect(shown).toEqual([
|
value: [],
|
||||||
{
|
height: 2,
|
||||||
type: 'ExtrudeGroup',
|
position: [0, 0, 0],
|
||||||
id: expect.any(String),
|
rotation: [0, 0, 0, 1],
|
||||||
value: [],
|
endCapId: null,
|
||||||
height: 2,
|
startCapId: null,
|
||||||
position: [0, 0, 0],
|
xAxis: { x: 1, y: 0, z: 0 },
|
||||||
rotation: [0, 0, 0, 1],
|
yAxis: { x: 0, y: 1, z: 0 },
|
||||||
endCapId: null,
|
zAxis: { x: 0, y: 0, z: 1 },
|
||||||
startCapId: null,
|
__meta: [{ sourceRange: [46, 71] }],
|
||||||
xAxis: { x: 1, y: 0, z: 0 },
|
})
|
||||||
yAxis: { x: 0, y: 1, z: 0 },
|
|
||||||
zAxis: { x: 0, y: 0, z: 1 },
|
|
||||||
__meta: [{ sourceRange: [46, 71] }],
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
test('sketch extrude and sketch on one of the faces', async () => {
|
test('sketch extrude and sketch on one of the faces', async () => {
|
||||||
// Enable rotations #152
|
// Enable rotations #152
|
||||||
@ -120,14 +108,10 @@ const sk2 = startSketchOn('XY')
|
|||||||
// |> transform(theTransf, %)
|
// |> transform(theTransf, %)
|
||||||
|> extrude(2, %)
|
|> extrude(2, %)
|
||||||
|
|
||||||
|
`
|
||||||
show(theExtrude, sk2)`
|
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parse(code))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const geos = programMemory?.return?.map(
|
const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2]
|
||||||
// @ts-ignore
|
|
||||||
({ name }) => programMemory?.root?.[name]
|
|
||||||
)
|
|
||||||
expect(geos).toEqual([
|
expect(geos).toEqual([
|
||||||
{
|
{
|
||||||
type: 'ExtrudeGroup',
|
type: 'ExtrudeGroup',
|
||||||
|
@ -47,9 +47,8 @@ const newVar = myVar + 1`
|
|||||||
|> lineTo([2,3], %)
|
|> lineTo([2,3], %)
|
||||||
|> lineTo({ to: [5,-1], tag: "rightPath" }, %)
|
|> lineTo({ to: [5,-1], tag: "rightPath" }, %)
|
||||||
// |> close(%)
|
// |> close(%)
|
||||||
show(mySketch)
|
|
||||||
`
|
`
|
||||||
const { root, return: _return } = await exe(code)
|
const { root } = await exe(code)
|
||||||
// geo is three js buffer geometry and is very bloated to have in tests
|
// geo is three js buffer geometry and is very bloated to have in tests
|
||||||
const minusGeo = root.mySketch.value
|
const minusGeo = root.mySketch.value
|
||||||
expect(minusGeo).toEqual([
|
expect(minusGeo).toEqual([
|
||||||
@ -84,15 +83,6 @@ show(mySketch)
|
|||||||
name: 'rightPath',
|
name: 'rightPath',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
// expect(root.mySketch.sketch[0]).toEqual(root.mySketch.sketch[4].firstPath)
|
|
||||||
expect(_return).toEqual([
|
|
||||||
{
|
|
||||||
type: 'Identifier',
|
|
||||||
start: 203,
|
|
||||||
end: 211,
|
|
||||||
name: 'mySketch',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pipe binary expression into call expression', async () => {
|
it('pipe binary expression into call expression', async () => {
|
||||||
@ -357,7 +347,6 @@ describe('testing math operators', () => {
|
|||||||
` -legLen(segLen('seg01', %), myVar)`,
|
` -legLen(segLen('seg01', %), myVar)`,
|
||||||
`], %)`,
|
`], %)`,
|
||||||
``,
|
``,
|
||||||
`show(part001)`,
|
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const { root } = await exe(code)
|
const { root } = await exe(code)
|
||||||
const sketch = root.part001
|
const sketch = root.part001
|
||||||
@ -392,8 +381,7 @@ const theExtrude = startSketchOn('XY')
|
|||||||
|> line([-0.76], myVarZ, %)
|
|> line([-0.76], myVarZ, %)
|
||||||
|> line([5,5], %)
|
|> line([5,5], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(4, %)
|
|> extrude(4, %)`
|
||||||
show(theExtrude)`
|
|
||||||
await expect(exe(code)).rejects.toEqual(
|
await expect(exe(code)).rejects.toEqual(
|
||||||
new KCLError(
|
new KCLError(
|
||||||
'undefined_value',
|
'undefined_value',
|
||||||
|
@ -122,7 +122,6 @@ describe('Testing addSketchTo', () => {
|
|||||||
expect(str).toBe(`const part001 = startSketchOn('YZ')
|
expect(str).toBe(`const part001 = startSketchOn('YZ')
|
||||||
|> startProfileAt('default', %)
|
|> startProfileAt('default', %)
|
||||||
|> line('default', %)
|
|> line('default', %)
|
||||||
show(part001)
|
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -147,8 +146,7 @@ describe('Testing giveSketchFnCallTag', () => {
|
|||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([0, 0], %)
|
||||||
|> line([-2.57, -0.13], %)
|
|> line([-2.57, -0.13], %)
|
||||||
|> line([0, 0.83], %)
|
|> line([0, 0.83], %)
|
||||||
|> line([0.82, 0.34], %)
|
|> line([0.82, 0.34], %)`
|
||||||
show(part001)`
|
|
||||||
it('Should add tag to a sketch function call', () => {
|
it('Should add tag to a sketch function call', () => {
|
||||||
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
|
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
|
||||||
code,
|
code,
|
||||||
@ -204,8 +202,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> angledLine([def(yo), 3.09], %)
|
|> angledLine([def(yo), 3.09], %)
|
||||||
|> angledLine([ghi(%), 3.09], %)
|
|> angledLine([ghi(%), 3.09], %)
|
||||||
|> angledLine([jkl(yo) + 2, 3.09], %)
|
|> angledLine([jkl(yo) + 2, 3.09], %)
|
||||||
const yo2 = hmm([identifierGuy + 5])
|
const yo2 = hmm([identifierGuy + 5])`
|
||||||
show(part001)`
|
|
||||||
it('should move a binary expression into a new variable', async () => {
|
it('should move a binary expression into a new variable', async () => {
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
PipeExpression,
|
PipeExpression,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
ExpressionStatement,
|
|
||||||
Value,
|
Value,
|
||||||
Literal,
|
Literal,
|
||||||
PipeSubstitution,
|
PipeSubstitution,
|
||||||
@ -128,16 +127,8 @@ export function addSketchTo(
|
|||||||
createPipeExpression(pipeBody)
|
createPipeExpression(pipeBody)
|
||||||
)
|
)
|
||||||
|
|
||||||
const showCallIndex = getShowIndex(_node)
|
_node.body = [...node.body, variableDeclaration]
|
||||||
let sketchIndex = showCallIndex
|
let sketchIndex = _node.body.length - 1
|
||||||
if (showCallIndex === -1) {
|
|
||||||
_node.body = [...node.body, variableDeclaration]
|
|
||||||
sketchIndex = _node.body.length - 1
|
|
||||||
} else {
|
|
||||||
const newBody = [...node.body]
|
|
||||||
newBody.splice(showCallIndex, 0, variableDeclaration)
|
|
||||||
_node.body = newBody
|
|
||||||
}
|
|
||||||
let pathToNode: PathToNode = [
|
let pathToNode: PathToNode = [
|
||||||
['body', ''],
|
['body', ''],
|
||||||
[sketchIndex, 'index'],
|
[sketchIndex, 'index'],
|
||||||
@ -150,7 +141,7 @@ export function addSketchTo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modifiedAst: addToShow(_node, _name),
|
modifiedAst: _node,
|
||||||
id: _name,
|
id: _name,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
}
|
}
|
||||||
@ -191,44 +182,6 @@ export function findUniqueName(
|
|||||||
return findUniqueName(searchStr, name, pad, index + 1)
|
return findUniqueName(searchStr, name, pad, index + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToShow(node: Program, name: string): Program {
|
|
||||||
const _node = { ...node }
|
|
||||||
const dumbyStartend = { start: 0, end: 0 }
|
|
||||||
const showCallIndex = getShowIndex(_node)
|
|
||||||
if (showCallIndex === -1) {
|
|
||||||
const showCall = createCallExpressionStdLib('show', [
|
|
||||||
createIdentifier(name),
|
|
||||||
])
|
|
||||||
const showExpressionStatement: ExpressionStatement = {
|
|
||||||
type: 'ExpressionStatement',
|
|
||||||
...dumbyStartend,
|
|
||||||
expression: showCall,
|
|
||||||
}
|
|
||||||
_node.body = [..._node.body, showExpressionStatement]
|
|
||||||
return _node
|
|
||||||
}
|
|
||||||
const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
|
|
||||||
const showCallArgs = (showCall.expression as CallExpression).arguments
|
|
||||||
const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)]
|
|
||||||
const newShowExpression = createCallExpressionStdLib('show', newShowCallArgs)
|
|
||||||
|
|
||||||
_node.body[showCallIndex] = {
|
|
||||||
...showCall,
|
|
||||||
expression: newShowExpression,
|
|
||||||
}
|
|
||||||
return _node
|
|
||||||
}
|
|
||||||
|
|
||||||
function getShowIndex(node: Program): number {
|
|
||||||
return node.body.findIndex(
|
|
||||||
(statement) =>
|
|
||||||
statement.type === 'ExpressionStatement' &&
|
|
||||||
statement.expression.type === 'CallExpression' &&
|
|
||||||
statement.expression.callee.type === 'Identifier' &&
|
|
||||||
statement.expression.callee.name === 'show'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mutateArrExp(
|
export function mutateArrExp(
|
||||||
node: Value,
|
node: Value,
|
||||||
updateWith: ArrayExpression
|
updateWith: ArrayExpression
|
||||||
@ -348,15 +301,10 @@ export function extrudeSketch(
|
|||||||
}
|
}
|
||||||
const name = findUniqueName(node, 'part')
|
const name = findUniqueName(node, 'part')
|
||||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||||
let showCallIndex = getShowIndex(_node)
|
_node.body.splice(_node.body.length, 0, VariableDeclaration)
|
||||||
if (showCallIndex === -1) {
|
|
||||||
// We didn't find a show, so let's just append everything
|
|
||||||
showCallIndex = _node.body.length
|
|
||||||
}
|
|
||||||
_node.body.splice(showCallIndex, 0, VariableDeclaration)
|
|
||||||
const pathToExtrudeArg: PathToNode = [
|
const pathToExtrudeArg: PathToNode = [
|
||||||
['body', ''],
|
['body', ''],
|
||||||
[showCallIndex, 'index'],
|
[_node.body.length, 'index'],
|
||||||
['declarations', 'VariableDeclaration'],
|
['declarations', 'VariableDeclaration'],
|
||||||
[0, 'index'],
|
[0, 'index'],
|
||||||
['init', 'VariableDeclarator'],
|
['init', 'VariableDeclarator'],
|
||||||
@ -365,7 +313,7 @@ export function extrudeSketch(
|
|||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
modifiedAst: node,
|
modifiedAst: node,
|
||||||
pathToNode: [...pathToNode.slice(0, -1), [showCallIndex, 'index']],
|
pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']],
|
||||||
pathToExtrudeArg,
|
pathToExtrudeArg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -425,7 +373,7 @@ export function sketchOnExtrudedFace(
|
|||||||
_node.body.splice(expressionIndex + 1, 0, newSketch)
|
_node.body.splice(expressionIndex + 1, 0, newSketch)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modifiedAst: addToShow(_node, newSketchName),
|
modifiedAst: _node,
|
||||||
pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']],
|
pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> xLine(3.84, %) // selection-range-7ish-before-this
|
|> xLine(3.84, %) // selection-range-7ish-before-this
|
||||||
|
|
||||||
const variableBelowShouldNotBeIncluded = 3
|
const variableBelowShouldNotBeIncluded = 3
|
||||||
|
`
|
||||||
show(part001)`
|
|
||||||
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
@ -69,8 +68,7 @@ describe('testing argIsNotIdentifier', () => {
|
|||||||
|> angledLine([ghi(%), 3.09], %)
|
|> angledLine([ghi(%), 3.09], %)
|
||||||
|> angledLine([jkl('yo') + 2, 3.09], %)
|
|> angledLine([jkl('yo') + 2, 3.09], %)
|
||||||
const yo = 5 + 6
|
const yo = 5 + 6
|
||||||
const yo2 = hmm([identifierGuy + 5])
|
const yo2 = hmm([identifierGuy + 5])`
|
||||||
show(part001)`
|
|
||||||
it('find a safe binaryExpression', () => {
|
it('find a safe binaryExpression', () => {
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
const rangeStart = code.indexOf('100 + 100') + 2
|
const rangeStart = code.indexOf('100 + 100') + 2
|
||||||
@ -201,8 +199,7 @@ describe('testing getNodePathFromSourceRange', () => {
|
|||||||
const code = `const part001 = startSketchOn('XY')
|
const code = `const part001 = startSketchOn('XY')
|
||||||
|> startProfileAt([0.39, -0.05], %)
|
|> startProfileAt([0.39, -0.05], %)
|
||||||
|> line([0.94, 2.61], %)
|
|> line([0.94, 2.61], %)
|
||||||
|> line([-0.21, -1.4], %)
|
|> line([-0.21, -1.4], %)`
|
||||||
show(part001)`
|
|
||||||
it('finds the second line when cursor is put at the end', () => {
|
it('finds the second line when cursor is put at the end', () => {
|
||||||
const searchLn = `line([0.94, 2.61], %)`
|
const searchLn = `line([0.94, 2.61], %)`
|
||||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||||
|
@ -68,8 +68,6 @@ log(5, myVar)
|
|||||||
|> lineTo([1, 1], %)
|
|> lineTo([1, 1], %)
|
||||||
|> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|
|> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|
|
||||||
show(mySketch)
|
|
||||||
`
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
@ -331,7 +329,6 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
|
|||||||
intersectTag: 'seg01'
|
intersectTag: 'seg01'
|
||||||
}, %)
|
}, %)
|
||||||
|> line([-0.42, -1.72], %)
|
|> line([-0.42, -1.72], %)
|
||||||
show(part001)
|
|
||||||
`
|
`
|
||||||
const { ast } = code2ast(code)
|
const { ast } = code2ast(code)
|
||||||
const recasted = recast(ast)
|
const recasted = recast(ast)
|
||||||
|
@ -796,7 +796,7 @@ interface UnreliableSubscription<T extends UnreliableResponses['type']> {
|
|||||||
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Subscription<T extends ModelTypes> {
|
export interface Subscription<T extends ModelTypes> {
|
||||||
event: T
|
event: T
|
||||||
callback: (
|
callback: (
|
||||||
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
|
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
|
||||||
@ -926,6 +926,15 @@ export class EngineCommandManager {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
sceneInfra.camControls.onCameraChange()
|
sceneInfra.camControls.onCameraChange()
|
||||||
|
this.sendSceneCommand({
|
||||||
|
// CameraControls subscribes to default_camera_get_settings response events
|
||||||
|
// firing this at connection ensure the camera's are synced initially
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
this.initPlanes().then(() => {
|
this.initPlanes().then(() => {
|
||||||
this.resolveReady()
|
this.resolveReady()
|
||||||
|
@ -101,7 +101,6 @@ describe('testing changeSketchArguments', () => {
|
|||||||
|> ${line}
|
|> ${line}
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
show(mySketch001)
|
|
||||||
`
|
`
|
||||||
const code = genCode(lineToChange)
|
const code = genCode(lineToChange)
|
||||||
const expectedCode = genCode(lineAfterChange)
|
const expectedCode = genCode(lineAfterChange)
|
||||||
@ -128,8 +127,7 @@ const mySketch001 = startSketchOn('XY')
|
|||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([0, 0], %)
|
||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)`
|
||||||
show(mySketch001)`
|
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
const programMemory = await enginelessExecutor(ast)
|
const programMemory = await enginelessExecutor(ast)
|
||||||
const sourceStart = code.indexOf(lineToChange)
|
const sourceStart = code.indexOf(lineToChange)
|
||||||
@ -155,7 +153,6 @@ show(mySketch001)`
|
|||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
|> lineTo([2, 3], %)
|
|> lineTo([2, 3], %)
|
||||||
show(mySketch001)
|
|
||||||
`
|
`
|
||||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||||
|
|
||||||
@ -177,7 +174,6 @@ show(mySketch001)
|
|||||||
|> lineTo([-1.59, -1.54], %)
|
|> lineTo([-1.59, -1.54], %)
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
show(mySketch001)
|
|
||||||
`
|
`
|
||||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||||
})
|
})
|
||||||
@ -192,7 +188,6 @@ describe('testing addTagForSketchOnFace', () => {
|
|||||||
// |> rx(45, %)
|
// |> rx(45, %)
|
||||||
|> ${line}
|
|> ${line}
|
||||||
|> lineTo([0.46, -5.82], %)
|
|> lineTo([0.46, -5.82], %)
|
||||||
show(mySketch001)
|
|
||||||
`
|
`
|
||||||
const code = genCode(originalLine)
|
const code = genCode(originalLine)
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
|
@ -91,12 +91,6 @@ export function createFirstArg(
|
|||||||
throw new Error('all sketch line types should have been covered')
|
throw new Error('all sketch line types should have been covered')
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
type LineData = {
|
|
||||||
from: [number, number, number]
|
|
||||||
to: [number, number, number]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const lineTo: SketchLineHelper = {
|
export const lineTo: SketchLineHelper = {
|
||||||
add: ({
|
add: ({
|
||||||
node,
|
node,
|
||||||
@ -966,6 +960,30 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
|||||||
addTag: addTagWithTo('angleTo'), // TODO might be wrong
|
addTag: addTagWithTo('angleTo'), // TODO might be wrong
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
|
||||||
|
node,
|
||||||
|
pathToNode,
|
||||||
|
to,
|
||||||
|
}) => {
|
||||||
|
const _node = { ...node }
|
||||||
|
const { node: callExpression } = getNodeFromPath<CallExpression>(
|
||||||
|
_node,
|
||||||
|
pathToNode
|
||||||
|
)
|
||||||
|
|
||||||
|
const toArrExp = createArrayExpression([
|
||||||
|
createLiteral(roundOff(to[0])),
|
||||||
|
createLiteral(roundOff(to[1])),
|
||||||
|
])
|
||||||
|
|
||||||
|
mutateArrExp(callExpression.arguments?.[0], toArrExp) ||
|
||||||
|
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
||||||
|
return {
|
||||||
|
modifiedAst: _node,
|
||||||
|
pathToNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
|
export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
|
||||||
line,
|
line,
|
||||||
lineTo,
|
lineTo,
|
||||||
|
@ -88,7 +88,6 @@ describe('testing swapping out sketch calls with xLine/xLineTo', () => {
|
|||||||
` |> yLine(-1.07, %)`,
|
` |> yLine(-1.07, %)`,
|
||||||
` |> xLineTo(3.27, %)`,
|
` |> xLineTo(3.27, %)`,
|
||||||
` |> yLineTo(2.14, %)`,
|
` |> yLineTo(2.14, %)`,
|
||||||
`show(part001)`,
|
|
||||||
]
|
]
|
||||||
const bigExample = bigExampleArr.join('\n')
|
const bigExample = bigExampleArr.join('\n')
|
||||||
it('line with tag converts to xLine', async () => {
|
it('line with tag converts to xLine', async () => {
|
||||||
@ -290,7 +289,6 @@ describe('testing swapping out sketch calls with xLine/xLineTo while keeping var
|
|||||||
` |> angledLineToX([330, angledLineToXx], %)`,
|
` |> angledLineToX([330, angledLineToXx], %)`,
|
||||||
` |> angledLineToY([217, angledLineToYy], %)`,
|
` |> angledLineToY([217, angledLineToYy], %)`,
|
||||||
` |> line([0.89, -0.1], %)`,
|
` |> line([0.89, -0.1], %)`,
|
||||||
`show(part001)`,
|
|
||||||
]
|
]
|
||||||
const varExample = variablesExampleArr.join('\n')
|
const varExample = variablesExampleArr.join('\n')
|
||||||
it('line keeps variable when converted to xLine', async () => {
|
it('line keeps variable when converted to xLine', async () => {
|
||||||
@ -378,8 +376,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> line([0, 0.4], %)
|
|> line([0, 0.4], %)
|
||||||
|> xLine(3.48, %)
|
|> xLine(3.48, %)
|
||||||
|> line([2.14, 1.35], %) // normal-segment
|
|> line([2.14, 1.35], %) // normal-segment
|
||||||
|> xLine(3.54, %)
|
|> xLine(3.54, %)`
|
||||||
show(part001)`
|
|
||||||
it('normal case works', async () => {
|
it('normal case works', async () => {
|
||||||
const programMemory = await enginelessExecutor(parse(code))
|
const programMemory = await enginelessExecutor(parse(code))
|
||||||
const index = code.indexOf('// normal-segment') - 7
|
const index = code.indexOf('// normal-segment') - 7
|
||||||
|
@ -123,7 +123,6 @@ const part001 = startSketchOn('XY')
|
|||||||
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|
||||||
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|
||||||
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
|
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
|
||||||
show(part001)
|
|
||||||
`
|
`
|
||||||
const expectModifiedScript = `const myVar = 3
|
const expectModifiedScript = `const myVar = 3
|
||||||
const myVar2 = 5
|
const myVar2 = 5
|
||||||
@ -196,7 +195,6 @@ const part001 = startSketchOn('XY')
|
|||||||
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|
||||||
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|
||||||
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
|
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
|
||||||
show(part001)
|
|
||||||
`
|
`
|
||||||
it('should transform the ast', async () => {
|
it('should transform the ast', async () => {
|
||||||
const ast = parse(inputScript)
|
const ast = parse(inputScript)
|
||||||
@ -257,7 +255,6 @@ const part001 = startSketchOn('XY')
|
|||||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||||
show(part001)
|
|
||||||
`
|
`
|
||||||
it('should transform horizontal lines the ast', async () => {
|
it('should transform horizontal lines the ast', async () => {
|
||||||
const expectModifiedScript = `const myVar = 2
|
const expectModifiedScript = `const myVar = 2
|
||||||
@ -286,7 +283,6 @@ const part001 = startSketchOn('XY')
|
|||||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||||
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|
||||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||||
show(part001)
|
|
||||||
`
|
`
|
||||||
const ast = parse(inputScript)
|
const ast = parse(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
@ -345,7 +341,6 @@ const part001 = startSketchOn('XY')
|
|||||||
|> yLineTo(7.68, %) // select for vertical constraint 9
|
|> yLineTo(7.68, %) // select for vertical constraint 9
|
||||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||||
|> yLineTo(myVar, %) // select for vertical constraint 10
|
|> yLineTo(myVar, %) // select for vertical constraint 10
|
||||||
show(part001)
|
|
||||||
`
|
`
|
||||||
const ast = parse(inputScript)
|
const ast = parse(inputScript)
|
||||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||||
@ -389,7 +384,6 @@ const part001 = startSketchOn('XY')
|
|||||||
|> line([0.45, 1.46], %) // free
|
|> line([0.45, 1.46], %) // free
|
||||||
|> line([myVar, 0.01], %) // xRelative
|
|> line([myVar, 0.01], %) // xRelative
|
||||||
|> line([0.7, myVar], %) // yRelative
|
|> line([0.7, myVar], %) // yRelative
|
||||||
show(part001)
|
|
||||||
`
|
`
|
||||||
it('testing for free to horizontal and vertical distance', async () => {
|
it('testing for free to horizontal and vertical distance', async () => {
|
||||||
const expectedHorizontalCode = await helperThing(
|
const expectedHorizontalCode = await helperThing(
|
||||||
@ -501,8 +495,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> xLine(3.36, %) // partial
|
|> xLine(3.36, %) // partial
|
||||||
|> line([-1.49, 1.06], %) // free
|
|> line([-1.49, 1.06], %) // free
|
||||||
|> xLine(-3.43 + 0, %) // full
|
|> xLine(-3.43 + 0, %) // full
|
||||||
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full
|
|> angledLineOfXLength([243 + 0, 1.2 + 0], %) // full`
|
||||||
show(part001)`
|
|
||||||
const ast = parse(code)
|
const ast = parse(code)
|
||||||
const constraintLevels: ReturnType<
|
const constraintLevels: ReturnType<
|
||||||
typeof getConstraintLevelFromSourceRange
|
typeof getConstraintLevelFromSourceRange
|
||||||
|
@ -15,8 +15,7 @@ describe('testing angledLineThatIntersects', () => {
|
|||||||
offset: ${offset},
|
offset: ${offset},
|
||||||
tag: "yo2"
|
tag: "yo2"
|
||||||
}, %)
|
}, %)
|
||||||
const intersect = segEndX('yo2', part001)
|
const intersect = segEndX('yo2', part001)`
|
||||||
show(part001)`
|
|
||||||
const { root } = await enginelessExecutor(parse(code('-1')))
|
const { root } = await enginelessExecutor(parse(code('-1')))
|
||||||
expect(root.intersect.value).toBe(1 + Math.sqrt(2))
|
expect(root.intersect.value).toBe(1 + Math.sqrt(2))
|
||||||
const { root: noOffset } = await enginelessExecutor(parse(code('0')))
|
const { root: noOffset } = await enginelessExecutor(parse(code('0')))
|
||||||
|
@ -40,9 +40,9 @@ export interface MouseGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const butName = (e: React.MouseEvent) => ({
|
const butName = (e: React.MouseEvent) => ({
|
||||||
middle: !!(e.buttons & 4),
|
middle: !!(e.buttons & 4) || e.button === 1,
|
||||||
right: !!(e.buttons & 2),
|
right: !!(e.buttons & 2) || e.button === 2,
|
||||||
left: !!(e.buttons & 1),
|
left: !!(e.buttons & 1) || e.button === 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||||
|
@ -28,7 +28,8 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
name: {
|
name: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
context.projects.map((p) => ({
|
context.projects.map((p) => ({
|
||||||
name: p.name!,
|
name: p.name!,
|
||||||
value: p.name!,
|
value: p.name!,
|
||||||
@ -43,7 +44,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
name: {
|
name: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.defaultProjectName,
|
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -55,7 +56,8 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
name: {
|
name: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
context.projects.map((p) => ({
|
context.projects.map((p) => ({
|
||||||
name: p.name!,
|
name: p.name!,
|
||||||
value: p.name!,
|
value: p.name!,
|
||||||
@ -71,7 +73,8 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
oldName: {
|
oldName: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
context.projects.map((p) => ({
|
context.projects.map((p) => ({
|
||||||
name: p.name!,
|
name: p.name!,
|
||||||
value: p.name!,
|
value: p.name!,
|
||||||
@ -80,7 +83,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
newName: {
|
newName: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.defaultProjectName,
|
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
|
||||||
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
|
type OutputTypeKey = OutputFormat['type']
|
||||||
|
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
||||||
|
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
||||||
|
|
||||||
export const EXTRUSION_RESULTS = [
|
export const EXTRUSION_RESULTS = [
|
||||||
'new',
|
'new',
|
||||||
'add',
|
'add',
|
||||||
@ -11,6 +17,10 @@ export const EXTRUSION_RESULTS = [
|
|||||||
|
|
||||||
export type ModelingCommandSchema = {
|
export type ModelingCommandSchema = {
|
||||||
'Enter sketch': {}
|
'Enter sketch': {}
|
||||||
|
Export: {
|
||||||
|
type: OutputTypeKey
|
||||||
|
storage?: StorageUnion
|
||||||
|
}
|
||||||
Extrude: {
|
Extrude: {
|
||||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||||
@ -26,6 +36,80 @@ export const modelingMachineConfig: CommandSetConfig<
|
|||||||
description: 'Enter sketch mode.',
|
description: 'Enter sketch mode.',
|
||||||
icon: 'sketch',
|
icon: 'sketch',
|
||||||
},
|
},
|
||||||
|
Export: {
|
||||||
|
description: 'Export the current model.',
|
||||||
|
icon: 'exportFile',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
type: {
|
||||||
|
inputType: 'options',
|
||||||
|
defaultValue: 'gltf',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ name: 'gLTF', isCurrent: true, value: 'gltf' },
|
||||||
|
{ name: 'OBJ', isCurrent: false, value: 'obj' },
|
||||||
|
{ name: 'STL', isCurrent: false, value: 'stl' },
|
||||||
|
{ name: 'STEP', isCurrent: false, value: 'step' },
|
||||||
|
{ name: 'PLY', isCurrent: false, value: 'ply' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
inputType: 'options',
|
||||||
|
defaultValue: (c) => {
|
||||||
|
switch (c.argumentsToSubmit.type) {
|
||||||
|
case 'gltf':
|
||||||
|
return 'embedded'
|
||||||
|
case 'stl':
|
||||||
|
return 'ascii'
|
||||||
|
case 'ply':
|
||||||
|
return 'ascii'
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skip: true,
|
||||||
|
required: (commandContext) =>
|
||||||
|
['gltf', 'stl', 'ply'].includes(
|
||||||
|
commandContext.argumentsToSubmit.type as string
|
||||||
|
),
|
||||||
|
options: (commandContext) => {
|
||||||
|
const type = commandContext.argumentsToSubmit.type as
|
||||||
|
| OutputTypeKey
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'gltf':
|
||||||
|
return [
|
||||||
|
{ name: 'embedded', isCurrent: true, value: 'embedded' },
|
||||||
|
{ name: 'binary', isCurrent: false, value: 'binary' },
|
||||||
|
{ name: 'standard', isCurrent: false, value: 'standard' },
|
||||||
|
]
|
||||||
|
case 'stl':
|
||||||
|
return [
|
||||||
|
{ name: 'binary', isCurrent: false, value: 'binary' },
|
||||||
|
{ name: 'ascii', isCurrent: true, value: 'ascii' },
|
||||||
|
]
|
||||||
|
case 'ply':
|
||||||
|
return [
|
||||||
|
{ name: 'ascii', isCurrent: true, value: 'ascii' },
|
||||||
|
{
|
||||||
|
name: 'binary_big_endian',
|
||||||
|
isCurrent: false,
|
||||||
|
value: 'binary_big_endian',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'binary_little_endian',
|
||||||
|
isCurrent: false,
|
||||||
|
value: 'binary_little_endian',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
Extrude: {
|
Extrude: {
|
||||||
description: 'Pull a sketch into 3D along its normal or perpendicular.',
|
description: 'Pull a sketch into 3D along its normal or perpendicular.',
|
||||||
icon: 'extrude',
|
icon: 'extrude',
|
||||||
|
@ -41,8 +41,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
baseUnit: {
|
baseUnit: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.baseUnit,
|
defaultValueFromContext: (context) => context.baseUnit,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
Object.values(baseUnitsUnion).map((v) => ({
|
Object.values(baseUnitsUnion).map((v) => ({
|
||||||
name: v,
|
name: v,
|
||||||
value: v,
|
value: v,
|
||||||
@ -57,8 +58,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
cameraControls: {
|
cameraControls: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.cameraControls,
|
defaultValueFromContext: (context) => context.cameraControls,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
Object.values(cameraSystems).map((v) => ({
|
Object.values(cameraSystems).map((v) => ({
|
||||||
name: v,
|
name: v,
|
||||||
value: v,
|
value: v,
|
||||||
@ -74,7 +76,7 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
defaultProjectName: {
|
defaultProjectName: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.defaultProjectName,
|
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -84,8 +86,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
textWrapping: {
|
textWrapping: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.textWrapping,
|
defaultValueFromContext: (context) => context.textWrapping,
|
||||||
options: (context) => [
|
options: [],
|
||||||
|
optionsFromContext: (context) => [
|
||||||
{
|
{
|
||||||
name: 'On',
|
name: 'On',
|
||||||
value: 'On' as Toggle,
|
value: 'On' as Toggle,
|
||||||
@ -106,8 +109,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
theme: {
|
theme: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.theme,
|
defaultValueFromContext: (context) => context.theme,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
Object.values(Themes).map((v) => ({
|
Object.values(Themes).map((v) => ({
|
||||||
name: v,
|
name: v,
|
||||||
value: v,
|
value: v,
|
||||||
@ -122,8 +126,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
unitSystem: {
|
unitSystem: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.unitSystem,
|
defaultValueFromContext: (context) => context.unitSystem,
|
||||||
options: (context) => [
|
options: [],
|
||||||
|
optionsFromContext: (context) => [
|
||||||
{
|
{
|
||||||
name: 'Imperial',
|
name: 'Imperial',
|
||||||
value: 'imperial' as UnitSystem,
|
value: 'imperial' as UnitSystem,
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { Selection } from './selections'
|
import { Selection } from './selections'
|
||||||
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
|
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
|
||||||
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
type Icon = CustomIconName
|
type Icon = CustomIconName
|
||||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
@ -93,15 +94,31 @@ export type CommandArgumentConfig<
|
|||||||
> =
|
> =
|
||||||
| {
|
| {
|
||||||
description?: string
|
description?: string
|
||||||
required: boolean
|
required:
|
||||||
skip?: true
|
| boolean
|
||||||
|
| ((
|
||||||
|
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
|
) => boolean)
|
||||||
|
skip?: boolean
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'options'>
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
options:
|
options:
|
||||||
| CommandArgumentOption<OutputType>[]
|
| CommandArgumentOption<OutputType>[]
|
||||||
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
| ((
|
||||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
commandBarContext: {
|
||||||
|
argumentsToSubmit: Record<string, unknown>
|
||||||
|
} // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
|
) => CommandArgumentOption<OutputType>[])
|
||||||
|
optionsFromContext?: (
|
||||||
|
context: ContextFrom<T>
|
||||||
|
) => CommandArgumentOption<OutputType>[]
|
||||||
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||||
|
) => OutputType)
|
||||||
|
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'selection'>
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
@ -111,7 +128,12 @@ export type CommandArgumentConfig<
|
|||||||
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'string'>
|
inputType: Extract<CommandInputType, 'string'>
|
||||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||||
|
) => OutputType)
|
||||||
|
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -121,24 +143,42 @@ export type CommandArgument<
|
|||||||
> =
|
> =
|
||||||
| {
|
| {
|
||||||
description?: string
|
description?: string
|
||||||
required: boolean
|
required:
|
||||||
skip?: true
|
| boolean
|
||||||
|
| ((
|
||||||
|
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
|
) => boolean)
|
||||||
|
skip?: boolean
|
||||||
|
machineActor: InterpreterFrom<T>
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'options'>
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
options: CommandArgumentOption<OutputType>[]
|
options:
|
||||||
defaultValue?: OutputType
|
| CommandArgumentOption<OutputType>[]
|
||||||
|
| ((
|
||||||
|
commandBarContext: {
|
||||||
|
argumentsToSubmit: Record<string, unknown>
|
||||||
|
} // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
|
) => CommandArgumentOption<OutputType>[])
|
||||||
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||||
|
) => OutputType)
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'selection'>
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
selectionTypes: Selection['type'][]
|
selectionTypes: Selection['type'][]
|
||||||
actor: InterpreterFrom<T>
|
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
}
|
}
|
||||||
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'string'>
|
inputType: Extract<CommandInputType, 'string'>
|
||||||
defaultValue?: OutputType
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||||
|
) => OutputType)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ interface CreateMachineCommandProps<
|
|||||||
ownerMachine: T['id']
|
ownerMachine: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
actor?: InterpreterFrom<T>
|
actor: InterpreterFrom<T>
|
||||||
commandBarConfig?: CommandSetConfig<T, S>
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
@ -91,13 +91,13 @@ function buildCommandArguments<
|
|||||||
>(
|
>(
|
||||||
state: StateFrom<T>,
|
state: StateFrom<T>,
|
||||||
args: CommandConfig<T, CommandName, S>['args'],
|
args: CommandConfig<T, CommandName, S>['args'],
|
||||||
actor?: InterpreterFrom<T>
|
machineActor: InterpreterFrom<T>
|
||||||
): NonNullable<Command<T, CommandName, S>['args']> {
|
): NonNullable<Command<T, CommandName, S>['args']> {
|
||||||
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
||||||
|
|
||||||
for (const arg in args) {
|
for (const arg in args) {
|
||||||
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
||||||
const newArg = buildCommandArgument(argConfig, arg, state, actor)
|
const newArg = buildCommandArgument(argConfig, arg, state, machineActor)
|
||||||
newArgs[arg] = newArg
|
newArgs[arg] = newArg
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,44 +111,36 @@ function buildCommandArgument<
|
|||||||
arg: CommandArgumentConfig<O, T>,
|
arg: CommandArgumentConfig<O, T>,
|
||||||
argName: string,
|
argName: string,
|
||||||
state: StateFrom<T>,
|
state: StateFrom<T>,
|
||||||
actor?: InterpreterFrom<T>
|
machineActor: InterpreterFrom<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
const baseCommandArgument = {
|
const baseCommandArgument = {
|
||||||
description: arg.description,
|
description: arg.description,
|
||||||
required: arg.required,
|
required: arg.required,
|
||||||
skip: arg.skip,
|
skip: arg.skip,
|
||||||
|
machineActor,
|
||||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||||
|
|
||||||
if (arg.inputType === 'options') {
|
if (arg.inputType === 'options') {
|
||||||
const options = arg.options
|
if (!arg.options) {
|
||||||
? arg.options instanceof Function
|
|
||||||
? arg.options(state.context)
|
|
||||||
: arg.options
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (!options) {
|
|
||||||
throw new Error('Options must be provided for options input type')
|
throw new Error('Options must be provided for options input type')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
defaultValue:
|
defaultValue: arg.defaultValueFromContext
|
||||||
arg.defaultValue instanceof Function
|
? arg.defaultValueFromContext(state.context)
|
||||||
? arg.defaultValue(state.context)
|
: arg.defaultValue,
|
||||||
: arg.defaultValue,
|
options: arg.optionsFromContext
|
||||||
options,
|
? arg.optionsFromContext(state.context)
|
||||||
|
: arg.options,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
||||||
} else if (arg.inputType === 'selection') {
|
} else if (arg.inputType === 'selection') {
|
||||||
if (!actor)
|
|
||||||
throw new Error('Actor must be provided for selection input type')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
multiple: arg.multiple,
|
multiple: arg.multiple,
|
||||||
selectionTypes: arg.selectionTypes,
|
selectionTypes: arg.selectionTypes,
|
||||||
actor,
|
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
} else if (arg.inputType === 'kcl') {
|
} else if (arg.inputType === 'kcl') {
|
||||||
return {
|
return {
|
||||||
@ -159,10 +151,7 @@ function buildCommandArgument<
|
|||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
defaultValue:
|
defaultValue: arg.defaultValue,
|
||||||
arg.defaultValue instanceof Function
|
|
||||||
? arg.defaultValue(state.context)
|
|
||||||
: arg.defaultValue,
|
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
src/lib/exportFromEngine.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { type Models } from '@kittycad/lib'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
// Isolating a function to call the engine to export the current scene.
|
||||||
|
// Because it has given us trouble in automated testing environments.
|
||||||
|
export function exportFromEngine({
|
||||||
|
source_unit,
|
||||||
|
format,
|
||||||
|
}: {
|
||||||
|
source_unit: Models['UnitLength_type']
|
||||||
|
format: Models['OutputFormat_type']
|
||||||
|
}) {
|
||||||
|
return engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'export',
|
||||||
|
// By default let's leave this blank to export the whole scene.
|
||||||
|
// In the future we might want to let the user choose which entities
|
||||||
|
// in the scene to export. In that case, you'd pass the IDs thru here.
|
||||||
|
entity_ids: [],
|
||||||
|
format,
|
||||||
|
source_unit,
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
@ -20,6 +20,7 @@ import {
|
|||||||
TANGENTIAL_ARC_TO_SEGMENT,
|
TANGENTIAL_ARC_TO_SEGMENT,
|
||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
getParentGroup,
|
getParentGroup,
|
||||||
|
PROFILE_START,
|
||||||
} from 'clientSideScene/sceneEntities'
|
} from 'clientSideScene/sceneEntities'
|
||||||
import { Mesh } from 'three'
|
import { Mesh } from 'three'
|
||||||
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
|
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
|
||||||
@ -188,7 +189,11 @@ export async function getEventForSelectWithPoint(
|
|||||||
export function getEventForSegmentSelection(
|
export function getEventForSegmentSelection(
|
||||||
obj: any
|
obj: any
|
||||||
): ModelingMachineEvent | null {
|
): ModelingMachineEvent | null {
|
||||||
const group = getParentGroup(obj)
|
const group = getParentGroup(obj, [
|
||||||
|
STRAIGHT_SEGMENT,
|
||||||
|
TANGENTIAL_ARC_TO_SEGMENT,
|
||||||
|
PROFILE_START,
|
||||||
|
])
|
||||||
const axisGroup = getParentGroup(obj, [AXIS_GROUP])
|
const axisGroup = getParentGroup(obj, [AXIS_GROUP])
|
||||||
if (!group && !axisGroup) return null
|
if (!group && !axisGroup) return null
|
||||||
if (axisGroup?.userData.type === AXIS_GROUP) {
|
if (axisGroup?.userData.type === AXIS_GROUP) {
|
||||||
@ -407,8 +412,8 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
|
|||||||
}
|
}
|
||||||
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
|
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
|
||||||
if (
|
if (
|
||||||
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT].includes(
|
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT, PROFILE_START].includes(
|
||||||
segmentGroup?.userData?.type
|
segmentGroup?.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@ -420,7 +425,9 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
|
|||||||
const groupHasCursor = codeBasedSelections.some((selection) => {
|
const groupHasCursor = codeBasedSelections.some((selection) => {
|
||||||
return isOverlap(selection.range, [node.start, node.end])
|
return isOverlap(selection.range, [node.start, node.end])
|
||||||
})
|
})
|
||||||
const color = groupHasCursor ? 0x0000ff : 0xffffff
|
const color = groupHasCursor
|
||||||
|
? 0x0000ff
|
||||||
|
: segmentGroup?.userData?.baseColor || 0xffffff
|
||||||
segmentGroup.traverse(
|
segmentGroup.traverse(
|
||||||
(child) => child instanceof Mesh && child.material.color.set(color)
|
(child) => child instanceof Mesh && child.material.color.set(color)
|
||||||
)
|
)
|
||||||
|
@ -8,21 +8,29 @@ import {
|
|||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
|
|
||||||
|
export type CommandBarContext = {
|
||||||
|
commands: Command[]
|
||||||
|
selectedCommand?: Command
|
||||||
|
currentArgument?: CommandArgument<unknown> & { name: string }
|
||||||
|
selectionRanges: Selections
|
||||||
|
argumentsToSubmit: { [x: string]: unknown }
|
||||||
|
}
|
||||||
|
|
||||||
export const commandBarMachine = createMachine(
|
export const commandBarMachine = createMachine(
|
||||||
{
|
{
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSjHFPHQNFCIzk3jWRURJIAQNvlA4oFo8zWlSPUT00pVhYw9NMHWrhKwbH2GaNQgdYxNm-JDPAxCvLw1mAzTY3opJ9TqKFehqlUTMMwiUdIrNA5gMbB8IhQlh5bkWFsVwyJshYmkNYQs9DNhI2vJhFQZp+SgkDklXS+kr7wHUZA+G-UzwygUPyLB+xApFh9BaCU6hpSLGqNKDheC5yBlsfNCORlTLmTWpGaytl+G0wwhoEU7ilDWnMN4zGhRPqO0Cn1XQthVKaBsbNZK9iYlLUyhfBJKSR7OH2FYAi1oDGzFSNIIUGwZiVD0BKTCSkFCB2FiZRpIlMjgk+v2VQch0jEUKIYz+HoOSaA0IeJRO8m4OImXLTQjDYRpAFIsfckillZAUjYGipceQ6zvF4IAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */
|
||||||
|
predictableActionArguments: true,
|
||||||
|
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
|
||||||
context: {
|
context: {
|
||||||
commands: [] as Command[],
|
commands: [],
|
||||||
selectedCommand: undefined as Command | undefined,
|
selectedCommand: undefined,
|
||||||
currentArgument: undefined as
|
currentArgument: undefined,
|
||||||
| (CommandArgument<unknown> & { name: string })
|
|
||||||
| undefined,
|
|
||||||
selectionRanges: {
|
selectionRanges: {
|
||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
codeBasedSelections: [],
|
codeBasedSelections: [],
|
||||||
} as Selections,
|
},
|
||||||
argumentsToSubmit: {} as { [x: string]: unknown },
|
argumentsToSubmit: {},
|
||||||
},
|
} as CommandBarContext,
|
||||||
id: 'Command Bar',
|
id: 'Command Bar',
|
||||||
initial: 'Closed',
|
initial: 'Closed',
|
||||||
states: {
|
states: {
|
||||||
@ -267,7 +275,6 @@ export const commandBarMachine = createMachine(
|
|||||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
predictableActionArguments: true,
|
|
||||||
preserveActionOrder: true,
|
preserveActionOrder: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -279,28 +286,45 @@ export const commandBarMachine = createMachine(
|
|||||||
(selectedCommand?.args && event.type === 'Submit command') ||
|
(selectedCommand?.args && event.type === 'Submit command') ||
|
||||||
event.type === 'done.invoke.validateArguments'
|
event.type === 'done.invoke.validateArguments'
|
||||||
) {
|
) {
|
||||||
selectedCommand?.onSubmit(getCommandArgumentKclValuesOnly(event.data))
|
const resolvedArgs = {} as { [x: string]: unknown }
|
||||||
|
for (const [argName, argValue] of Object.entries(
|
||||||
|
getCommandArgumentKclValuesOnly(event.data)
|
||||||
|
)) {
|
||||||
|
resolvedArgs[argName] =
|
||||||
|
typeof argValue === 'function' ? argValue(context) : argValue
|
||||||
|
}
|
||||||
|
selectedCommand?.onSubmit(resolvedArgs)
|
||||||
} else {
|
} else {
|
||||||
selectedCommand?.onSubmit()
|
selectedCommand?.onSubmit()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Set current argument to first non-skippable': assign({
|
'Set current argument to first non-skippable': assign({
|
||||||
currentArgument: (context) => {
|
currentArgument: (context, event) => {
|
||||||
const { selectedCommand } = context
|
const { selectedCommand } = context
|
||||||
if (!(selectedCommand && selectedCommand.args)) return undefined
|
if (!(selectedCommand && selectedCommand.args)) return undefined
|
||||||
|
const rejectedArg = 'data' in event && event.data.arg
|
||||||
|
|
||||||
// Find the first argument that is not to be skipped:
|
// Find the first argument that is not to be skipped:
|
||||||
// that is, the first argument that is not already in the argumentsToSubmit
|
// that is, the first argument that is not already in the argumentsToSubmit
|
||||||
// or that is not undefined, or that is not marked as "skippable".
|
// or that is not undefined, or that is not marked as "skippable".
|
||||||
// TODO validate the type of the existing arguments
|
// TODO validate the type of the existing arguments
|
||||||
let argIndex = 0
|
let argIndex = 0
|
||||||
|
|
||||||
while (argIndex < Object.keys(selectedCommand.args).length) {
|
while (argIndex < Object.keys(selectedCommand.args).length) {
|
||||||
const argName = Object.keys(selectedCommand.args)[argIndex]
|
const [argName, argConfig] = Object.entries(selectedCommand.args)[
|
||||||
|
argIndex
|
||||||
|
]
|
||||||
|
const argIsRequired =
|
||||||
|
typeof argConfig.required === 'function'
|
||||||
|
? argConfig.required(context)
|
||||||
|
: argConfig.required
|
||||||
const mustNotSkipArg =
|
const mustNotSkipArg =
|
||||||
!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
argIsRequired &&
|
||||||
context.argumentsToSubmit[argName] === undefined ||
|
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
||||||
!selectedCommand.args[argName].skip
|
context.argumentsToSubmit[argName] === undefined ||
|
||||||
if (mustNotSkipArg) {
|
(rejectedArg && rejectedArg.name === argName))
|
||||||
|
|
||||||
|
if (mustNotSkipArg === true) {
|
||||||
return {
|
return {
|
||||||
...selectedCommand.args[argName],
|
...selectedCommand.args[argName],
|
||||||
name: argName,
|
name: argName,
|
||||||
@ -308,14 +332,10 @@ export const commandBarMachine = createMachine(
|
|||||||
}
|
}
|
||||||
argIndex++
|
argIndex++
|
||||||
}
|
}
|
||||||
// Just show the last argument if all are skippable
|
|
||||||
// TODO: use an XState service to continue onto review step
|
// TODO: use an XState service to continue onto review step
|
||||||
// if all arguments are skippable and contain values.
|
// if all arguments are skippable and contain values.
|
||||||
const argName = Object.keys(selectedCommand.args)[argIndex - 1]
|
return undefined
|
||||||
return {
|
|
||||||
...selectedCommand.args[argName],
|
|
||||||
name: argName,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'Clear current argument': assign({
|
'Clear current argument': assign({
|
||||||
@ -333,8 +353,6 @@ export const commandBarMachine = createMachine(
|
|||||||
'Set current argument': assign({
|
'Set current argument': assign({
|
||||||
currentArgument: (context, event) => {
|
currentArgument: (context, event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'error.platform.validateArguments':
|
|
||||||
return event.data.arg
|
|
||||||
case 'Edit argument':
|
case 'Edit argument':
|
||||||
return event.data.arg
|
return event.data.arg
|
||||||
default:
|
default:
|
||||||
@ -343,27 +361,22 @@ export const commandBarMachine = createMachine(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'Remove current argument and set a new one': assign({
|
'Remove current argument and set a new one': assign({
|
||||||
currentArgument: (context, event) => {
|
|
||||||
if (event.type !== 'Change current argument')
|
|
||||||
return context.currentArgument
|
|
||||||
return Object.values(event.data)[0]
|
|
||||||
},
|
|
||||||
argumentsToSubmit: (context, event) => {
|
argumentsToSubmit: (context, event) => {
|
||||||
if (
|
if (
|
||||||
event.type !== 'Change current argument' ||
|
event.type !== 'Change current argument' ||
|
||||||
!context.currentArgument
|
!context.currentArgument
|
||||||
)
|
)
|
||||||
return context.argumentsToSubmit
|
return context.argumentsToSubmit
|
||||||
const { name, required } = context.currentArgument
|
const { name } = context.currentArgument
|
||||||
if (required)
|
|
||||||
return {
|
|
||||||
[name]: undefined,
|
|
||||||
...context.argumentsToSubmit,
|
|
||||||
}
|
|
||||||
|
|
||||||
const { [name]: _, ...rest } = context.argumentsToSubmit
|
const { [name]: _, ...rest } = context.argumentsToSubmit
|
||||||
return rest
|
return rest
|
||||||
},
|
},
|
||||||
|
currentArgument: (context, event) => {
|
||||||
|
if (event.type !== 'Change current argument')
|
||||||
|
return context.currentArgument
|
||||||
|
return Object.values(event.data)[0]
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
'Clear argument data': assign({
|
'Clear argument data': assign({
|
||||||
selectedCommand: undefined,
|
selectedCommand: undefined,
|
||||||
@ -388,11 +401,6 @@ export const commandBarMachine = createMachine(
|
|||||||
}),
|
}),
|
||||||
'Initialize arguments to submit': assign({
|
'Initialize arguments to submit': assign({
|
||||||
argumentsToSubmit: (c, e) => {
|
argumentsToSubmit: (c, e) => {
|
||||||
if (
|
|
||||||
e.type !== 'Select command' &&
|
|
||||||
e.type !== 'Find and select command'
|
|
||||||
)
|
|
||||||
return c.argumentsToSubmit
|
|
||||||
const command =
|
const command =
|
||||||
'command' in e.data ? e.data.command : c.selectedCommand!
|
'command' in e.data ? e.data.command : c.selectedCommand!
|
||||||
if (!command.args) return {}
|
if (!command.args) return {}
|
||||||
@ -421,38 +429,67 @@ export const commandBarMachine = createMachine(
|
|||||||
},
|
},
|
||||||
'Validate all arguments': (context, _) => {
|
'Validate all arguments': (context, _) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
for (const [argName, arg] of Object.entries(
|
for (const [argName, argConfig] of Object.entries(
|
||||||
context.argumentsToSubmit
|
context.selectedCommand!.args!
|
||||||
)) {
|
)) {
|
||||||
let argConfig = context.selectedCommand!.args![argName]
|
let arg = context.argumentsToSubmit[argName]
|
||||||
|
let argValue = typeof arg === 'function' ? arg(context) : arg
|
||||||
|
|
||||||
if (
|
try {
|
||||||
('defaultValue' in argConfig &&
|
const isRequired =
|
||||||
argConfig.defaultValue &&
|
typeof argConfig.required === 'function'
|
||||||
typeof arg !== typeof argConfig.defaultValue &&
|
? argConfig.required(context)
|
||||||
argConfig.inputType !== 'kcl') ||
|
: argConfig.required
|
||||||
(argConfig.inputType === 'kcl' &&
|
|
||||||
!(arg as Partial<KclCommandValue>).valueAst) ||
|
|
||||||
('options' in argConfig &&
|
|
||||||
typeof arg !== typeof argConfig.options[0].value)
|
|
||||||
) {
|
|
||||||
return reject({
|
|
||||||
message: 'Argument payload is of the wrong type',
|
|
||||||
arg: {
|
|
||||||
...argConfig,
|
|
||||||
name: argName,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!arg && argConfig.required) {
|
const resolvedDefaultValue =
|
||||||
return reject({
|
'defaultValue' in argConfig
|
||||||
message: 'Argument payload is falsy but is required',
|
? typeof argConfig.defaultValue === 'function'
|
||||||
arg: {
|
? argConfig.defaultValue(context)
|
||||||
...argConfig,
|
: argConfig.defaultValue
|
||||||
name: argName,
|
: undefined
|
||||||
},
|
|
||||||
})
|
const hasMismatchedDefaultValueType =
|
||||||
|
isRequired &&
|
||||||
|
typeof argValue !== typeof resolvedDefaultValue &&
|
||||||
|
!(argConfig.inputType === 'kcl' || argConfig.skip)
|
||||||
|
const hasInvalidKclValue =
|
||||||
|
argConfig.inputType === 'kcl' &&
|
||||||
|
!(argValue as Partial<KclCommandValue> | undefined)?.valueAst
|
||||||
|
const hasInvalidOptionsValue =
|
||||||
|
isRequired &&
|
||||||
|
'options' in argConfig &&
|
||||||
|
!(
|
||||||
|
typeof argConfig.options === 'function'
|
||||||
|
? argConfig.options(context)
|
||||||
|
: argConfig.options
|
||||||
|
).some((o) => o.value === argValue)
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasMismatchedDefaultValueType ||
|
||||||
|
hasInvalidKclValue ||
|
||||||
|
hasInvalidOptionsValue
|
||||||
|
) {
|
||||||
|
return reject({
|
||||||
|
message: 'Argument payload is of the wrong type',
|
||||||
|
arg: {
|
||||||
|
...argConfig,
|
||||||
|
name: argName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!argValue && isRequired) {
|
||||||
|
return reject({
|
||||||
|
message: 'Argument payload is falsy but is required',
|
||||||
|
arg: {
|
||||||
|
...argConfig,
|
||||||
|
name: argName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error validating argument', context, e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Constrain parallel' }
|
| { type: 'Constrain parallel' }
|
||||||
| { type: 'Constrain remove constraints' }
|
| { type: 'Constrain remove constraints' }
|
||||||
| { type: 'Re-execute' }
|
| { type: 'Re-execute' }
|
||||||
|
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
||||||
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
||||||
| { type: 'Equip Line tool' }
|
| { type: 'Equip Line tool' }
|
||||||
| { type: 'Equip tangential arc to' }
|
| { type: 'Equip tangential arc to' }
|
||||||
@ -119,7 +120,7 @@ export type MoveDesc = { line: number; snippet: string }
|
|||||||
|
|
||||||
export const modelingMachine = createMachine(
|
export const modelingMachine = createMachine(
|
||||||
{
|
{
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaF07bCNLiRYlGiZUyZDbFYQzfIaLSQ5Sw0TGZQnM6eNodLoEW73J6wV7sD5UQyRZisMbcP4IQRrGiSdRmfIqUxrCrQlbKcoQ4RHDTWUx5DHNLGXXHXPjsB4AVwwX0psXGCSEohWO3Uh2UOSsmmhhn2jPyNGUolMhjWwvZoo8rUk3mJH2IZEomEdb0+9BGVN+qoQhoUPOMwgRi1BplkBqOZUDCkN4ksClEClt5zaHpJ7wdTveAEkrj0AkEugNwt7vr6VaBEoZQ2UKsIOcmrPMDRCLIKaOa6oKU6I0+LMx8c56C7jkCRXn0oABbMB3fwAN0enHIJEwiuiVZp-qsFskRxkmibmlSBX0Rg0hikifBcgk+SUGkH9uH2ff4+6k+nQTnC4Cd5UAebAAC9uHYDctx+at+CMeFJFUINu0NfJkRkA1xENcoNHkSwVmMCoZFfC531HLMv2IbhYBlEg8H8ICQPAu4N38CBsBo10wGgnd4hreDRA0Ts0hoKp0gtdRoTSdkLAqURwVwi0bxIjNc3Ij5KKIajaPolcHjXVj2M4ihuIrJVqT4uCA2WBRJGFY9UR1YxNAwy8EBkVlymsAVYSBM0X1cU4xTfNTP0LLTcBoh46NwfwAEEACFvH8AANHjlV3fjrOTWZxDEQwbCwmRbHEKThSE4VkUWVJzVqBpAsxELPXU-Nwu06L6MS5KAE10os2k9W2aw7GUOR9jNUboUTdRJCBOTLTUUMAqaO1SNC3NNPamL-DIKAuj6v0spvHJpCUapk0UOtTCk+S4X7Xz1WEUxTGEFSWpazbIp02KunwdgvQpbcMss2sShmFR1VEm80n2KTDlsptTGvJ6wxUV6GuCtbmrC3EIqi7amEeQncHY8hZUwEgniMyCTIO2Daw82ybEjKwMiOVRlCk+MhP5KoXqBBENDEN6yJx7o8e+hjgLAiCN0wPQdpwKAhjMoH+r3ZYZCZFMtAkXCMhWA1HMkYqddhfJ1RKEX1rHNqvo62K9IMzB5cV7BlbpzKrKsJ7cuPJ7HLMVEDVG7YUxeqo1mKxx6pW9N3rFqj7e22BcBIJh-HYVBUs9kGjCwqRlhBNQXuqMwpOyLWPPVeZVBTIFiIx1bVOxja7fx+jU-TzPs961WYK90GbwsJssNhPLjA0KSCpmQTHDkC1Qx1WOgubhO29xrb6LAABHWVWN+qB-tzgbiqkVEhaerI8gkC8ijrRNcpMcFuwm0xrdb23N+T+imEpuXqD914gNWQMwlgER7MjKeblDCRjAaUQ4z1MhqAHE3eOosN7iy3rFB4YBZyoBXP4cg2D2CwBPhrLQiFWbqmPNeFMpUYFKDKPhWw2QwRBkbnHIcNsKKFgAEpgEEGAPgIRZT3HIUdOoUgbywMTGkIWFRDDQnUFrdIqRkS1yUIcD+WYPqFmuHvbAGcAAyeAwA91QJuIBwMBoKBkhIFQzM9aSTckoMOmRhpyDmKiQwOiRyJwMbKIxmddoAWwKxSm5Ae4SKsiNWyIJLaWnkG4hh99jBlFPHXRQ6Q5jLVXugtScUADudFALS2YpBTAbEOI00oP4PAAAzVABAIDcDAO0XAS5UCvEkDAdgghGIyxYpgQQjTUAxMSCJISCIdSiVqM9ceUlrzmGTMeGGyhbCPT8dmYppSpZMVllU6mXF6m4CaQQR4DxgKSCYBTdgTSHizl6X4AZ5TDmjLOeM6x6ssoiW2IaBECZcLHhelJEEWtsimnBHUNCvi0HcOarsjgy5VzYHXEcmpJyxktLaR0rpPS+mCCdmijcHymkTMQDDLWN5LomGRLA-IUljxSEcDfOwpQgTbMkEigIxL0XVOMnU7Flzrm3JIPc4CTzCV8tJWMil7kipMm7GIReOsmWCkPBNVIIJzpKC5Ty+KSVUqnPOa03A7S8D4vaYSkgAAjWAgg+Bkq+YDAeecFWWlmqNe8cg6jrDcsVUah4MjXmXprWo+qSnIq6sa4VDwrkPBuXch5UqXl2odU6uV3zDqxLHrlHVdRIRzzKp6v1ZosLZFDO-eFTVdEGpjd1E1zSzUWs6d061ab7WCD0M6+V6zpCNgqLYOBuEpqmhmDYOx8l6HdjyY1LGdao0BAbU2i58bRXJslc8-p6bu29uzfTSlebzwrD8isUNU1LRCVNOyUaY08qcPyQixdezdpdFXS2vF7bt2CDfYIrNrrgH+hhuDSwN4b7PWDgGioPJIzdjAxUIMsJI2vvwO+uNCak3ipTT+v9+7AM2OA1hBG2Q9RWBLnWG6dZDzgayHIbsyx0ZcNrSOA1h9-oftxZa79hL2PvHwz6QjvysLbByNJKBMhzxwxyqaDlKg6xWBQ8ivjq6RWJrFRKx5P6+MCcrEJ3NnrjAmBTJaE8rkijLA1fMawdiFEonRDWhdrGl3+EJg8YmpNyaUwFbU8x2LP3cYJS8tzHm0VeYeIIY5JldPmRzZMzWdl6HWDvVHUQXM5DSH5HMM0kHfVKYCCFhcnmKZU0xSZVT671Obq04SwrJMwslci2VygMW1ZxcpQl4UShkseVS1JXymWCrzHNPZCQXLTHmosZgIsfRSxxD7ZJ8w15JNHEyEcPIqTKUvUPF10SNmkKPvnS3XRE3zFZ0sXiQxGcaYwDuOEqpkTokHsHpS2EQl9hmHBOoPmnM3L83zYoWEokHPMac9mU7U3JB5lwBwAgC2VBMgSatpGC8uTxhNiCSMkI8pPTSONsxkPoew7JARn5sT8ha3kseK95GNnQlhEtlYEhNkbPBKg0Hx2RwQ-O5gSQuBJUbhmyWEI83nvuusLUaQx54wcIyDXLkQtZrXgUdza8WF8eTZ55IAActnAACqgPApCCBxQgBAQIkF9KucN3cPt8wKqv3NIKRYOouQbNyhHLIigzoa7O6gSxOv9c2+N6QEyVjSftYDJocwJg0jVFErrFxRQshlBsIg0ox4VhcqJ+wOHYuBr4VmBdOlDGE8GmBLMDRywoWWbhRz96Oe4fkkE2T2sKEmSQkhHMWQ4ICrl4PIoPWhxLORhB0+lj2YAAqoS7sRIeFErOQv+ii4j4e9yo1zAbKUMKRMthirl+vEyK9knLC4RTFy6f+Awlz4X80wJwTuf+-Dy3yPlnpmJkH8KU05nEBIx5ObNIPCU0OYJjcfMHSQWUEmbOTifSd8AAeVwBxXNS-R6Tim8En0EEgNaUEBgPYHgJVlXxewDFkCkDmBMFwlEkyEWDvkQHNFshbEQxpScW2X8H538AaRIEoB6DmzYjAA4PJgKwpnNXlUEAKnMASWM0NFDAkFgS5AyyRnZCG0sAfFAKO0kDIGwFnHFVaB7lcyEO6ACzbR6Q0K0PuEECzkEA4MoHlUUUPGoMWAyCWChDclGniSsDBBBVDHUDehMO0PwF0LFXNSXzm0GD7SBBozkAXiQlDGhDnkPFWDsHBBWBqB8Jh1MJ0Kzn8GERqR0KJE9CQNbStXULSPFUEQsLyKzBsLNCoVZGeg8jSFhGhDoQHWPGALsDmFoRcECn5wwHgCiCOxfzX0EEWDKFyGySQk0B0DckEEEiVSgX2DyFGktDehxDAEGKIPpEoUehehBFwhPGUXRwkDUA+3cMsGrXr3fHWPdXZCZmLxUFLyRimi1g2UqEDmcnZzAM5w-EwSuJAWehNh1HjDymqF2GjGSBvDylSGqAtBhPy32SGUqR8yxU+V+L3CDCZgyFLn5BqFSDBWSABDvHmFsH5DhJlQxUFT8xRL01byMBUU1V7ykQ3yUQDXowsG8m5mG1RDhJjRSibVRKOjsSkDUCKlH0SNEjKiwh2CqlUAWhTEO0xi+O5RcxXTGX5O9jsUp1oWI0rjHWqOrkHSqGhLhL-T5OpMjyOGFEQjvByAcDyEjBujUCZDjxg0yFGjqDhJU1VLNLXwtMZAyFWBM1HzbADW5FmhhMcGRCRktjhLq2K28yiyFSpNix9OTH-3ZHkjPDTxKH63R15mTAtEbBtEc0VMf0sTVMmXsBHhKGTEnXRNiNUBNmVxWBl1wmRF90Jxh3YHLMpX2GYTqhrMNLWC5HBPmByRWC7w2XbK1350eQ3G7PchsAnXHmKjUFjx-wQBT1mEUMjCKiDFkCnKf0D38ANyNz6OTKIOsAkBNiYS0HsDSENjcibCrnjATCOCbFvOz07PnMIk331msBMG3z7xgRjBo3UATCTHP2LPekv1u04Bvx7nnLkE0Fmj8hUAciemuhgQYwBKDA5GvisA+LULIiwOgMtzwNzAQO-KtHKF7FTL9VkEwqKEEh5DmAZTPyqnVBYLYKsLWO9I2L7BouWVUDqCekmhcIy2vHsGnS0FkFTCgt8M4H8MyMCN4vPPdUtCkBelZxsHkFpzkJ5AAojLnmmlUIVOKM0L8KgF0OyJolyNzG-McBNlhGQjynZHZSmibF5HsGoKFmKgKi6KcCAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaF07bCNLiRYlGiZUyZDbFYQzfIaLSQ5Sw0TGZQnM6eNodLoEW73J6wV7sD5UQyRZisMbcP4IQRrGiSdRmfIqUxrCrQlbKcoQ4RHDTWUx5DHNLGXXHXPjsB4AVwwX0psXGCSEFThkNEymFGlECkM0MM+0ZMgUa1MwuypgRhlFHlaEpufBYD3YiuiVN+qrpdg0klMwkFaU0ermwuhwlUkiBhxUyKUazt5za3mJH2IZEomFTb0+9BGnpVoESRrNkmMga0ahMplkhqOZVL+sM4ksCj1SfFOZJ70k3Y+AEkrj0AkEugNwvnvoWad7DIGyuqOe2rPNDRCLIKaKJa6JBXrRJ2Hf3eyeh7jkCRXn0oABbMB3fwAN0enHIJEw7p+Rf4RhkpkbapNEjTRUgKfQjA0FsUnBOQJHyJQNCPC4Tz7NN3nPbpL2vII7wfAJ3lQB5sAAL24dgPy-Gd4mLP9A0kVQzW3I18mRGRDXEI1yl1LJDBWYwKhkZCU3QtDc0w4huFgGUSDwfxCOIsi7g-fwIGwaTMzAKjlVnWiECsPdNzSGgqnSAD1GhNJ2QsCpRHBXUAJbYSxJ7FzB2HIgpJkuSX1dbB30wVT1IoigtKnJVqRo399OWBR-TyFdlCgtRBUs1lymsAVYRjPdnNQs8PK8h5ZNwfwAEEACFvH8AANbTItpKx21mcQxEMGxOP-NJLJ1eL41gndagaVxTjFY9RIK3FPNwaTirkyrqoATXqr09Ka7ZrDsZQ5H2ZQtXYiDijUOKgVsvi1ErPKJvQiTptmkr-DIKAuhWn8S3SKQQRBU15H1YRTEsuy4QPbKtX+gMrtzNyMMKmbvNKrp8HYPMKQ9HSove1rpD21QANkTjREsw4Tu1KD-oRBNhEh1zJu6O74f8JhHiZ3A1PIWVMBIJ41I00LXt06KrFNWYLU6jIjlUZRLP1P1+SqAMgQRDQxGpj5oduoqHoU0jyI-TA9EenAoCGcK0YaudlhkJlQ3bAEMhWQ1UR5f9Q1hfItRKVXTxu2H7p819-L1g2P2wY3+YxujzByU1-qdsxUUNbbtj1AMqjWf9HGGpp7RQ67xN9hnYFwEgmH8dhUFq8PGs4qRlhBNQA2qMxLOyK2ZDkeZ5m3Rx2699WC7m0qi5LsuK+W03vwF97oMyfjYVa4wNEs9qZj3Hud3nOYcj72nJLhwf-DAABHWUVMRqBkari3a3KFf-qyPIJHAop51bFqTHBbccdMHefamzW5JMC5nragE9qLV3yBYdu-EdzpE3oaWsMwTCcUOBaTIahDwjUxONKGu96YHweGAW8qAXz+HIAAu4sAr5rWsHFQ41h9qaH2DoQ6RxOIWD4rYbIYIzRCSwWNXOuC-7dAAEpgEEGAPgIRZT3GoYLOoUgWyGFrKkdIYgE6HXUFbdIqRkSdwTLafhOcRJCPzpKE+2BS4ABk8BgFHqgT8YD0aNQUNZCQKgbDCnSBZTRdReSK3bnuPaRpf5mJuBY0uIUYB3GwCpLm5BR5yMSFtOKIIPZ8V+go+slhoxrFUJkhESU+5lQAO6yQIkRHWylAo8xCpQfweAABmqACAQG4GAdouAnyoFeJIGA7BBDayUhRTAggmmoCSYgYyfpCkZADGYBW4hLJQXMO2U0aROEdypkY5M0NJClPKfJSpwyVK1M0g03AzSCCPAeERSQTBObsGaQ8W8fS-CDOObrUZ4zJkIGMtsI0NoWy6lNAGSy31Nw0FUCYPIkZDHZ12ahA5HBnwBwCkFXm9TxmtPaZ07pvT+mCF8m+D8YzLkTKcebPSGyrYtkUPYZEyj8hpSUEyMQcg7ClCBMUspKLiWBxqcFc52Kbl3IeSQJ5RFXmEv5QFMlzTfk0qZNuMQAExCaDSoKcsONVFaOZDyw5C1aoXKuW03AHS8D4o6YSkgAAjWAgg+DyopajSeEc-koNyTIWCcg6jrEOv+ba5YMhQSSrIbatQDUoqNTVE1LTRUPHuY8550r3l2odU6n5lLVrRQ2TMHIIIlA7m3FoHqfFpB6j2pxbIgYf47K7KJZFAQjWLTjTi81eKenWrTfawQehnWKs9XUeQFRbCIN1NCVsyIRauLsnqVsuV604Nck28qVV-CtpFQ8W5ibxWSpeW8gZ6a+0DuzW9KZnEkGsVassFYobJ18T9FC9k20do3qjQEJ6XQ21motV0rth7BBfvEVm114DvR5pSEaOocgLQaKKHBHkKi7JGgqGaWEH7Hr4G-VundSaJUpsA8B09YHnEQc4idbITUrAN3nIDec5YWzyFSHO5Y2yEUNqhqu8+yMf24stQBwlPH3gkYLGR6lBMWpWTJluZlAaOQWBWJelQ84rCYeE22hN+H92poGcJ0T05xO5s9cYGF+o1jKwOghpKVt5i0OtGIFE6Il2CJXbygITMHgszZhzLmGK6l2Oxb+ztBL3mee8-5XzDxBBnNCgZiKObkmW3ikoawr706EwDdUbYC85h7Tg76zD4WHw+c5tzIVoVNPbrFcmqVgHius0i2VmLFXKDxbNolqZyXhSpbmNA7LllsrSH5HlncwpCsuZMa5Gx5r7GYBHH0cccRFXeqjjRo4mQjh5CWQGgM5YesmVoYxPhHHl1qxm3Y8uDi8QRLLk9fCsTArxMSWeqeUzYR+n2GYcE6h5ZS0OgrFqrVHBZHZH3C7c3JADlwBwAgK2VBMjSRt60sGuT6kkMGWskJWr-TSOD2xkPoew7JKRqlub8hWzsqaR9NHtQRn+rMFYEhbBakhNufHs2ruYEkLgKVH4FtjhCMt177rrC1GkKafUvCMhan+0UJKMwFzKz1O1KCnEOeXdQA4yQAA5CuAAFVAeB2CwAIGVCAEBAgUVdIzI3dxFXzD9JqfLgpFhJS5NqFqqcsiKCUAoDXkO9f+EN8b03pBQqONJ51-SmhzAmGDKtrQep-VFF4iLVBpRTQrD7kT9gcOReNUsDyJPWQVDbkT4aYEjPIzLChboqCOeYd55J2JsnJZmJMkhJCOYshwTtUrwBaMts5irg6s507rm1YABV7sxLiQ8BJ5cBf9GF1H89fztrmG1EoLxh3-yV6gkyR93rLC6g7JNvZM-8APfn4vlp1xbsQ65w7hE79FA7jsttaE1oeRuzSMxqFOYdjUaYxPZWUVmCuDSV0E8AAeVwHbT-StX2W8Cn0EHALaUECgPYFgJNjXze30nxkZxMF1BMkyEWGfkQB3DihXDQzpU8UwQnymw+H8F538EaRIEoB6CW1UjAHYI5g805nNV+UEHanMDSRhSNEDAkGUS5DkH9BsEODmEsDgmAOwQuDIGwFvAlVaFHkZkEO6GCwE16Q0K0PuEEHLkEHYMoF+QqEAjIOyB9ShEOm2lSSsDBFBUDHUGchMO0PwF0PFXNWXyW0GEVSBEYw5R3EYkDGhDXnLFWDsHBBWBqG8Jh1MJ0PLkPj4GCh0KJFzAQJCw6R8LMIsNyJ7BsL2gYisFqAtHbjSFhGhCghywSkAN9BcKQhOF5wwHgCiDUKgFb2j0EEWDKFyEUDL3UC0B2yKEGJNC1EsFqCFGMDNGchxDAH6PX3pC0HKGRHmRDGAmhHUEXFbH1GkOMmUS9jWPwPZDihLxMHyQr0OiQQWGtEGlsHmAwwv3ymEQuPdRbAtAxySn1GB19UyxfiZTiNalSGqAAmhMwyGS+X82FXJW+JcTWFmDmUUH5BqFSHBWSABFbChW1HyHhRAMRUbXc1RT8nRViyxSRMMzbyMC0W1T7wUU3wNADTkEZFSCghlnmFsMwxjTjWRLnFcSkDUE6lrG2lMwBl23YV1HjFUHOj1BOxJM4zc0NXXU3VpIS3XyOGSAShyA21SkOinQ2jghHSqChMw2A0FLpOj11O2BWHshKCqDyFrEBjUCZGDAqFUBrzqHUwfAvneBtO1PwN1MZAyFWBVwlLXHkwR3+mUUcG2LlKzhVLO17FXQa1Kz82pMCy1I6x1PbF-3ZDslAgUJKEG3RzlnbAAnVDBw+NEify10wCFOpXsAsCGnbBsHUDWBiKjG5MEmQVtnH1TMn17EbO11zxbNzX2DKEyBKE7ItJ7OcOSD4jvR72721ADy5x5z52bNtPXyFHzXnn-GrFkCs0QDTwUOUUzzNFkC3KbN1wNztxNynOSV1FriUHan1HjxUEmIvKDWp2gyOEjC0GJN6L2UnP3NDO3C311EFAjJ3371YQbEYwOJbDbHP0YMv1n04Fv1HlfKmVDGjBjBUB+nBnXDyH+LNA5AfgMj7nQMgOt2wPQjgIIv0nNHKDqCTyZ1S2lKKCCUZyZTP3jC1C9hYIrisNWKgvdSmDkIkBWVUDqH+i-2cLkKgnsDnQmNNBSM0N8KgH8P0LYr4ikADG1CtHkFp1kJ5BMHUEjDXiONUIETaCKPSIrkkWyL8NKI+CMscAx1hCYlanZE5UnUjF5HsHIMswAnaKwpcr8IyICLsQAApyEmA9AABKNikQCQCXTJRiTQFhIoHhRTF9QJOlZU8C2K-S+K-Q-wZK1AVKtK2qlK9KzKsQT6OQUYvKiYydBESnHcawA0muYUFwFwIAA */
|
||||||
id: 'Modeling',
|
id: 'Modeling',
|
||||||
|
|
||||||
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
||||||
@ -170,6 +171,13 @@ export const modelingMachine = createMachine(
|
|||||||
actions: ['AST extrude'],
|
actions: ['AST extrude'],
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Export: {
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
cond: 'Has exportable geometry',
|
||||||
|
actions: 'Engine export',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
entry: 'reset client scene mouse handlers',
|
entry: 'reset client scene mouse handlers',
|
||||||
@ -481,6 +489,7 @@ export const modelingMachine = createMachine(
|
|||||||
'animate after sketch',
|
'animate after sketch',
|
||||||
'tear down client sketch',
|
'tear down client sketch',
|
||||||
'remove sketch grid',
|
'remove sketch grid',
|
||||||
|
'engineToClient cam sync direction',
|
||||||
],
|
],
|
||||||
|
|
||||||
entry: ['add axis n grid', 'conditionally equip line tool'],
|
entry: ['add axis n grid', 'conditionally equip line tool'],
|
||||||
@ -514,6 +523,8 @@ export const modelingMachine = createMachine(
|
|||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
entry: 'clientToEngine cam sync direction',
|
||||||
},
|
},
|
||||||
|
|
||||||
'animating to existing sketch': {
|
'animating to existing sketch': {
|
||||||
@ -524,7 +535,12 @@ export const modelingMachine = createMachine(
|
|||||||
onDone: 'Sketch',
|
onDone: 'Sketch',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
entry: 'clientToEngine cam sync direction',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'animating to plane (copy)': {},
|
||||||
|
'animating to plane (copy) (copy)': {},
|
||||||
},
|
},
|
||||||
|
|
||||||
initial: 'idle',
|
initial: 'idle',
|
||||||
@ -824,13 +840,13 @@ export const modelingMachine = createMachine(
|
|||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
if (!args) return
|
if (!args) return
|
||||||
if (args.event.which !== 1) return
|
if (args.mouseEvent.which !== 1) return
|
||||||
const { intersection2d } = args
|
const { intersectionPoint } = args
|
||||||
if (!intersection2d || !sketchPathToNode) return
|
if (!intersectionPoint?.twoD || !sketchPathToNode) return
|
||||||
const { modifiedAst } = addStartProfileAt(
|
const { modifiedAst } = addStartProfileAt(
|
||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
[intersection2d.x, intersection2d.y]
|
[intersectionPoint.twoD.x, intersectionPoint.twoD.y]
|
||||||
)
|
)
|
||||||
await kclManager.updateAst(modifiedAst, false)
|
await kclManager.updateAst(modifiedAst, false)
|
||||||
sceneEntitiesManager.removeIntersectionPlane()
|
sceneEntitiesManager.removeIntersectionPlane()
|
||||||
@ -845,6 +861,12 @@ export const modelingMachine = createMachine(
|
|||||||
// (note the orbit controls are always active though)
|
// (note the orbit controls are always active though)
|
||||||
sceneInfra.resetMouseListeners()
|
sceneInfra.resetMouseListeners()
|
||||||
},
|
},
|
||||||
|
'clientToEngine cam sync direction': () => {
|
||||||
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
|
},
|
||||||
|
'engineToClient cam sync direction': () => {
|
||||||
|
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// end actions
|
// end actions
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const [state, send] = useMachine(homeMachine, {
|
const [state, send, actor] = useMachine(homeMachine, {
|
||||||
context: {
|
context: {
|
||||||
projects: loadedProjects,
|
projects: loadedProjects,
|
||||||
defaultProjectName,
|
defaultProjectName,
|
||||||
@ -176,6 +176,7 @@ const Home = () => {
|
|||||||
send,
|
send,
|
||||||
state,
|
state,
|
||||||
commandBarConfig: homeCommandBarConfig,
|
commandBarConfig: homeCommandBarConfig,
|
||||||
|
actor,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -21,7 +21,7 @@ export default function Export() {
|
|||||||
<section className="flex-1">
|
<section className="flex-1">
|
||||||
<h2 className="text-2xl font-bold">Export</h2>
|
<h2 className="text-2xl font-bold">Export</h2>
|
||||||
<p className="my-4">
|
<p className="my-4">
|
||||||
Try opening the project menu and clicking "Export Model".
|
Try opening the project menu and clicking "Export Part".
|
||||||
</p>
|
</p>
|
||||||
<p className="my-4">
|
<p className="my-4">
|
||||||
{APP_NAME} uses{' '}
|
{APP_NAME} uses{' '}
|
||||||
|
21
src/wasm-lib/Cargo.lock
generated
@ -1990,11 +1990,12 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-execution-plan"
|
name = "kittycad-execution-plan"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
|
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"insta",
|
"insta",
|
||||||
"kittycad",
|
"kittycad",
|
||||||
|
"kittycad-execution-plan-macros",
|
||||||
"kittycad-execution-plan-traits",
|
"kittycad-execution-plan-traits",
|
||||||
"kittycad-modeling-cmds",
|
"kittycad-modeling-cmds",
|
||||||
"kittycad-modeling-session",
|
"kittycad-modeling-session",
|
||||||
@ -2008,8 +2009,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-execution-plan-macros"
|
name = "kittycad-execution-plan-macros"
|
||||||
version = "0.1.6"
|
version = "0.1.8"
|
||||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
|
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -2018,8 +2019,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-execution-plan-traits"
|
name = "kittycad-execution-plan-traits"
|
||||||
version = "0.1.11"
|
version = "0.1.12"
|
||||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
|
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@ -2028,8 +2029,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-modeling-cmds"
|
name = "kittycad-modeling-cmds"
|
||||||
version = "0.1.26"
|
version = "0.1.28"
|
||||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
|
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -2056,8 +2057,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-modeling-cmds-macros"
|
name = "kittycad-modeling-cmds-macros"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
|
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -2067,7 +2068,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-modeling-session"
|
name = "kittycad-modeling-session"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#29086e1079adb82b6427639a779dc58eabcd7f78"
|
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"kittycad",
|
"kittycad",
|
||||||
|
@ -105,6 +105,10 @@ impl BindingScope {
|
|||||||
"startSketchAt".into(),
|
"startSketchAt".into(),
|
||||||
EpBinding::from(KclFunction::StartSketchAt(native_functions::sketch::StartSketchAt)),
|
EpBinding::from(KclFunction::StartSketchAt(native_functions::sketch::StartSketchAt)),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"lineTo".into(),
|
||||||
|
EpBinding::from(KclFunction::LineTo(native_functions::sketch::LineTo)),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
parent: None,
|
parent: None,
|
||||||
}
|
}
|
||||||
|
@ -252,6 +252,7 @@ impl Planner {
|
|||||||
} = match callee {
|
} = match callee {
|
||||||
KclFunction::Id(f) => f.call(&mut self.next_addr, args)?,
|
KclFunction::Id(f) => f.call(&mut self.next_addr, args)?,
|
||||||
KclFunction::StartSketchAt(f) => f.call(&mut self.next_addr, args)?,
|
KclFunction::StartSketchAt(f) => f.call(&mut self.next_addr, args)?,
|
||||||
|
KclFunction::LineTo(f) => f.call(&mut self.next_addr, args)?,
|
||||||
KclFunction::Add(f) => f.call(&mut self.next_addr, args)?,
|
KclFunction::Add(f) => f.call(&mut self.next_addr, args)?,
|
||||||
KclFunction::UserDefined(f) => {
|
KclFunction::UserDefined(f) => {
|
||||||
let UserDefinedFunction {
|
let UserDefinedFunction {
|
||||||
@ -619,6 +620,7 @@ impl Eq for UserDefinedFunction {}
|
|||||||
enum KclFunction {
|
enum KclFunction {
|
||||||
Id(native_functions::Id),
|
Id(native_functions::Id),
|
||||||
StartSketchAt(native_functions::sketch::StartSketchAt),
|
StartSketchAt(native_functions::sketch::StartSketchAt),
|
||||||
|
LineTo(native_functions::sketch::LineTo),
|
||||||
Add(native_functions::Add),
|
Add(native_functions::Add),
|
||||||
UserDefined(UserDefinedFunction),
|
UserDefined(UserDefinedFunction),
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,5 @@
|
|||||||
|
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub mod stdlib_functions;
|
pub mod stdlib_functions;
|
||||||
pub mod types;
|
|
||||||
|
|
||||||
pub use stdlib_functions::StartSketchAt;
|
pub use stdlib_functions::{LineTo, StartSketchAt};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use kittycad_execution_plan::{api_request::ApiRequest, Instruction};
|
use kittycad_execution_plan::{api_request::ApiRequest, Destination, Instruction};
|
||||||
use kittycad_execution_plan_traits::{Address, InMemory};
|
use kittycad_execution_plan_traits::{Address, InMemory};
|
||||||
use kittycad_modeling_cmds::{id::ModelingCmdId, ModelingCmdEndpoint};
|
use kittycad_modeling_cmds::{id::ModelingCmdId, ModelingCmdEndpoint};
|
||||||
|
|
||||||
@ -120,11 +120,13 @@ pub fn arg_point2d(
|
|||||||
instructions.extend([
|
instructions.extend([
|
||||||
Instruction::Copy {
|
Instruction::Copy {
|
||||||
source: single_binding(elements[0].clone(), "startSketchAt", "number", arg_number)?,
|
source: single_binding(elements[0].clone(), "startSketchAt", "number", arg_number)?,
|
||||||
destination: start_x,
|
destination: Destination::Address(start_x),
|
||||||
|
length: 1,
|
||||||
},
|
},
|
||||||
Instruction::Copy {
|
Instruction::Copy {
|
||||||
source: single_binding(elements[1].clone(), "startSketchAt", "number", arg_number)?,
|
source: single_binding(elements[1].clone(), "startSketchAt", "number", arg_number)?,
|
||||||
destination: start_y,
|
destination: Destination::Address(start_y),
|
||||||
|
length: 1,
|
||||||
},
|
},
|
||||||
Instruction::SetPrimitive {
|
Instruction::SetPrimitive {
|
||||||
address: start_z,
|
address: start_z,
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
use kittycad_execution_plan::{api_request::ApiRequest, Instruction};
|
use kittycad_execution_plan::{
|
||||||
|
api_request::ApiRequest,
|
||||||
|
sketch_types::{self, Axes, BasePath, Plane, SketchGroup},
|
||||||
|
Destination, Instruction,
|
||||||
|
};
|
||||||
use kittycad_execution_plan_traits::{Address, InMemory, Value};
|
use kittycad_execution_plan_traits::{Address, InMemory, Value};
|
||||||
use kittycad_modeling_cmds::{
|
use kittycad_modeling_cmds::{
|
||||||
shared::{Point3d, Point4d},
|
shared::{Point3d, Point4d},
|
||||||
@ -6,12 +10,85 @@ use kittycad_modeling_cmds::{
|
|||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{
|
use super::helpers::{arg_point2d, no_arg_api_call, single_binding, stack_api_call};
|
||||||
helpers::{arg_point2d, no_arg_api_call, single_binding, stack_api_call},
|
|
||||||
types::{Axes, BasePath, Plane, SketchGroup},
|
|
||||||
};
|
|
||||||
use crate::{binding_scope::EpBinding, error::CompileError, native_functions::Callable, EvalPlan};
|
use crate::{binding_scope::EpBinding, error::CompileError, native_functions::Callable, EvalPlan};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||||
|
pub struct LineTo;
|
||||||
|
|
||||||
|
impl Callable for LineTo {
|
||||||
|
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||||
|
let mut instructions = Vec::new();
|
||||||
|
let fn_name = "lineTo";
|
||||||
|
// Get both required params.
|
||||||
|
let mut args_iter = args.into_iter();
|
||||||
|
let Some(to) = args_iter.next() else {
|
||||||
|
return Err(CompileError::NotEnoughArgs {
|
||||||
|
fn_name: fn_name.into(),
|
||||||
|
required: 2,
|
||||||
|
actual: 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let Some(sketch_group) = args_iter.next() else {
|
||||||
|
return Err(CompileError::NotEnoughArgs {
|
||||||
|
fn_name: fn_name.into(),
|
||||||
|
required: 2,
|
||||||
|
actual: 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// Check the type of both required params.
|
||||||
|
let to = arg_point2d(to, fn_name, &mut instructions, next_addr, 0)?;
|
||||||
|
let sg = single_binding(sketch_group, fn_name, "sketch group", 1)?;
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let start_of_line = next_addr.offset(1);
|
||||||
|
let length_of_3d_point = Point3d::<f64>::default().into_parts().len();
|
||||||
|
instructions.extend([
|
||||||
|
// Push the `to` 2D point onto the stack.
|
||||||
|
Instruction::Copy {
|
||||||
|
source: to,
|
||||||
|
length: 2,
|
||||||
|
destination: Destination::StackPush,
|
||||||
|
},
|
||||||
|
// Make it a 3D point.
|
||||||
|
Instruction::StackExtend { data: vec![0.0.into()] },
|
||||||
|
// Append the new path segment to memory.
|
||||||
|
// First comes its tag.
|
||||||
|
Instruction::SetPrimitive {
|
||||||
|
address: start_of_line,
|
||||||
|
value: "Line".to_owned().into(),
|
||||||
|
},
|
||||||
|
// Then its end
|
||||||
|
Instruction::StackPop {
|
||||||
|
destination: Some(start_of_line + 1),
|
||||||
|
},
|
||||||
|
// Then its `relative` field.
|
||||||
|
Instruction::SetPrimitive {
|
||||||
|
address: start_of_line + 1 + length_of_3d_point,
|
||||||
|
value: false.into(),
|
||||||
|
},
|
||||||
|
// Send the ExtendPath request
|
||||||
|
Instruction::ApiRequest(ApiRequest {
|
||||||
|
endpoint: ModelingCmdEndpoint::ExtendPath,
|
||||||
|
store_response: None,
|
||||||
|
arguments: vec![
|
||||||
|
// Path ID
|
||||||
|
InMemory::Address(sg + SketchGroup::path_id_offset()),
|
||||||
|
// Segment
|
||||||
|
InMemory::Address(start_of_line),
|
||||||
|
],
|
||||||
|
cmd_id: id.into(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO: Create a new SketchGroup from the old one + add the new path, then store it.
|
||||||
|
Ok(EvalPlan {
|
||||||
|
instructions,
|
||||||
|
binding: EpBinding::Single(Address::ZERO + 9999),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||||
pub struct StartSketchAt;
|
pub struct StartSketchAt;
|
||||||
@ -108,9 +185,9 @@ impl Callable for StartSketchAt {
|
|||||||
name: Default::default(),
|
name: Default::default(),
|
||||||
},
|
},
|
||||||
path_rest: Vec::new(),
|
path_rest: Vec::new(),
|
||||||
on: super::types::SketchSurface::Plane(Plane {
|
on: sketch_types::SketchSurface::Plane(Plane {
|
||||||
id: plane_id,
|
id: plane_id,
|
||||||
value: super::types::PlaneType::XY,
|
value: sketch_types::PlaneType::XY,
|
||||||
origin,
|
origin,
|
||||||
axes,
|
axes,
|
||||||
}),
|
}),
|
||||||
|
@ -1,133 +0,0 @@
|
|||||||
use kittycad_execution_plan::Instruction;
|
|
||||||
use kittycad_execution_plan_macros::ExecutionPlanValue;
|
|
||||||
use kittycad_execution_plan_traits::{Address, Value};
|
|
||||||
use kittycad_modeling_cmds::shared::{Point2d, Point3d, Point4d};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
/// A sketch group is a collection of paths.
|
|
||||||
#[derive(Clone, ExecutionPlanValue)]
|
|
||||||
pub struct SketchGroup {
|
|
||||||
/// The id of the sketch group.
|
|
||||||
pub id: Uuid,
|
|
||||||
/// What the sketch is on (can be a plane or a face).
|
|
||||||
pub on: SketchSurface,
|
|
||||||
/// The position of the sketch group.
|
|
||||||
pub position: Point3d,
|
|
||||||
/// The rotation of the sketch group base plane.
|
|
||||||
pub rotation: Point4d,
|
|
||||||
/// The X, Y and Z axes of this sketch's base plane, in 3D space.
|
|
||||||
pub axes: Axes,
|
|
||||||
/// The plane id or face id of the sketch group.
|
|
||||||
pub entity_id: Option<Uuid>,
|
|
||||||
/// The base path.
|
|
||||||
pub path_first: BasePath,
|
|
||||||
/// Paths after the first path, if any.
|
|
||||||
pub path_rest: Vec<Path>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SketchGroup {
|
|
||||||
pub fn set_base_path(&self, sketch_group: Address, start_point: Address, tag: Option<Address>) -> Vec<Instruction> {
|
|
||||||
let base_path_addr = sketch_group
|
|
||||||
+ self.id.into_parts().len()
|
|
||||||
+ self.on.into_parts().len()
|
|
||||||
+ self.position.into_parts().len()
|
|
||||||
+ self.rotation.into_parts().len()
|
|
||||||
+ self.axes.into_parts().len()
|
|
||||||
+ self.entity_id.into_parts().len()
|
|
||||||
+ self.entity_id.into_parts().len();
|
|
||||||
let mut out = vec![
|
|
||||||
// Copy over the `from` field.
|
|
||||||
Instruction::Copy {
|
|
||||||
source: start_point,
|
|
||||||
destination: base_path_addr,
|
|
||||||
},
|
|
||||||
// Copy over the `to` field.
|
|
||||||
Instruction::Copy {
|
|
||||||
source: start_point,
|
|
||||||
destination: base_path_addr + self.path_first.from.into_parts().len(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if let Some(tag) = tag {
|
|
||||||
// Copy over the `name` field.
|
|
||||||
out.push(Instruction::Copy {
|
|
||||||
source: tag,
|
|
||||||
destination: base_path_addr
|
|
||||||
+ self.path_first.from.into_parts().len()
|
|
||||||
+ self.path_first.to.into_parts().len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The X, Y and Z axes.
|
|
||||||
#[derive(Clone, Copy, ExecutionPlanValue)]
|
|
||||||
pub struct Axes {
|
|
||||||
pub x: Point3d,
|
|
||||||
pub y: Point3d,
|
|
||||||
pub z: Point3d,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, ExecutionPlanValue)]
|
|
||||||
pub struct BasePath {
|
|
||||||
pub from: Point2d<f64>,
|
|
||||||
pub to: Point2d<f64>,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A path.
|
|
||||||
#[derive(Clone, ExecutionPlanValue)]
|
|
||||||
pub enum Path {
|
|
||||||
/// A path that goes to a point.
|
|
||||||
ToPoint { base: BasePath },
|
|
||||||
/// A arc that is tangential to the last path segment that goes to a point
|
|
||||||
TangentialArcTo {
|
|
||||||
base: BasePath,
|
|
||||||
/// the arc's center
|
|
||||||
center: Point2d,
|
|
||||||
/// arc's direction
|
|
||||||
ccw: bool,
|
|
||||||
},
|
|
||||||
/// A path that is horizontal.
|
|
||||||
Horizontal {
|
|
||||||
base: BasePath,
|
|
||||||
/// The x coordinate.
|
|
||||||
x: f64,
|
|
||||||
},
|
|
||||||
/// An angled line to.
|
|
||||||
AngledLineTo {
|
|
||||||
base: BasePath,
|
|
||||||
/// The x coordinate.
|
|
||||||
x: Option<f64>,
|
|
||||||
/// The y coordinate.
|
|
||||||
y: Option<f64>,
|
|
||||||
},
|
|
||||||
/// A base path.
|
|
||||||
Base { base: BasePath },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, ExecutionPlanValue)]
|
|
||||||
pub enum SketchSurface {
|
|
||||||
Plane(Plane),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A plane.
|
|
||||||
#[derive(Clone, Copy, ExecutionPlanValue)]
|
|
||||||
pub struct Plane {
|
|
||||||
/// The id of the plane.
|
|
||||||
pub id: Uuid,
|
|
||||||
// The code for the plane either a string or custom.
|
|
||||||
pub value: PlaneType,
|
|
||||||
/// Origin of the plane.
|
|
||||||
pub origin: Point3d,
|
|
||||||
pub axes: Axes,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type for a plane.
|
|
||||||
#[derive(Clone, Copy, ExecutionPlanValue)]
|
|
||||||
pub enum PlaneType {
|
|
||||||
XY,
|
|
||||||
XZ,
|
|
||||||
YZ,
|
|
||||||
Custom,
|
|
||||||
}
|
|
@ -1048,15 +1048,30 @@ fn store_object_with_array_property() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stdlib_cube_partial() {
|
async fn stdlib_cube_partial() {
|
||||||
let program = r#"
|
let program = r#"
|
||||||
let cube = startSketchAt([22.0, 33.0])
|
let cube = startSketchAt([0.0, 0.0])
|
||||||
|
|> lineTo([4.0, 0.0], %)
|
||||||
"#;
|
"#;
|
||||||
let (plan, _scope) = must_plan(program);
|
let (_plan, _scope) = must_plan(program);
|
||||||
std::fs::write("stdlib_cube_partial.json", serde_json::to_string_pretty(&plan).unwrap()).unwrap();
|
|
||||||
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
|
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
|
||||||
.ast()
|
.ast()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mem = crate::execute(ast, Some(test_client().await)).await.unwrap();
|
let client = test_client().await;
|
||||||
dbg!(mem);
|
let _mem = crate::execute(ast, Some(client)).await.unwrap();
|
||||||
|
// use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat, ModelingCmd};
|
||||||
|
// let out = client
|
||||||
|
// .run_command(
|
||||||
|
// uuid::Uuid::new_v4().into(),
|
||||||
|
// each_cmd::TakeSnapshot {
|
||||||
|
// format: ImageFormat::Png,
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// .await
|
||||||
|
// .unwrap();
|
||||||
|
// let out = match out {
|
||||||
|
// OkModelingCmdResponse::TakeSnapshot(b) => b,
|
||||||
|
// other => panic!("wrong output: {other:?}"),
|
||||||
|
// };
|
||||||
|
// let out: Vec<u8> = out.contents.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_client() -> Session {
|
async fn test_client() -> Session {
|
||||||
|
@ -3091,8 +3091,7 @@ let baz = {a: 1, b: "thing"}
|
|||||||
fn ghi = (x) => {
|
fn ghi = (x) => {
|
||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
"#;
|
||||||
show(part001)"#;
|
|
||||||
let tokens = crate::token::lexer(code);
|
let tokens = crate::token::lexer(code);
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let program = parser.ast().unwrap();
|
let program = parser.ast().unwrap();
|
||||||
@ -3372,9 +3371,7 @@ const mySk1 = startSketchOn('XY')
|
|||||||
offset: -1.35,
|
offset: -1.35,
|
||||||
intersectTag: 'seg01'
|
intersectTag: 'seg01'
|
||||||
}, %)
|
}, %)
|
||||||
|> line([-0.42, -1.72], %)
|
|> line([-0.42, -1.72], %)"#;
|
||||||
|
|
||||||
show(part001)"#;
|
|
||||||
let tokens = crate::token::lexer(some_program_string);
|
let tokens = crate::token::lexer(some_program_string);
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let program = parser.ast().unwrap();
|
let program = parser.ast().unwrap();
|
||||||
@ -3523,8 +3520,7 @@ let baz = {a: 1, part001: "thing"}
|
|||||||
fn ghi = (part001) => {
|
fn ghi = (part001) => {
|
||||||
return part001
|
return part001
|
||||||
}
|
}
|
||||||
|
"#;
|
||||||
show(part001)"#;
|
|
||||||
let tokens = crate::token::lexer(some_program_string);
|
let tokens = crate::token::lexer(some_program_string);
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let mut program = parser.ast().unwrap();
|
let mut program = parser.ast().unwrap();
|
||||||
@ -3546,8 +3542,6 @@ let baz = { a: 1, part001: "thing" }
|
|||||||
fn ghi = (part001) => {
|
fn ghi = (part001) => {
|
||||||
return part001
|
return part001
|
||||||
}
|
}
|
||||||
|
|
||||||
show(mySuperCoolPart)
|
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -3676,8 +3670,7 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> line([0, -l], %)
|
|> line([0, -l], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)
|
||||||
|
"#;
|
||||||
show(firstExtrude)"#;
|
|
||||||
let tokens = crate::token::lexer(some_program_string);
|
let tokens = crate::token::lexer(some_program_string);
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let program = parser.ast().unwrap();
|
let program = parser.ast().unwrap();
|
||||||
@ -3696,8 +3689,6 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> line([0, -l], %)
|
|> line([0, -l], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)
|
||||||
|
|
||||||
show(firstExtrude)
|
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -3718,8 +3709,7 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> line([0, -l], %)
|
|> line([0, -l], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)
|
||||||
|
"#;
|
||||||
show(firstExtrude)"#;
|
|
||||||
let tokens = crate::token::lexer(some_program_string);
|
let tokens = crate::token::lexer(some_program_string);
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let program = parser.ast().unwrap();
|
let program = parser.ast().unwrap();
|
||||||
@ -3741,8 +3731,6 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> line([0, -l], %)
|
|> line([0, -l], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)
|
||||||
|
|
||||||
show(firstExtrude)
|
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -566,17 +566,4 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deserialize_function_show() {
|
|
||||||
let some_function_string = r#"{"type":"StdLib","func":{"name":"show","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false}}"#;
|
|
||||||
let some_function: crate::ast::types::Function = serde_json::from_str(some_function_string).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
some_function,
|
|
||||||
crate::ast::types::Function::StdLib {
|
|
||||||
func: Box::new(crate::std::Show),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
|
//! Functions for setting up our WebSocket and WebRTC connections for communications with the
|
||||||
//! engine.
|
//! engine.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
@ -15,6 +15,12 @@ use crate::{
|
|||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum SocketHealth {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
}
|
||||||
|
|
||||||
type WebSocketTcpWrite = futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>;
|
type WebSocketTcpWrite = futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>;
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)] // for the TcpReadHandle
|
#[allow(dead_code)] // for the TcpReadHandle
|
||||||
@ -22,6 +28,7 @@ pub struct EngineConnection {
|
|||||||
engine_req_tx: mpsc::Sender<ToEngineReq>,
|
engine_req_tx: mpsc::Sender<ToEngineReq>,
|
||||||
responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>>,
|
responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>>,
|
||||||
tcp_read_handle: Arc<TcpReadHandle>,
|
tcp_read_handle: Arc<TcpReadHandle>,
|
||||||
|
socket_health: Arc<Mutex<SocketHealth>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TcpRead {
|
pub struct TcpRead {
|
||||||
@ -119,7 +126,9 @@ impl EngineConnection {
|
|||||||
|
|
||||||
let responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>> = Arc::new(DashMap::new());
|
let responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>> = Arc::new(DashMap::new());
|
||||||
let responses_clone = responses.clone();
|
let responses_clone = responses.clone();
|
||||||
|
let socket_health = Arc::new(Mutex::new(SocketHealth::Active));
|
||||||
|
|
||||||
|
let socket_health_tcp_read = socket_health.clone();
|
||||||
let tcp_read_handle = tokio::spawn(async move {
|
let tcp_read_handle = tokio::spawn(async move {
|
||||||
// Get Websocket messages from API server
|
// Get Websocket messages from API server
|
||||||
loop {
|
loop {
|
||||||
@ -131,6 +140,7 @@ impl EngineConnection {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("got ws error: {:?}", e);
|
println!("got ws error: {:?}", e);
|
||||||
|
*socket_health_tcp_read.lock().unwrap() = SocketHealth::Inactive;
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,6 +153,7 @@ impl EngineConnection {
|
|||||||
handle: Arc::new(tcp_read_handle),
|
handle: Arc::new(tcp_read_handle),
|
||||||
}),
|
}),
|
||||||
responses,
|
responses,
|
||||||
|
socket_health,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,6 +203,14 @@ impl EngineManager for EngineConnection {
|
|||||||
// Wait for the response.
|
// Wait for the response.
|
||||||
let current_time = std::time::Instant::now();
|
let current_time = std::time::Instant::now();
|
||||||
while current_time.elapsed().as_secs() < 60 {
|
while current_time.elapsed().as_secs() < 60 {
|
||||||
|
if let Ok(guard) = self.socket_health.lock() {
|
||||||
|
if *guard == SocketHealth::Inactive {
|
||||||
|
return Err(KclError::Engine(KclErrorDetails {
|
||||||
|
message: "Modeling command failed: websocket closed early".to_string(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
// We pop off the responses to cleanup our mappings.
|
// We pop off the responses to cleanup our mappings.
|
||||||
if let Some((_, resp)) = self.responses.remove(&id) {
|
if let Some((_, resp)) = self.responses.remove(&id) {
|
||||||
return if let Some(data) = &resp.resp {
|
return if let Some(data) = &resp.resp {
|
||||||
|
@ -1017,7 +1017,7 @@ impl ExecutorContext {
|
|||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
program: crate::ast::types::Program,
|
program: crate::ast::types::Program,
|
||||||
memory: &mut ProgramMemory,
|
memory: &mut ProgramMemory,
|
||||||
options: BodyType,
|
_options: BodyType,
|
||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
) -> Result<ProgramMemory, KclError> {
|
) -> Result<ProgramMemory, KclError> {
|
||||||
// Before we even start executing the program, set the units.
|
// Before we even start executing the program, set the units.
|
||||||
@ -1073,24 +1073,11 @@ pub async fn execute(
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _show_fn = Box::new(crate::std::Show);
|
|
||||||
match ctx.stdlib.get_either(&call_expr.callee.name) {
|
match ctx.stdlib.get_either(&call_expr.callee.name) {
|
||||||
FunctionKind::Core(func) => {
|
FunctionKind::Core(func) => {
|
||||||
use crate::docs::StdLibFn;
|
let args = crate::std::Args::new(args, call_expr.into(), ctx.clone());
|
||||||
if func.name() == _show_fn.name() {
|
let result = func.std_lib_fn()(args).await?;
|
||||||
if options != BodyType::Root {
|
memory.return_ = Some(ProgramReturn::Value(result));
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
|
||||||
message: "Cannot call show outside of a root".to_string(),
|
|
||||||
source_ranges: vec![call_expr.into()],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
|
|
||||||
} else {
|
|
||||||
let args = crate::std::Args::new(args, call_expr.into(), ctx.clone());
|
|
||||||
let result = func.std_lib_fn()(args).await?;
|
|
||||||
memory.return_ = Some(ProgramReturn::Value(result));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FunctionKind::Std(func) => {
|
FunctionKind::Std(func) => {
|
||||||
let mut newmem = memory.clone();
|
let mut newmem = memory.clone();
|
||||||
@ -1352,8 +1339,7 @@ const newVar = myVar + 1"#;
|
|||||||
offset: {},
|
offset: {},
|
||||||
tag: "yo2"
|
tag: "yo2"
|
||||||
}}, %)
|
}}, %)
|
||||||
const intersect = segEndX('yo2', part001)
|
const intersect = segEndX('yo2', part001)"#,
|
||||||
show(part001)"#,
|
|
||||||
offset
|
offset
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@ -1399,8 +1385,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> angledLine([ghi(2), 3.04], %)
|
|> angledLine([ghi(2), 3.04], %)
|
||||||
|> angledLine([jkl(yo) + 2, 3.05], %)
|
|> angledLine([jkl(yo) + 2, 3.05], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
const yo2 = hmm([identifierGuy + 5])
|
const yo2 = hmm([identifierGuy + 5])"#;
|
||||||
show(part001)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1415,8 +1400,7 @@ const part001 = startSketchOn('XY')
|
|||||||
min(segLen('seg01', %), myVar),
|
min(segLen('seg01', %), myVar),
|
||||||
-legLen(segLen('seg01', %), myVar)
|
-legLen(segLen('seg01', %), myVar)
|
||||||
], %)
|
], %)
|
||||||
|
"#;
|
||||||
show(part001)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1431,8 +1415,7 @@ const part001 = startSketchOn('XY')
|
|||||||
min(segLen('seg01', %), myVar),
|
min(segLen('seg01', %), myVar),
|
||||||
legLen(segLen('seg01', %), myVar)
|
legLen(segLen('seg01', %), myVar)
|
||||||
], %)
|
], %)
|
||||||
|
"#;
|
||||||
show(part001)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1454,8 +1437,7 @@ const part001 = startSketchOn('XY')
|
|||||||
|> xLine(3.84, %) // selection-range-7ish-before-this
|
|> xLine(3.84, %) // selection-range-7ish-before-this
|
||||||
|
|
||||||
const variableBelowShouldNotBeIncluded = 3
|
const variableBelowShouldNotBeIncluded = 3
|
||||||
|
"#;
|
||||||
show(part001)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1476,9 +1458,7 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> line([w, 0], %)
|
|> line([w, 0], %)
|
||||||
|> line([0, thing()], %)
|
|> line([0, thing()], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)"#;
|
||||||
|
|
||||||
show(firstExtrude)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1499,9 +1479,7 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> line([w, 0], %)
|
|> line([w, 0], %)
|
||||||
|> line([0, thing(8)], %)
|
|> line([0, thing(8)], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)"#;
|
||||||
|
|
||||||
show(firstExtrude)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1522,9 +1500,7 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> line([w, 0], %)
|
|> line([w, 0], %)
|
||||||
|> line(thing(8), %)
|
|> line(thing(8), %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)"#;
|
||||||
|
|
||||||
show(firstExtrude)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1549,9 +1525,7 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> line([w, 0], %)
|
|> line([w, 0], %)
|
||||||
|> line([0, thing(8)], %)
|
|> line([0, thing(8)], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(h, %)
|
|> extrude(h, %)"#;
|
||||||
|
|
||||||
show(firstExtrude)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1570,9 +1544,7 @@ show(firstExtrude)"#;
|
|||||||
return myBox
|
return myBox
|
||||||
}
|
}
|
||||||
|
|
||||||
const fnBox = box(3, 6, 10)
|
const fnBox = box(3, 6, 10)"#;
|
||||||
|
|
||||||
show(fnBox)"#;
|
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1592,8 +1564,6 @@ show(fnBox)"#;
|
|||||||
}
|
}
|
||||||
|
|
||||||
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
||||||
|
|
||||||
show(thisBox)
|
|
||||||
"#;
|
"#;
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1613,8 +1583,6 @@ show(thisBox)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
||||||
|
|
||||||
show(thisBox)
|
|
||||||
"#;
|
"#;
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1634,8 +1602,6 @@ show(thisBox)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
||||||
|
|
||||||
show(thisBox)
|
|
||||||
"#;
|
"#;
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1657,7 +1623,6 @@ let myBox = startSketchOn('XY')
|
|||||||
|
|
||||||
for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
|
for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
|
||||||
const thisBox = box(var)
|
const thisBox = box(var)
|
||||||
show(thisBox)
|
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
@ -1681,7 +1646,6 @@ for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h:
|
|||||||
|
|
||||||
for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
|
for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
|
||||||
const thisBox = box(var[0], var[1], var[2], var[3])
|
const thisBox = box(var[0], var[1], var[2], var[3])
|
||||||
show(thisBox)
|
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
@ -1703,7 +1667,6 @@ for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
|
|||||||
|
|
||||||
const thisBox = box([[0,0], 6, 10, 3])
|
const thisBox = box([[0,0], 6, 10, 3])
|
||||||
|
|
||||||
show(thisBox)
|
|
||||||
"#;
|
"#;
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1820,7 +1783,6 @@ const bracket = startSketchOn('XY')
|
|||||||
|> line([0, -1 * leg1 + thickness], %)
|
|> line([0, -1 * leg1 + thickness], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(width, %)
|
|> extrude(width, %)
|
||||||
show(bracket)
|
|
||||||
"#;
|
"#;
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -1845,7 +1807,6 @@ const bracket = startSketchOn('XY')
|
|||||||
|> line([0, -1 * leg1 + thickness], %)
|
|> line([0, -1 * leg1 + thickness], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(width, %)
|
|> extrude(width, %)
|
||||||
show(bracket)
|
|
||||||
"#;
|
"#;
|
||||||
parse_execute(ast).await.unwrap();
|
parse_execute(ast).await.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -17,14 +17,13 @@ use tower_lsp::{
|
|||||||
DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, Documentation, FullDocumentDiagnosticReport,
|
DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, Documentation, FullDocumentDiagnosticReport,
|
||||||
Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
|
Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
|
||||||
InitializedParams, InlayHint, InlayHintParams, InsertTextFormat, MarkupContent, MarkupKind, MessageType, OneOf,
|
InitializedParams, InlayHint, InlayHintParams, InsertTextFormat, MarkupContent, MarkupKind, MessageType, OneOf,
|
||||||
ParameterInformation, ParameterLabel, Position, RelatedFullDocumentDiagnosticReport, RenameFilesParams,
|
Position, RelatedFullDocumentDiagnosticReport, RenameFilesParams, RenameParams, SemanticToken,
|
||||||
RenameParams, SemanticToken, SemanticTokenType, SemanticTokens, SemanticTokensFullOptions,
|
SemanticTokenType, SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions,
|
||||||
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams, SemanticTokensRegistrationOptions,
|
SemanticTokensParams, SemanticTokensRegistrationOptions, SemanticTokensResult,
|
||||||
SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelp,
|
SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelp, SignatureHelpOptions, SignatureHelpParams,
|
||||||
SignatureHelpOptions, SignatureHelpParams, SignatureInformation, StaticRegistrationOptions, TextDocumentItem,
|
StaticRegistrationOptions, TextDocumentItem, TextDocumentRegistrationOptions, TextDocumentSyncCapability,
|
||||||
TextDocumentRegistrationOptions, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
|
TextDocumentSyncKind, TextDocumentSyncOptions, TextEdit, WorkDoneProgressOptions, WorkspaceEdit,
|
||||||
TextEdit, WorkDoneProgressOptions, WorkspaceEdit, WorkspaceFoldersServerCapabilities,
|
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
|
||||||
WorkspaceServerCapabilities,
|
|
||||||
},
|
},
|
||||||
Client, LanguageServer,
|
Client, LanguageServer,
|
||||||
};
|
};
|
||||||
@ -636,8 +635,9 @@ impl LanguageServer for Backend {
|
|||||||
/// Get completions from our stdlib.
|
/// Get completions from our stdlib.
|
||||||
pub fn get_completions_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, CompletionItem>> {
|
pub fn get_completions_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, CompletionItem>> {
|
||||||
let mut completions = HashMap::new();
|
let mut completions = HashMap::new();
|
||||||
|
let combined = stdlib.combined();
|
||||||
|
|
||||||
for internal_fn in stdlib.fns.values() {
|
for internal_fn in combined.values() {
|
||||||
completions.insert(internal_fn.name(), internal_fn.to_completion_item());
|
completions.insert(internal_fn.name(), internal_fn.to_completion_item());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,32 +652,12 @@ pub fn get_completions_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMa
|
|||||||
/// Get signatures from our stdlib.
|
/// Get signatures from our stdlib.
|
||||||
pub fn get_signatures_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, SignatureHelp>> {
|
pub fn get_signatures_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, SignatureHelp>> {
|
||||||
let mut signatures = HashMap::new();
|
let mut signatures = HashMap::new();
|
||||||
|
let combined = stdlib.combined();
|
||||||
|
|
||||||
for internal_fn in stdlib.fns.values() {
|
for internal_fn in combined.values() {
|
||||||
signatures.insert(internal_fn.name(), internal_fn.to_signature_help());
|
signatures.insert(internal_fn.name(), internal_fn.to_signature_help());
|
||||||
}
|
}
|
||||||
|
|
||||||
let show = SignatureHelp {
|
|
||||||
signatures: vec![SignatureInformation {
|
|
||||||
label: "show".to_string(),
|
|
||||||
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
||||||
kind: MarkupKind::PlainText,
|
|
||||||
value: "Show a model.".to_string(),
|
|
||||||
})),
|
|
||||||
parameters: Some(vec![ParameterInformation {
|
|
||||||
label: ParameterLabel::Simple("sg: SketchGroup".to_string()),
|
|
||||||
documentation: Some(Documentation::MarkupContent(MarkupContent {
|
|
||||||
kind: MarkupKind::PlainText,
|
|
||||||
value: "A sketch group.".to_string(),
|
|
||||||
})),
|
|
||||||
}]),
|
|
||||||
active_parameter: None,
|
|
||||||
}],
|
|
||||||
active_signature: Some(0),
|
|
||||||
active_parameter: None,
|
|
||||||
};
|
|
||||||
signatures.insert("show".to_string(), show);
|
|
||||||
|
|
||||||
Ok(signatures)
|
Ok(signatures)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1909,7 +1909,6 @@ const mySk1 = startSketchAt([0, 0])"#;
|
|||||||
let test_program = r#"startSketchAt([0, 0])
|
let test_program = r#"startSketchAt([0, 0])
|
||||||
|> lineTo([0, -0], %) // MoveRelative
|
|> lineTo([0, -0], %) // MoveRelative
|
||||||
|
|
||||||
show(svg)
|
|
||||||
"#;
|
"#;
|
||||||
let tokens = crate::token::lexer(test_program);
|
let tokens = crate::token::lexer(test_program);
|
||||||
let mut slice = &tokens[..];
|
let mut slice = &tokens[..];
|
||||||
@ -2239,8 +2238,6 @@ const firstExtrude = startSketchOn('XY')
|
|||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(2, %)
|
|> extrude(2, %)
|
||||||
|
|
||||||
show(firstExtrude)
|
|
||||||
|
|
||||||
const secondExtrude = startSketchOn('XY')
|
const secondExtrude = startSketchOn('XY')
|
||||||
|> startProfileAt([0,0], %)
|
|> startProfileAt([0,0], %)
|
||||||
|",
|
|",
|
||||||
@ -2724,9 +2721,7 @@ const b2 = cube([3,3], 4)
|
|||||||
|
|
||||||
const pt1 = b1[0]
|
const pt1 = b1[0]
|
||||||
const pt2 = b2[0]
|
const pt2 = b2[0]
|
||||||
|
"#;
|
||||||
show(b1)
|
|
||||||
show(b2)"#;
|
|
||||||
let tokens = crate::token::lexer(some_program_string);
|
let tokens = crate::token::lexer(some_program_string);
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
parser.ast().unwrap();
|
parser.ast().unwrap();
|
||||||
@ -2755,7 +2750,7 @@ let other_thing = 2 * cos(3)"#;
|
|||||||
return myBox
|
return myBox
|
||||||
}
|
}
|
||||||
let myBox = box([0,0], -3, -16, -10)
|
let myBox = box([0,0], -3, -16, -10)
|
||||||
show(myBox)"#;
|
"#;
|
||||||
let tokens = crate::token::lexer(some_program_string);
|
let tokens = crate::token::lexer(some_program_string);
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
parser.ast().unwrap();
|
parser.ast().unwrap();
|
||||||
|
@ -4,18 +4,18 @@ expression: actual
|
|||||||
---
|
---
|
||||||
{
|
{
|
||||||
"start": 0,
|
"start": 0,
|
||||||
"end": 90,
|
"end": 59,
|
||||||
"body": [
|
"body": [
|
||||||
{
|
{
|
||||||
"type": "VariableDeclaration",
|
"type": "VariableDeclaration",
|
||||||
"type": "VariableDeclaration",
|
"type": "VariableDeclaration",
|
||||||
"start": 0,
|
"start": 0,
|
||||||
"end": 74,
|
"end": 58,
|
||||||
"declarations": [
|
"declarations": [
|
||||||
{
|
{
|
||||||
"type": "VariableDeclarator",
|
"type": "VariableDeclarator",
|
||||||
"start": 6,
|
"start": 6,
|
||||||
"end": 74,
|
"end": 58,
|
||||||
"id": {
|
"id": {
|
||||||
"type": "Identifier",
|
"type": "Identifier",
|
||||||
"start": 6,
|
"start": 6,
|
||||||
@ -26,47 +26,47 @@ expression: actual
|
|||||||
"type": "PipeExpression",
|
"type": "PipeExpression",
|
||||||
"type": "PipeExpression",
|
"type": "PipeExpression",
|
||||||
"start": 17,
|
"start": 17,
|
||||||
"end": 74,
|
"end": 58,
|
||||||
"body": [
|
"body": [
|
||||||
{
|
{
|
||||||
"type": "CallExpression",
|
"type": "CallExpression",
|
||||||
"type": "CallExpression",
|
"type": "CallExpression",
|
||||||
"start": 17,
|
"start": 17,
|
||||||
"end": 56,
|
"end": 40,
|
||||||
"callee": {
|
"callee": {
|
||||||
"type": "Identifier",
|
"type": "Identifier",
|
||||||
"start": 17,
|
"start": 17,
|
||||||
"end": 39,
|
"end": 23,
|
||||||
"name": "unstable_stdlib_circle"
|
"name": "circle"
|
||||||
},
|
},
|
||||||
"arguments": [
|
"arguments": [
|
||||||
{
|
{
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"start": 40,
|
"start": 24,
|
||||||
"end": 44,
|
"end": 28,
|
||||||
"value": "XY",
|
"value": "XY",
|
||||||
"raw": "'XY'"
|
"raw": "'XY'"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "ArrayExpression",
|
"type": "ArrayExpression",
|
||||||
"type": "ArrayExpression",
|
"type": "ArrayExpression",
|
||||||
"start": 46,
|
"start": 30,
|
||||||
"end": 51,
|
"end": 35,
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"start": 47,
|
"start": 31,
|
||||||
"end": 48,
|
"end": 32,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"raw": "0"
|
"raw": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"start": 49,
|
"start": 33,
|
||||||
"end": 50,
|
"end": 34,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"raw": "0"
|
"raw": "0"
|
||||||
}
|
}
|
||||||
@ -75,8 +75,8 @@ expression: actual
|
|||||||
{
|
{
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"start": 53,
|
"start": 37,
|
||||||
"end": 55,
|
"end": 39,
|
||||||
"value": 22,
|
"value": 22,
|
||||||
"raw": "22"
|
"raw": "22"
|
||||||
}
|
}
|
||||||
@ -86,28 +86,28 @@ expression: actual
|
|||||||
{
|
{
|
||||||
"type": "CallExpression",
|
"type": "CallExpression",
|
||||||
"type": "CallExpression",
|
"type": "CallExpression",
|
||||||
"start": 60,
|
"start": 44,
|
||||||
"end": 74,
|
"end": 58,
|
||||||
"callee": {
|
"callee": {
|
||||||
"type": "Identifier",
|
"type": "Identifier",
|
||||||
"start": 60,
|
"start": 44,
|
||||||
"end": 67,
|
"end": 51,
|
||||||
"name": "extrude"
|
"name": "extrude"
|
||||||
},
|
},
|
||||||
"arguments": [
|
"arguments": [
|
||||||
{
|
{
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"start": 68,
|
"start": 52,
|
||||||
"end": 70,
|
"end": 54,
|
||||||
"value": 14,
|
"value": 14,
|
||||||
"raw": "14"
|
"raw": "14"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "PipeSubstitution",
|
"type": "PipeSubstitution",
|
||||||
"type": "PipeSubstitution",
|
"type": "PipeSubstitution",
|
||||||
"start": 72,
|
"start": 56,
|
||||||
"end": 73
|
"end": 57
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"optional": false
|
"optional": false
|
||||||
@ -121,34 +121,6 @@ expression: actual
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"kind": "const"
|
"kind": "const"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "ExpressionStatement",
|
|
||||||
"type": "ExpressionStatement",
|
|
||||||
"start": 75,
|
|
||||||
"end": 89,
|
|
||||||
"expression": {
|
|
||||||
"type": "CallExpression",
|
|
||||||
"type": "CallExpression",
|
|
||||||
"start": 75,
|
|
||||||
"end": 89,
|
|
||||||
"callee": {
|
|
||||||
"type": "Identifier",
|
|
||||||
"start": 75,
|
|
||||||
"end": 79,
|
|
||||||
"name": "show"
|
|
||||||
},
|
|
||||||
"arguments": [
|
|
||||||
{
|
|
||||||
"type": "Identifier",
|
|
||||||
"type": "Identifier",
|
|
||||||
"start": 80,
|
|
||||||
"end": 88,
|
|
||||||
"name": "cylinder"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"optional": false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nonCodeMeta": {
|
"nonCodeMeta": {
|
||||||
|
@ -110,6 +110,9 @@ impl From<ImportFormat> for kittycad::types::InputFormat {
|
|||||||
/// For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters.
|
/// For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters.
|
||||||
/// Otherwise you can specify the unit by passing in the options parameter.
|
/// Otherwise you can specify the unit by passing in the options parameter.
|
||||||
/// If you import a gltf file, we will try to find the bin file and import it as well.
|
/// If you import a gltf file, we will try to find the bin file and import it as well.
|
||||||
|
///
|
||||||
|
/// Import paths are relative to the current project directory. This only works in the desktop app
|
||||||
|
/// not in browser.
|
||||||
pub async fn import(args: Args) -> Result<MemoryItem, KclError> {
|
pub async fn import(args: Args) -> Result<MemoryItem, KclError> {
|
||||||
let (file_path, options): (String, Option<ImportFormat>) = args.get_import_data()?;
|
let (file_path, options): (String, Option<ImportFormat>) = args.get_import_data()?;
|
||||||
|
|
||||||
@ -121,6 +124,9 @@ pub async fn import(args: Args) -> Result<MemoryItem, KclError> {
|
|||||||
/// For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters.
|
/// For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters.
|
||||||
/// Otherwise you can specify the unit by passing in the options parameter.
|
/// Otherwise you can specify the unit by passing in the options parameter.
|
||||||
/// If you import a gltf file, we will try to find the bin file and import it as well.
|
/// If you import a gltf file, we will try to find the bin file and import it as well.
|
||||||
|
///
|
||||||
|
/// Import paths are relative to the current project directory. This only works in the desktop app
|
||||||
|
/// not in browser.
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "import",
|
name = "import",
|
||||||
}]
|
}]
|
||||||
|
@ -11,6 +11,9 @@ pub trait KclStdLibFn: StdLibFn {
|
|||||||
fn kcl_clone_box(&self) -> Box<dyn KclStdLibFn>;
|
fn kcl_clone_box(&self) -> Box<dyn KclStdLibFn>;
|
||||||
fn function(&self) -> &FunctionExpression;
|
fn function(&self) -> &FunctionExpression;
|
||||||
fn program(&self) -> &Program;
|
fn program(&self) -> &Program;
|
||||||
|
fn std_lib(&self) -> Box<dyn StdLibFn> {
|
||||||
|
self.clone_box()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ts_rs::TS for dyn KclStdLibFn {
|
impl ts_rs::TS for dyn KclStdLibFn {
|
||||||
|
@ -37,7 +37,6 @@ pub type FnMap = HashMap<String, StdFn>;
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CORE_FNS: Vec<Box<dyn StdLibFn>> = vec![
|
static ref CORE_FNS: Vec<Box<dyn StdLibFn>> = vec![
|
||||||
Box::new(Show),
|
|
||||||
Box::new(LegLen),
|
Box::new(LegLen),
|
||||||
Box::new(LegAngX),
|
Box::new(LegAngX),
|
||||||
Box::new(LegAngY),
|
Box::new(LegAngY),
|
||||||
@ -133,6 +132,15 @@ impl StdLib {
|
|||||||
Self { fns, kcl_fns }
|
Self { fns, kcl_fns }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the combined hashmaps.
|
||||||
|
pub fn combined(&self) -> HashMap<String, Box<dyn StdLibFn>> {
|
||||||
|
let mut combined = self.fns.clone();
|
||||||
|
for (k, v) in self.kcl_fns.clone() {
|
||||||
|
combined.insert(k, v.std_lib());
|
||||||
|
}
|
||||||
|
combined
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, name: &str) -> Option<Box<dyn StdLibFn>> {
|
pub fn get(&self, name: &str) -> Option<Box<dyn StdLibFn>> {
|
||||||
self.fns.get(name).cloned()
|
self.fns.get(name).cloned()
|
||||||
}
|
}
|
||||||
@ -705,21 +713,6 @@ impl Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a model.
|
|
||||||
// This never actually gets called so this is fine.
|
|
||||||
pub async fn show<'a>(args: Args) -> Result<MemoryItem, KclError> {
|
|
||||||
let sketch_group = args.get_sketch_group()?;
|
|
||||||
inner_show(sketch_group);
|
|
||||||
|
|
||||||
args.make_user_val_from_f64(0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a model.
|
|
||||||
#[stdlib {
|
|
||||||
name = "show",
|
|
||||||
}]
|
|
||||||
fn inner_show(_sketch: Box<SketchGroup>) {}
|
|
||||||
|
|
||||||
/// Returns the length of the given leg.
|
/// Returns the length of the given leg.
|
||||||
pub async fn leg_length(args: Args) -> Result<MemoryItem, KclError> {
|
pub async fn leg_length(args: Args) -> Result<MemoryItem, KclError> {
|
||||||
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
|
||||||
@ -789,6 +782,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_generate_stdlib_markdown_docs() {
|
fn test_generate_stdlib_markdown_docs() {
|
||||||
let stdlib = StdLib::new();
|
let stdlib = StdLib::new();
|
||||||
|
let combined = stdlib.combined();
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
|
|
||||||
buf.push_str("<!--- DO NOT EDIT THIS FILE. IT IS AUTOMATICALLY GENERATED. -->\n\n");
|
buf.push_str("<!--- DO NOT EDIT THIS FILE. IT IS AUTOMATICALLY GENERATED. -->\n\n");
|
||||||
@ -800,8 +794,8 @@ mod tests {
|
|||||||
|
|
||||||
buf.push_str("* [Functions](#functions)\n");
|
buf.push_str("* [Functions](#functions)\n");
|
||||||
|
|
||||||
for key in stdlib.fns.keys().sorted() {
|
for key in combined.keys().sorted() {
|
||||||
let internal_fn = stdlib.fns.get(key).unwrap();
|
let internal_fn = combined.get(key).unwrap();
|
||||||
if internal_fn.unpublished() || internal_fn.deprecated() {
|
if internal_fn.unpublished() || internal_fn.deprecated() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -813,8 +807,8 @@ mod tests {
|
|||||||
|
|
||||||
buf.push_str("## Functions\n\n");
|
buf.push_str("## Functions\n\n");
|
||||||
|
|
||||||
for key in stdlib.fns.keys().sorted() {
|
for key in combined.keys().sorted() {
|
||||||
let internal_fn = stdlib.fns.get(key).unwrap();
|
let internal_fn = combined.get(key).unwrap();
|
||||||
if internal_fn.unpublished() {
|
if internal_fn.unpublished() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -874,11 +868,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_generate_stdlib_json_schema() {
|
fn test_generate_stdlib_json_schema() {
|
||||||
let stdlib = StdLib::new();
|
let stdlib = StdLib::new();
|
||||||
|
let combined = stdlib.combined();
|
||||||
|
|
||||||
let mut json_data = vec![];
|
let mut json_data = vec![];
|
||||||
|
|
||||||
for key in stdlib.fns.keys().sorted() {
|
for key in combined.keys().sorted() {
|
||||||
let internal_fn = stdlib.fns.get(key).unwrap();
|
let internal_fn = combined.get(key).unwrap();
|
||||||
json_data.push(internal_fn.to_json().unwrap());
|
json_data.push(internal_fn.to_json().unwrap());
|
||||||
}
|
}
|
||||||
expectorate::assert_contents(
|
expectorate::assert_contents(
|
||||||
|
@ -48,7 +48,7 @@ impl std::fmt::Debug for Circle {
|
|||||||
/// TODO: Parse the KCL in a macro and generate these
|
/// TODO: Parse the KCL in a macro and generate these
|
||||||
impl StdLibFn for Circle {
|
impl StdLibFn for Circle {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"unstable_stdlib_circle".to_owned()
|
"circle".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn summary(&self) -> String {
|
fn summary(&self) -> String {
|
||||||
@ -64,15 +64,56 @@ impl StdLibFn for Circle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
Vec::new() // TODO
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
|
settings.inline_subschemas = true;
|
||||||
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
|
let mut args = Vec::new();
|
||||||
|
for parameter in &self.function.params {
|
||||||
|
match parameter.identifier.name.as_str() {
|
||||||
|
"plane" => {
|
||||||
|
args.push(crate::docs::StdLibFnArg {
|
||||||
|
name: parameter.identifier.name.to_owned(),
|
||||||
|
type_: "SketchData".to_string(),
|
||||||
|
schema: <crate::std::sketch::SketchData>::json_schema(&mut generator),
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"center" => {
|
||||||
|
args.push(crate::docs::StdLibFnArg {
|
||||||
|
name: parameter.identifier.name.to_owned(),
|
||||||
|
type_: "[number, number]".to_string(),
|
||||||
|
schema: <[f64; 2]>::json_schema(&mut generator),
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"radius" => {
|
||||||
|
args.push(crate::docs::StdLibFnArg {
|
||||||
|
name: parameter.identifier.name.to_owned(),
|
||||||
|
type_: "number".to_string(),
|
||||||
|
schema: <f64>::json_schema(&mut generator),
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => panic!("Unknown parameter: {:?}", parameter.identifier.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
|
||||||
None
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
|
settings.inline_subschemas = true;
|
||||||
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
|
Some(crate::docs::StdLibFnArg {
|
||||||
|
name: "SketchGroup".to_owned(),
|
||||||
|
type_: "SketchGroup".to_string(),
|
||||||
|
schema: <crate::executor::SketchGroup>::json_schema(&mut generator),
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unpublished(&self) -> bool {
|
fn unpublished(&self) -> bool {
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deprecated(&self) -> bool {
|
fn deprecated(&self) -> bool {
|
||||||
|
@ -1463,13 +1463,13 @@ const things = "things"
|
|||||||
fn test_kitt() {
|
fn test_kitt() {
|
||||||
let program = include_str!("../../../tests/executor/inputs/kittycad_svg.kcl");
|
let program = include_str!("../../../tests/executor/inputs/kittycad_svg.kcl");
|
||||||
let actual = lexer(program).unwrap();
|
let actual = lexer(program).unwrap();
|
||||||
assert_eq!(actual.len(), 5098);
|
assert_eq!(actual.len(), 5093);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pipes_on_pipes() {
|
fn test_pipes_on_pipes() {
|
||||||
let program = include_str!("../../../tests/executor/inputs/pipes_on_pipes.kcl");
|
let program = include_str!("../../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||||
let actual = lexer(program).unwrap();
|
let actual = lexer(program).unwrap();
|
||||||
assert_eq!(actual.len(), 17846);
|
assert_eq!(actual.len(), 17841);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lexer_negative_word() {
|
fn test_lexer_negative_word() {
|
||||||
|
@ -6,7 +6,7 @@ fn cube = (length, center) => {
|
|||||||
let p1 = [-l + x, l + y]
|
let p1 = [-l + x, l + y]
|
||||||
let p2 = [ l + x, l + y]
|
let p2 = [ l + x, l + y]
|
||||||
let p3 = [ l + x, -l + y]
|
let p3 = [ l + x, -l + y]
|
||||||
|
|
||||||
return startSketchAt(p0)
|
return startSketchAt(p0)
|
||||||
|> lineTo(p1, %)
|
|> lineTo(p1, %)
|
||||||
|> lineTo(p2, %)
|
|> lineTo(p2, %)
|
||||||
@ -17,4 +17,3 @@ fn cube = (length, center) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const myCube = cube(40, [0,0])
|
const myCube = cube(40, [0,0])
|
||||||
show(myCube)
|
|
||||||
|
@ -1,2 +1 @@
|
|||||||
const cylinder = unstable_stdlib_circle('XY', [0,0], 22) |> extrude(14, %)
|
const cylinder = circle('XY', [0,0], 22) |> extrude(14, %)
|
||||||
show(cylinder)
|
|
||||||
|
@ -308,4 +308,3 @@ const svg = startSketchOn('XY')
|
|||||||
|> lineTo([13.44, -13.44], %) // VerticalLineHorizonal
|
|> lineTo([13.44, -13.44], %) // VerticalLineHorizonal
|
||||||
|> lineTo([14.28, -13.44], %) // HorizontalLineRelative
|
|> lineTo([14.28, -13.44], %) // HorizontalLineRelative
|
||||||
|> close(%)
|
|> close(%)
|
||||||
show(svg)
|
|
||||||
|
@ -468,4 +468,3 @@ const svg = startSketchOn('XY')
|
|||||||
|> bezierCurve({ control1: [0, -2], control2: [-2.68, -2.67], to: [-1.36, -2.34] }, %) // CubicBezierAbsolute
|
|> bezierCurve({ control1: [0, -2], control2: [-2.68, -2.67], to: [-1.36, -2.34] }, %) // CubicBezierAbsolute
|
||||||
|> bezierCurve({ control1: [0, -0], control2: [0, -1.34], to: [0, -0.68] }, %) // CubicBezierAbsolute
|
|> bezierCurve({ control1: [0, -0], control2: [0, -1.34], to: [0, -0.68] }, %) // CubicBezierAbsolute
|
||||||
|> close(%)
|
|> close(%)
|
||||||
show(svg)
|
|
||||||
|
@ -602,23 +602,14 @@ const part004 = startSketchOn('YZ')
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_holes() {
|
async fn serial_test_holes() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const square = startSketchOn('XY')
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt(pos, %)
|
|
||||||
|> arc({angle_end: 360, angle_start: 0, radius: radius}, %)
|
|
||||||
|> close(%)
|
|
||||||
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const square = startSketchOn('XY')
|
|
||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([0, 0], %)
|
||||||
|> line([0, 10], %)
|
|> line([0, 10], %)
|
||||||
|> line([10, 0], %)
|
|> line([10, 0], %)
|
||||||
|> line([0, -10], %)
|
|> line([0, -10], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> hole(circle([2, 2], .5), %)
|
|> hole(circle('XY', [2, 2], .5), %)
|
||||||
|> hole(circle([2, 8], .5), %)
|
|> hole(circle('XY', [2, 8], .5), %)
|
||||||
|> extrude(2, %)
|
|> extrude(2, %)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
@ -631,7 +622,7 @@ const square = startSketchOn('XY')
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn optional_params() {
|
async fn optional_params() {
|
||||||
let code = r#"
|
let code = r#"
|
||||||
fn circle = (pos, radius, tag?) => {
|
fn other_circle = (pos, radius, tag?) => {
|
||||||
const sg = startSketchOn('XY')
|
const sg = startSketchOn('XY')
|
||||||
|> startProfileAt(pos, %)
|
|> startProfileAt(pos, %)
|
||||||
|> arc({angle_end: 360, angle_start: 0, radius: radius}, %)
|
|> arc({angle_end: 360, angle_start: 0, radius: radius}, %)
|
||||||
@ -640,7 +631,7 @@ async fn optional_params() {
|
|||||||
return sg
|
return sg
|
||||||
}
|
}
|
||||||
|
|
||||||
const thing = circle([2, 2], 20)
|
const thing = other_circle([2, 2], 20)
|
||||||
"#;
|
"#;
|
||||||
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
|
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
|
||||||
.await
|
.await
|
||||||
@ -650,19 +641,7 @@ const thing = circle([2, 2], 20)
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_rounded_with_holes() {
|
async fn serial_test_rounded_with_holes() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"fn tarc = (to, sketchGroup, tag?) => {
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tarc = (to, sketchGroup, tag?) => {
|
|
||||||
return tangentialArcTo(to, sketchGroup, tag)
|
return tangentialArcTo(to, sketchGroup, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,10 +664,10 @@ const holeRadius = 1
|
|||||||
const holeIndex = 6
|
const holeIndex = 6
|
||||||
|
|
||||||
const part = roundedRectangle([0, 0], 20, 20, 4)
|
const part = roundedRectangle([0, 0], 20, 20, 4)
|
||||||
|> hole(circle([-holeIndex, holeIndex], holeRadius), %)
|
|> hole(circle('XY', [-holeIndex, holeIndex], holeRadius), %)
|
||||||
|> hole(circle([holeIndex, holeIndex], holeRadius), %)
|
|> hole(circle('XY', [holeIndex, holeIndex], holeRadius), %)
|
||||||
|> hole(circle([-holeIndex, -holeIndex], holeRadius), %)
|
|> hole(circle('XY', [-holeIndex, -holeIndex], holeRadius), %)
|
||||||
|> hole(circle([holeIndex, -holeIndex], holeRadius), %)
|
|> hole(circle('XY', [holeIndex, -holeIndex], holeRadius), %)
|
||||||
|> extrude(2, %)
|
|> extrude(2, %)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
@ -700,19 +679,7 @@ const part = roundedRectangle([0, 0], 20, 20, 4)
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_top_level_expression() {
|
async fn serial_test_top_level_expression() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"circle('XY', [0,0], 22) |> extrude(14, %)"#;
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
circle([0,0], 22) |> extrude(14, %)"#;
|
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
|
let result = execute_and_snapshot(code, kittycad::types::UnitLength::Mm)
|
||||||
.await
|
.await
|
||||||
@ -722,19 +689,7 @@ circle([0,0], 22) |> extrude(14, %)"#;
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_patterns_linear_basic() {
|
async fn serial_test_patterns_linear_basic() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const part = circle('XY', [0,0], 2)
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = circle([0,0], 2)
|
|
||||||
|> patternLinear({axis: [0,1], repetitions: 12, distance: 2}, %)
|
|> patternLinear({axis: [0,1], repetitions: 12, distance: 2}, %)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
@ -746,19 +701,7 @@ const part = circle([0,0], 2)
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_patterns_linear_basic_3d() {
|
async fn serial_test_patterns_linear_basic_3d() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const part = startSketchOn('XY')
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = startSketchOn('XY')
|
|
||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([0, 0], %)
|
||||||
|> line([0,1], %)
|
|> line([0,1], %)
|
||||||
|> line([1, 0], %)
|
|> line([1, 0], %)
|
||||||
@ -776,19 +719,7 @@ const part = startSketchOn('XY')
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_patterns_linear_basic_negative_distance() {
|
async fn serial_test_patterns_linear_basic_negative_distance() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const part = circle('XY', [0,0], 2)
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = circle([0,0], 2)
|
|
||||||
|> patternLinear({axis: [0,1], repetitions: 12, distance: -2}, %)
|
|> patternLinear({axis: [0,1], repetitions: 12, distance: -2}, %)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
@ -804,19 +735,7 @@ const part = circle([0,0], 2)
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_patterns_linear_basic_negative_axis() {
|
async fn serial_test_patterns_linear_basic_negative_axis() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const part = circle('XY', [0,0], 2)
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = circle([0,0], 2)
|
|
||||||
|> patternLinear({axis: [0,-1], repetitions: 12, distance: 2}, %)
|
|> patternLinear({axis: [0,-1], repetitions: 12, distance: 2}, %)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
@ -832,19 +751,7 @@ const part = circle([0,0], 2)
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_patterns_linear_basic_holes() {
|
async fn serial_test_patterns_linear_basic_holes() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const circles = circle('XY', [5, 5], 1)
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const circles = circle([5, 5], 1)
|
|
||||||
|> patternLinear({axis: [1,1], repetitions: 12, distance: 3}, %)
|
|> patternLinear({axis: [1,1], repetitions: 12, distance: 3}, %)
|
||||||
|
|
||||||
const rectangle = startSketchOn('XY')
|
const rectangle = startSketchOn('XY')
|
||||||
@ -865,19 +772,7 @@ const rectangle = startSketchOn('XY')
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_patterns_circular_basic_2d() {
|
async fn serial_test_patterns_circular_basic_2d() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const part = circle('XY', [0,0], 2)
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = circle([0,0], 2)
|
|
||||||
|> patternCircular({axis: [0,1], center: [20, 20, 20], repetitions: 12, arcDegrees: 210, rotateDuplicates: true}, %)
|
|> patternCircular({axis: [0,1], center: [20, 20, 20], repetitions: 12, arcDegrees: 210, rotateDuplicates: true}, %)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
@ -889,19 +784,7 @@ const part = circle([0,0], 2)
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_patterns_circular_basic_3d() {
|
async fn serial_test_patterns_circular_basic_3d() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const part = startSketchOn('XY')
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = startSketchOn('XY')
|
|
||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([0, 0], %)
|
||||||
|> line([0,1], %)
|
|> line([0,1], %)
|
||||||
|> line([1, 0], %)
|
|> line([1, 0], %)
|
||||||
@ -919,19 +802,7 @@ const part = startSketchOn('XY')
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_patterns_circular_3d_tilted_axis() {
|
async fn serial_test_patterns_circular_3d_tilted_axis() {
|
||||||
let code = r#"fn circle = (pos, radius) => {
|
let code = r#"const part = startSketchOn('XY')
|
||||||
const sg = startSketchOn('XY')
|
|
||||||
|> startProfileAt([pos[0] + radius, pos[1]], %)
|
|
||||||
|> arc({
|
|
||||||
angle_end: 360,
|
|
||||||
angle_start: 0,
|
|
||||||
radius: radius
|
|
||||||
}, %)
|
|
||||||
|> close(%)
|
|
||||||
return sg
|
|
||||||
}
|
|
||||||
|
|
||||||
const part = startSketchOn('XY')
|
|
||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([0, 0], %)
|
||||||
|> line([0,1], %)
|
|> line([0,1], %)
|
||||||
|> line([1, 0], %)
|
|> line([1, 0], %)
|
||||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |