Compare commits
1 Commits
kurt-reduc
...
kurt-add-s
Author | SHA1 | Date | |
---|---|---|---|
3e0c44e689 |
@ -3,3 +3,4 @@ VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
VITE_KC_SENTRY_DSN=
|
||||
|
@ -3,3 +3,4 @@ VITE_KC_API_BASE_URL=https://api.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
VITE_KC_SENTRY_DSN=
|
||||
|
16
.github/workflows/cargo-test.yml
vendored
@ -40,20 +40,6 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
- name: Install vector
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
|
||||
chmod +x /tmp/vector.sh
|
||||
/tmp/vector.sh -y -no-modify-path
|
||||
mkdir -p /tmp/vector
|
||||
cp .github/workflows/vector.toml /tmp/vector.toml
|
||||
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
|
||||
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
|
||||
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
|
||||
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
|
||||
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
|
||||
cat /tmp/vector.toml
|
||||
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- name: Rust Cache
|
||||
@ -62,7 +48,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |-
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo nextest run --workspace --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
|
||||
cargo nextest run --workspace --no-fail-fast -P ci
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
RUST_MIN_STACK: 10485760000
|
||||
|
2
.github/workflows/ci.yml
vendored
@ -336,7 +336,7 @@ jobs:
|
||||
cat last_download.json
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.1.2'
|
||||
uses: 'google-github-actions/auth@v2.1.1'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
|
6
.github/workflows/playwright.yml
vendored
@ -4,11 +4,6 @@ on:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
playwright-ubuntu:
|
||||
timeout-minutes: 60
|
||||
@ -85,6 +80,7 @@ jobs:
|
||||
playwright-macos:
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-14
|
||||
needs: playwright-ubuntu
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
21
.github/workflows/vector.toml
vendored
@ -1,21 +0,0 @@
|
||||
[sources.github-actions-file]
|
||||
type = "file"
|
||||
data_dir = "/tmp/vector"
|
||||
include = ["/tmp/github-actions.log"]
|
||||
|
||||
# Modify the logs to include the action name.
|
||||
[transforms.add-action-name]
|
||||
type = "remap"
|
||||
inputs = [ "github-actions-file" ]
|
||||
source = '''
|
||||
.action = "GITHUB_WORKFLOW"
|
||||
.repo = "GITHUB_REPOSITORY"
|
||||
.sha = "GITHUB_SHA"
|
||||
.ref = "GITHUB_REF_NAME"
|
||||
'''
|
||||
|
||||
[sinks.axiom]
|
||||
type = "axiom"
|
||||
inputs = ["add-action-name"]
|
||||
token = "GH_ACTIONS_AXIOM_TOKEN"
|
||||
dataset = "github-actions"
|
3
.gitignore
vendored
@ -33,7 +33,6 @@ src/wasm-lib/bindings
|
||||
src/wasm-lib/kcl/bindings
|
||||
public/wasm_lib_bg.wasm
|
||||
src/wasm-lib/lcov.info
|
||||
src/wasm-lib/grackle/test_json_output
|
||||
|
||||
e2e/playwright/playwright-secrets.env
|
||||
e2e/playwright/temp1.png
|
||||
@ -55,5 +54,3 @@ e2e/playwright/export-snapshots/*embedded.gltf
|
||||
|
||||
## generated files
|
||||
src/**/*.typegen.ts
|
||||
|
||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||
|
@ -8,6 +8,10 @@ once fixed in engine will just start working here with no language changes.
|
||||
model for that sketch and its underlying 3D object.
|
||||
If you see a red line around your model, it means this is happening.
|
||||
|
||||
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
|
||||
item in the pattern is being subtracted. This is an engine bug that is being
|
||||
worked on.
|
||||
|
||||
- **Import**: Right now you can import a file, even if that file has brep data
|
||||
you cannot edit it, after v1, the engine will account for this. You also cannot
|
||||
currently move or transform the imported objects at all, once we have assemblies
|
||||
|
2443
docs/kcl/std.json
474
docs/kcl/std.md
@ -20,7 +20,6 @@
|
||||
* [`atan`](#atan)
|
||||
* [`bezierCurve`](#bezierCurve)
|
||||
* [`ceil`](#ceil)
|
||||
* [`circle`](#circle)
|
||||
* [`close`](#close)
|
||||
* [`cos`](#cos)
|
||||
* [`e`](#e)
|
||||
@ -50,6 +49,7 @@
|
||||
* [`segEndX`](#segEndX)
|
||||
* [`segEndY`](#segEndY)
|
||||
* [`segLen`](#segLen)
|
||||
* [`show`](#show)
|
||||
* [`sin`](#sin)
|
||||
* [`sqrt`](#sqrt)
|
||||
* [`startProfileAt`](#startProfileAt)
|
||||
@ -3438,290 +3438,6 @@ 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 the current sketch.
|
||||
@ -4987,7 +4703,6 @@ hole(hole_sketch_group: SketchGroupSet, sketch_group: SketchGroup) -> SketchGrou
|
||||
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.
|
||||
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
|
||||
@ -6371,8 +6086,8 @@ patternCircular(data: CircularPatternData, geometry: Geometry) -> Geometries
|
||||
{
|
||||
// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
|
||||
arcDegrees: number,
|
||||
// The axis around which to make the pattern. This is a 2D vector.
|
||||
axis: [number, number],
|
||||
// The axis around which to make the pattern. This is a 3D vector.
|
||||
axis: [number, number, number],
|
||||
// The center about which to make th pattern. This is a 3D vector.
|
||||
center: [number, number, number],
|
||||
// The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once.
|
||||
@ -6640,8 +6355,8 @@ patternLinear(data: LinearPatternData, geometry: Geometry) -> Geometries
|
||||
* `data`: `LinearPatternData` - Data for a linear pattern.
|
||||
```
|
||||
{
|
||||
// The axis of the pattern. This is a 2D vector.
|
||||
axis: [number, number],
|
||||
// The axis of the pattern. This is a 3D vector.
|
||||
axis: [number, number, number],
|
||||
// The distance between each repetition. This can also be referred to as spacing.
|
||||
distance: number,
|
||||
// The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once.
|
||||
@ -7668,6 +7383,185 @@ 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
|
||||
|
||||
Computes the sine of a number (in radians).
|
||||
|
@ -3,8 +3,6 @@ import { secrets } from './secrets'
|
||||
import { getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
import { Themes } from '../../src/lib/theme'
|
||||
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
|
||||
@ -17,9 +15,9 @@ document.addEventListener('mousemove', (e) =>
|
||||
*/
|
||||
|
||||
const commonPoints = {
|
||||
startAt: '[9.06, -12.22]',
|
||||
num1: 9.14,
|
||||
num2: 18.2,
|
||||
startAt: '[26.38, -35.59]',
|
||||
num1: 26.63,
|
||||
num2: 53.01,
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
@ -67,8 +65,10 @@ test('Basic sketch', async ({ page }) => {
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
@ -90,6 +90,7 @@ test('Basic sketch', async ({ page }) => {
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const num = 26.63
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
@ -135,126 +136,6 @@ test('Basic sketch', async ({ page }) => {
|
||||
|> 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 }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
@ -403,9 +284,10 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
}) => {
|
||||
await u.openDebugPanel()
|
||||
|
||||
await u.updateCamPosition(viewCmd)
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.updateCamPosition(viewCmd)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(clickCoords.x, clickCoords.y)
|
||||
@ -433,7 +315,7 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
const codeTemplate = (
|
||||
plane = 'XY'
|
||||
) => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([1.14, -1.54], %)`
|
||||
|> startProfileAt([32.13, -43.34], %)`
|
||||
await TestSinglePlane({
|
||||
viewCmd: camPos,
|
||||
expectedCode: codeTemplate('XY'),
|
||||
@ -443,7 +325,7 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
await TestSinglePlane({
|
||||
viewCmd: camPos,
|
||||
expectedCode: codeTemplate('YZ'),
|
||||
clickCoords: { x: 700, y: 250 }, // green plane
|
||||
clickCoords: { x: 700, y: 300 }, // green plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camPos,
|
||||
@ -504,16 +386,12 @@ test('Auto complete works', async ({ page }) => {
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('Enter')
|
||||
// finish line with comment
|
||||
await page.keyboard.type('(5, %) // lin')
|
||||
await page.waitForTimeout(100)
|
||||
// there shouldn't be any auto complete options for 'lin' in the comment
|
||||
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
|
||||
await page.keyboard.type('(5, %)')
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> xLine(5, %) // lin`)
|
||||
|> xLine(5, %)`)
|
||||
})
|
||||
|
||||
// Onboarding tests
|
||||
@ -743,12 +621,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
||||
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
||||
await expect(themeOption).toBeVisible()
|
||||
await themeOption.click()
|
||||
const themeInput = page.getByPlaceholder('system')
|
||||
const themeInput = page.getByPlaceholder('Select an option')
|
||||
await expect(themeInput).toBeVisible()
|
||||
await expect(themeInput).toBeFocused()
|
||||
// Select dark theme
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
||||
'data-headlessui-state',
|
||||
'active'
|
||||
@ -821,8 +699,6 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
||||
).toBeDisabled()
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||
|
||||
// Check that the code was updated
|
||||
await page.keyboard.press('Enter')
|
||||
// Unfortunately this indentation seems to matter for the test
|
||||
@ -917,7 +793,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
const startAt2 = '[0.93,-1.25]'
|
||||
const startAt2 = '[26.23, -35.39]'
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
@ -931,7 +807,7 @@ const part002 = startSketchOn('XY')
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const num2 = 0.94
|
||||
const num2 = 26.48
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
@ -949,7 +825,7 @@ const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('XY')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)
|
||||
|> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
|
||||
|> line([0, ${num2}], %)`.replace(/\s/g, '')
|
||||
)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(
|
||||
@ -959,8 +835,8 @@ const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('XY')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)
|
||||
|> line([0, ${roundOff(num2 - 0.01)}], %)
|
||||
|> line([-1.87, 0], %)`.replace(/\s/g, '')
|
||||
|> line([0, ${num2}], %)
|
||||
|> line([-52.71, 0], %)`.replace(/\s/g, '')
|
||||
)
|
||||
})
|
||||
|
||||
@ -1054,7 +930,7 @@ fn yohey = (pos) => {
|
||||
|> line([-15.79, 17.08], %)
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
yohey([15.79, -34.6])
|
||||
`
|
||||
)
|
||||
@ -1116,6 +992,7 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
@ -1173,160 +1050,3 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
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,10 +29,98 @@ test.beforeEach(async ({ context, page }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
})
|
||||
|
||||
test.setTimeout(60_000)
|
||||
test.setTimeout(60000)
|
||||
|
||||
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.setTimeout(120_000)
|
||||
// FYI this test doesn't work with only engine running locally
|
||||
// And you will need to have the KittyCAD CLI installed
|
||||
const u = getUtils(page)
|
||||
@ -91,6 +179,8 @@ const part001 = startSketchOn('-XZ')
|
||||
await page.waitForTimeout(1000)
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
|
||||
interface Paths {
|
||||
modelPath: string
|
||||
imagePath: string
|
||||
@ -99,21 +189,19 @@ const part001 = startSketchOn('-XZ')
|
||||
const doExport = async (
|
||||
output: Models['OutputFormat_type']
|
||||
): Promise<Paths> => {
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
await page.getByRole('button', { name: 'Export Part' }).click()
|
||||
await page.getByRole('button', { name: 'Export Model' }).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) {
|
||||
await page.getByRole('button', { name: 'storage', exact: false }).click()
|
||||
await page
|
||||
.getByRole('option', { name: output.storage, exact: false })
|
||||
.click()
|
||||
const storageSelect = page.getByTestId('export-storage')
|
||||
await storageSelect.selectOption({ label: output.storage })
|
||||
}
|
||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||
|
||||
// Handle download
|
||||
const download = await page.waitForEvent('download')
|
||||
const downloadPromise = page.waitForEvent('download')
|
||||
await page.getByRole('button', { name: 'Export', exact: true }).click()
|
||||
const download = await downloadPromise
|
||||
const downloadLocationer = (extra = '', isImage = false) =>
|
||||
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||
'storage' in output ? output.storage : ''
|
||||
@ -296,13 +384,13 @@ test('extrude on each default plane should be stable', async ({
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([7.00, 4.40], %)
|
||||
|> line([6.60, -0.20], %)
|
||||
|> line([2.80, 5.00], %)
|
||||
|> line([-5.60, 4.40], %)
|
||||
|> line([-5.40, -3.80], %)
|
||||
|> startProfileAt([0.70, 0.44], %)
|
||||
|> line([0.66, -0.02], %)
|
||||
|> line([0.28, 0.50], %)
|
||||
|> line([-0.56, 0.44], %)
|
||||
|> line([-0.54, -0.38], %)
|
||||
|> close(%)
|
||||
|> extrude(10.00, %)
|
||||
|> extrude(1.00, %)
|
||||
`
|
||||
await context.addInitScript(async (code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
@ -347,23 +435,7 @@ test('extrude on each default plane should be stable', async ({
|
||||
await runSnapshotsForOtherPlanes('-YZ')
|
||||
})
|
||||
|
||||
test('Draft segments should look right', async ({ page, context }) => {
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'in',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'system',
|
||||
unitSystem: 'imperial',
|
||||
})
|
||||
)
|
||||
})
|
||||
test('Draft segments should look right', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -396,7 +468,7 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
@ -410,8 +482,8 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
|
||||
@ -421,202 +493,3 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('Client side scene scale should match engine scale inch', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'in',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'system',
|
||||
unitSystem: 'imperial',
|
||||
})
|
||||
)
|
||||
})
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
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()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('-XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([9.06, -12.22], %)
|
||||
|> line([9.14, 0], %)
|
||||
|> 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
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('Client side scene scale should match engine scale mm', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'mm',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'system',
|
||||
unitSystem: 'metric',
|
||||
})
|
||||
)
|
||||
})
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
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()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// select a plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`const part001 = startSketchOn('-XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([230.03, -310.33], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([230.03, -310.33], %)
|
||||
|> line([232.2, 0], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([230.03, -310.33], %)
|
||||
|> line([232.2, 0], %)
|
||||
|> tangentialArcTo([694.43, -78.12], %)`)
|
||||
|
||||
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// screen shot should show the sketch
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 120 KiB |
After Width: | Height: | Size: 75 KiB |
After Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 52 KiB |
@ -1,23 +1,16 @@
|
||||
import { expect, Page, errors } from '@playwright/test'
|
||||
import { expect, Page } from '@playwright/test'
|
||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import fsp from 'fs/promises'
|
||||
import pixelMatch from 'pixelmatch'
|
||||
import { PNG } from 'pngjs'
|
||||
|
||||
async function waitForPageLoad(page: Page) {
|
||||
try {
|
||||
// wait for 'Loading stream...' spinner
|
||||
await page.getByTestId('loading-stream').waitFor()
|
||||
// wait for all spinners to be gone
|
||||
await page.getByTestId('loading').waitFor({ state: 'detached' })
|
||||
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
|
||||
}
|
||||
}
|
||||
// wait for 'Loading stream...' spinner
|
||||
await page.getByTestId('loading-stream').waitFor()
|
||||
// wait for all spinners to be gone
|
||||
await page.getByTestId('loading').waitFor({ state: 'detached' })
|
||||
|
||||
await page.getByTestId('start-sketch').waitFor()
|
||||
}
|
||||
|
||||
async function removeCurrentCode(page: Page) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.15.4",
|
||||
"version": "0.15.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.10.2",
|
||||
@ -10,11 +10,12 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.54",
|
||||
"@kittycad/lib": "^0.0.53",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@replit/codemirror-interact": "^6.3.0",
|
||||
"@sentry/react": "^7.77.0",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
|
@ -1,34 +1,16 @@
|
||||
import re
|
||||
import os
|
||||
import requests
|
||||
import os
|
||||
|
||||
webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
|
||||
release_version = os.getenv('RELEASE_VERSION')
|
||||
release_body = os.getenv('RELEASE_BODY')
|
||||
|
||||
# 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
|
||||
# message to send to Discord
|
||||
data = {
|
||||
"content":
|
||||
f'''
|
||||
**{release_version}** is now available! Check out the latest features and improvements here: <https://zoo.dev/modeling-app/download>
|
||||
{modified_release_body}
|
||||
**{release_version}** is now available! Check out the latest features and improvements here: https://zoo.dev/modeling-app/download
|
||||
{release_body}
|
||||
''',
|
||||
"username": "Modeling App Release Updates",
|
||||
"avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png"
|
||||
@ -41,7 +23,4 @@ response = requests.post(webhook_url, json=data)
|
||||
if response.status_code == 204:
|
||||
print("Successfully sent the message to Discord.")
|
||||
else:
|
||||
print(f"Failed to send the message to Discord. Status code: {response.status_code}, Response: {response.text}")
|
||||
|
||||
print(modified_release_body)
|
||||
print(data["content"])
|
||||
print("Failed to send the message to Discord.")
|
22
src-tauri/Cargo.lock
generated
@ -67,9 +67,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.80"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
@ -1664,9 +1664,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.58"
|
||||
version = "0.2.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "049c3881ffbe77bf1c3a968372a246ce906eceb79f61cd0bc5fa229bec3504cb"
|
||||
checksum = "a086e1a1bbddb3b38959c0f0ce6de6b3a3b7566e38e0b7d5fb101e91911beed4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -3235,9 +3235,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -3253,9 +3253,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3275,9 +3275,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.114"
|
||||
version = "1.0.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||
dependencies = [
|
||||
"itoa 1.0.6",
|
||||
"ryu",
|
||||
@ -3872,7 +3872,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#ed682dd96eb765e7cd3cdbc3cc64f794a0d6f9df"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#01211ff0759d578e0e9ac8c98c31fdf09077eb34"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
|
@ -16,7 +16,7 @@ tauri-build = { version = "1.5.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kittycad = "0.2.58"
|
||||
kittycad = "0.2.53"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -7,6 +7,7 @@ use std::io::Read;
|
||||
|
||||
use anyhow::Result;
|
||||
use oauth2::TokenResponse;
|
||||
use std::process::Command;
|
||||
use tauri::{InvokeError, Manager};
|
||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "zoo-modeling-app",
|
||||
"version": "0.15.4"
|
||||
"version": "0.15.2"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -3,8 +3,15 @@ import {
|
||||
createBrowserRouter,
|
||||
Outlet,
|
||||
redirect,
|
||||
useLocation,
|
||||
RouterProvider,
|
||||
} from 'react-router-dom'
|
||||
import {
|
||||
matchRoutes,
|
||||
createRoutesFromChildren,
|
||||
useNavigationType,
|
||||
} from 'react-router'
|
||||
import { useEffect } from 'react'
|
||||
import { ErrorPage } from './components/ErrorPage'
|
||||
import { Settings } from './routes/Settings'
|
||||
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||
@ -31,6 +38,8 @@ import { ContextFrom } from 'xstate'
|
||||
import CommandBarProvider, {
|
||||
CommandBar,
|
||||
} from 'components/CommandBar/CommandBar'
|
||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import { KclContextProvider, kclManager } from 'lang/KclSingleton'
|
||||
import FileMachineProvider from 'components/FileMachineProvider'
|
||||
@ -39,6 +48,38 @@ import { paths } from 'lib/paths'
|
||||
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
|
||||
if (VITE_KC_SENTRY_DSN && !TEST) {
|
||||
Sentry.init({
|
||||
dsn: VITE_KC_SENTRY_DSN,
|
||||
// TODO(paultag): pass in the right env here.
|
||||
// environment: "production",
|
||||
integrations: [
|
||||
new Sentry.BrowserTracing({
|
||||
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes
|
||||
),
|
||||
}),
|
||||
new Sentry.Replay(),
|
||||
],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// TODO: Add in kittycad.io endpoints
|
||||
tracePropagationTargets: ['localhost'],
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
})
|
||||
}
|
||||
|
||||
export const BROWSER_FILE_NAME = 'new'
|
||||
|
||||
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
||||
|
@ -16,11 +16,7 @@ import {
|
||||
SKETCH_LAYER,
|
||||
ZOOM_MAGIC_NUMBER,
|
||||
} from './sceneInfra'
|
||||
import {
|
||||
EngineCommand,
|
||||
Subscription,
|
||||
engineCommandManager,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||
@ -32,12 +28,6 @@ const FRAMES_TO_ANIMATE_IN = 30
|
||||
|
||||
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 {
|
||||
position: Vector3
|
||||
quaternion: Quaternion
|
||||
@ -120,11 +110,10 @@ const throttledUpdateEngineFov = throttle(
|
||||
lastCmdDelay
|
||||
) as any as number
|
||||
},
|
||||
1000 / 30
|
||||
1000 / 15
|
||||
)
|
||||
|
||||
export class CameraControls {
|
||||
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
||||
camera: PerspectiveCamera | OrthographicCamera
|
||||
target: Vector3
|
||||
domElement: HTMLCanvasElement
|
||||
@ -162,17 +151,6 @@ export class CameraControls {
|
||||
get isPerspective() {
|
||||
return this.camera instanceof PerspectiveCamera
|
||||
}
|
||||
private debounceTimer = 0
|
||||
|
||||
handleStart = () => {
|
||||
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
||||
this._isCamMovingCallback(true, false)
|
||||
}
|
||||
handleEnd = () => {
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
this._isCamMovingCallback(false, false)
|
||||
}, 400) as any as number
|
||||
}
|
||||
|
||||
// reacts hooks into some of this singleton's properties
|
||||
reactCameraProperties: ReactCameraProperties = {
|
||||
@ -209,7 +187,6 @@ export class CameraControls {
|
||||
this.camera.zoom = camProps.zoom || 1
|
||||
}
|
||||
this.camera.updateProjectionMatrix()
|
||||
console.log('doing this thing', camProps)
|
||||
this.update(true)
|
||||
}
|
||||
|
||||
@ -232,46 +209,6 @@ export class CameraControls {
|
||||
this.onWindowResize()
|
||||
|
||||
this.update()
|
||||
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 =
|
||||
@ -305,21 +242,6 @@ export class CameraControls {
|
||||
onMouseDown = (event: MouseEvent) => {
|
||||
this.isDragging = true
|
||||
this.mouseDownPosition.set(event.clientX, event.clientY)
|
||||
let interaction = this.getInteractionType(event)
|
||||
if (interaction === 'none') return
|
||||
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) => {
|
||||
@ -330,34 +252,36 @@ export class CameraControls {
|
||||
.sub(this.mouseDownPosition)
|
||||
this.mouseDownPosition.copy(this.mouseNewPosition)
|
||||
|
||||
const interaction = this.getInteractionType(event)
|
||||
if (interaction === 'none') return
|
||||
let state: 'pan' | 'rotate' | 'zoom' = 'pan'
|
||||
|
||||
if (this.syncDirection === 'engineToClient') {
|
||||
throttledEngCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_move',
|
||||
interaction,
|
||||
window: { x: event.clientX, y: event.clientY },
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
if (this.interactionGuards.pan.callback(event as any)) {
|
||||
if (this.enablePan === false) return
|
||||
// handleMouseDownPan(event)
|
||||
state = 'pan'
|
||||
} else if (this.interactionGuards.rotate.callback(event as any)) {
|
||||
if (this.enableRotate === false) return
|
||||
// handleMouseDownRotate(event)
|
||||
state = 'rotate'
|
||||
} else if (this.interactionGuards.zoom.dragCallback(event as any)) {
|
||||
if (this.enableZoom === false) return
|
||||
// handleMouseDownDolly(event)
|
||||
state = 'zoom'
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// Implement camera movement logic here based on deltaMove
|
||||
// For example, for rotating the camera around the target:
|
||||
if (interaction === 'rotate') {
|
||||
if (state === 'rotate') {
|
||||
this.pendingRotation = this.pendingRotation
|
||||
? this.pendingRotation
|
||||
: new Vector2()
|
||||
this.pendingRotation.x += deltaMove.x
|
||||
this.pendingRotation.y += deltaMove.y
|
||||
} else if (interaction === 'zoom') {
|
||||
} else if (state === 'zoom') {
|
||||
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||
this.pendingZoom *= 1 + deltaMove.y * 0.01
|
||||
} else if (interaction === 'pan') {
|
||||
} else if (state === 'pan') {
|
||||
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
|
||||
let distance = this.camera.position.distanceTo(this.target)
|
||||
if (this.camera instanceof OrthographicCamera) {
|
||||
@ -373,52 +297,15 @@ export class CameraControls {
|
||||
|
||||
onMouseUp = (event: MouseEvent) => {
|
||||
this.isDragging = false
|
||||
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) => {
|
||||
// Assume trackpad if the deltas are small and integers
|
||||
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 zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
|
||||
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
|
||||
this.handleEnd()
|
||||
}
|
||||
|
||||
useOrthographicCamera = () => {
|
||||
@ -471,7 +358,7 @@ export class CameraControls {
|
||||
|
||||
return this.camera
|
||||
}
|
||||
_usePerspectiveCamera = () => {
|
||||
usePerspectiveCamera = () => {
|
||||
const { x: px, y: py, z: pz } = this.camera.position
|
||||
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||
const zoom = this.camera.zoom
|
||||
@ -487,17 +374,14 @@ export class CameraControls {
|
||||
)
|
||||
direction.normalize()
|
||||
this.camera.position.copy(this.target).addScaledVector(direction, distance)
|
||||
}
|
||||
usePerspectiveCamera = () => {
|
||||
this._usePerspectiveCamera()
|
||||
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_set_perspective',
|
||||
parameters: {
|
||||
fov_y:
|
||||
this.camera instanceof PerspectiveCamera ? this.camera.fov : 45,
|
||||
fov_y: this.camera.fov,
|
||||
...calculateNearFarFromFOV(this.lastPerspectiveFov),
|
||||
},
|
||||
},
|
||||
@ -570,7 +454,7 @@ export class CameraControls {
|
||||
update = (forceUpdate = false) => {
|
||||
// If there are any changes that need to be applied to the camera, apply them here.
|
||||
|
||||
let didChange = false
|
||||
let didChange = forceUpdate
|
||||
if (this.pendingRotation) {
|
||||
this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y)
|
||||
this.pendingRotation = null // Clear the pending rotation after applying it
|
||||
@ -622,8 +506,8 @@ export class CameraControls {
|
||||
|
||||
// Update the camera's matrices
|
||||
this.camera.updateMatrixWorld()
|
||||
if (didChange || forceUpdate) {
|
||||
this.onCameraChange(forceUpdate)
|
||||
if (didChange) {
|
||||
this.onCameraChange()
|
||||
}
|
||||
|
||||
// damping would be implemented here in update if we choose to add it.
|
||||
@ -734,10 +618,6 @@ export class CameraControls {
|
||||
duration = 500,
|
||||
toOrthographic = true
|
||||
): Promise<void> {
|
||||
if (this.syncDirection === 'engineToClient')
|
||||
console.warn(
|
||||
'tweenCameraToQuaternion not design to work with engineToClient syncDirection.'
|
||||
)
|
||||
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||
let remainingDuration = duration
|
||||
if (isVertical) {
|
||||
@ -820,10 +700,6 @@ export class CameraControls {
|
||||
|
||||
animateToOrthographic = () =>
|
||||
new Promise((resolve) => {
|
||||
if (this.syncDirection === 'engineToClient')
|
||||
console.warn(
|
||||
'animate To Orthographic not design to work with engineToClient syncDirection.'
|
||||
)
|
||||
this.isFovAnimationInProgress = true
|
||||
let currentFov = this.lastPerspectiveFov
|
||||
this.fovBeforeOrtho = currentFov
|
||||
@ -857,10 +733,6 @@ export class CameraControls {
|
||||
})
|
||||
animateToPerspective = () =>
|
||||
new Promise((resolve) => {
|
||||
if (this.syncDirection === 'engineToClient')
|
||||
console.warn(
|
||||
'animate To Perspective not design to work with engineToClient syncDirection.'
|
||||
)
|
||||
this.isFovAnimationInProgress = true
|
||||
// Immediately set the camera to perspective with a very low FOV
|
||||
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
|
||||
@ -899,7 +771,7 @@ export class CameraControls {
|
||||
this.reactCameraPropertiesCallback(a)
|
||||
}, 200)
|
||||
|
||||
onCameraChange = (forceUpdate = false) => {
|
||||
onCameraChange = () => {
|
||||
const distance = this.target.distanceTo(this.camera.position)
|
||||
if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) {
|
||||
this.camera.far = distance * 2
|
||||
@ -907,14 +779,13 @@ export class CameraControls {
|
||||
this.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
if (this.syncDirection === 'clientToEngine' || forceUpdate)
|
||||
throttledUpdateEngineCamera({
|
||||
quaternion: this.camera.quaternion,
|
||||
position: this.camera.position,
|
||||
zoom: this.camera.zoom,
|
||||
isPerspective: this.isPerspective,
|
||||
target: this.target,
|
||||
})
|
||||
throttledUpdateEngineCamera({
|
||||
quaternion: this.camera.quaternion,
|
||||
position: this.camera.position,
|
||||
zoom: this.camera.zoom,
|
||||
isPerspective: this.isPerspective,
|
||||
target: this.target,
|
||||
})
|
||||
this.deferReactUpdate({
|
||||
type: this.isPerspective ? 'perspective' : 'orthographic',
|
||||
[this.isPerspective ? 'fov' : 'zoom']:
|
||||
@ -935,18 +806,9 @@ export class CameraControls {
|
||||
})
|
||||
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
||||
}
|
||||
getInteractionType = (event: any) =>
|
||||
_getInteractionType(
|
||||
this.interactionGuards,
|
||||
event,
|
||||
this.enablePan,
|
||||
this.enableRotate,
|
||||
this.enableZoom
|
||||
)
|
||||
}
|
||||
|
||||
// Pure function helpers
|
||||
|
||||
// currently duplicated, delete one
|
||||
function calculateNearFarFromFOV(fov: number) {
|
||||
const nearFarRatio = (fov - 3) / (45 - 3)
|
||||
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
||||
@ -954,6 +816,7 @@ function calculateNearFarFromFOV(fov: number) {
|
||||
return { z_near: 0.1, z_far }
|
||||
}
|
||||
|
||||
// currently duplicated, delete one
|
||||
function convertThreeCamValuesToEngineCam({
|
||||
target,
|
||||
position,
|
||||
@ -994,6 +857,8 @@ function convertThreeCamValuesToEngineCam({
|
||||
}
|
||||
}
|
||||
|
||||
// Pure function helpers
|
||||
|
||||
function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
|
||||
// Direction from position to target, normalized.
|
||||
let direction = new Vector3().subVectors(target, position).normalize()
|
||||
@ -1012,17 +877,3 @@ function _lookAt(position: Vector3, target: Vector3, up: Vector3): 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,19 +3,17 @@ import {
|
||||
DoubleSide,
|
||||
ExtrudeGeometry,
|
||||
Group,
|
||||
Intersection,
|
||||
LineCurve3,
|
||||
Matrix4,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
Object3DEventMap,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
PlaneGeometry,
|
||||
Quaternion,
|
||||
Scene,
|
||||
Shape,
|
||||
SphereGeometry,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from 'three'
|
||||
@ -27,7 +25,6 @@ import {
|
||||
defaultPlaneColor,
|
||||
getSceneScale,
|
||||
INTERSECTION_PLANE_LAYER,
|
||||
OnMouseEnterLeaveArgs,
|
||||
RAYCASTABLE_PLANE,
|
||||
sceneInfra,
|
||||
SKETCH_GROUP_SEGMENTS,
|
||||
@ -60,7 +57,6 @@ import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import {
|
||||
createArcGeometry,
|
||||
dashedStraight,
|
||||
profileStart,
|
||||
straightSegment,
|
||||
tangentialArcToSegment,
|
||||
} from './segments'
|
||||
@ -68,7 +64,7 @@ import {
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
changeSketchArguments,
|
||||
updateStartProfileAtArgs,
|
||||
compareVec2Epsilon2,
|
||||
} from 'lang/std/sketch'
|
||||
import { isReducedMotion, throttle } from 'lib/utils'
|
||||
import {
|
||||
@ -90,7 +86,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_DASH =
|
||||
'tangential-arc-to-segment-body-dashed'
|
||||
export const PROFILE_START = 'profile-start'
|
||||
export const EXTRA_SEGMENT_HANDLE = 'extraSegmentHandle'
|
||||
|
||||
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
||||
// That mostly mean sketch elements.
|
||||
@ -112,10 +108,9 @@ class SceneEntities {
|
||||
|
||||
Object.values(this.activeSegments).forEach((segment) => {
|
||||
const factor =
|
||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camControls.camera, segment)) /
|
||||
sceneInfra._baseUnitMultiplier
|
||||
: perspScale(sceneInfra.camControls.camera, segment)
|
||||
if (
|
||||
segment.userData.from &&
|
||||
segment.userData.to &&
|
||||
@ -143,9 +138,6 @@ class SceneEntities {
|
||||
scale: factor,
|
||||
})
|
||||
}
|
||||
if (segment.name === PROFILE_START) {
|
||||
segment.scale.set(factor, factor, factor)
|
||||
}
|
||||
})
|
||||
if (this.axisGroup) {
|
||||
const factor =
|
||||
@ -153,9 +145,9 @@ class SceneEntities {
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
|
||||
const x = this.axisGroup.getObjectByName(X_AXIS)
|
||||
x?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
|
||||
x?.scale.set(1, factor, 1)
|
||||
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
||||
y?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
|
||||
y?.scale.set(factor, 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +171,6 @@ class SceneEntities {
|
||||
this.scene.add(this.intersectionPlane)
|
||||
}
|
||||
createSketchAxis(sketchPathToNode: PathToNode) {
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
const baseXColor = 0x000055
|
||||
const baseYColor = 0x550000
|
||||
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
|
||||
@ -219,14 +210,6 @@ class SceneEntities {
|
||||
sceneInfra.camControls.target
|
||||
)
|
||||
gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
|
||||
|
||||
const factor =
|
||||
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
|
||||
xAxisMesh?.scale.set(1, factor / sceneInfra._baseUnitMultiplier, 1)
|
||||
yAxisMesh?.scale.set(factor / sceneInfra._baseUnitMultiplier, 1, 1)
|
||||
|
||||
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
|
||||
this.currentSketchQuaternion &&
|
||||
this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
|
||||
@ -258,12 +241,16 @@ class SceneEntities {
|
||||
ast,
|
||||
// is draft line assumes the last segment is a draft line, and mods it as the user moves the mouse
|
||||
draftSegment,
|
||||
skipListeners,
|
||||
}: {
|
||||
sketchPathToNode: PathToNode
|
||||
ast?: Program
|
||||
draftSegment?: DraftSegment
|
||||
skipListeners?: boolean
|
||||
}) {
|
||||
sceneInfra.resetMouseListeners()
|
||||
if (!skipListeners) {
|
||||
sceneInfra.resetMouseListeners()
|
||||
}
|
||||
this.createIntersectionPlane()
|
||||
|
||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||
@ -298,55 +285,17 @@ class SceneEntities {
|
||||
)
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
const factor =
|
||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camControls.camera, dummy)) /
|
||||
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
|
||||
|
||||
: perspScale(sceneInfra.camControls.camera, dummy)
|
||||
sketchGroup.value.forEach((segment, index) => {
|
||||
let segPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
draftSegment ? truncatedAst : kclManager.ast,
|
||||
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 =
|
||||
draftSegment && index === sketchGroup.value.length - 1
|
||||
let seg
|
||||
const callExpName = getNodeFromPath<CallExpression>(
|
||||
kclManager.ast,
|
||||
segPathToNode,
|
||||
'CallExpression'
|
||||
)?.node?.callee?.name
|
||||
if (segment.type === 'TangentialArcTo') {
|
||||
seg = tangentialArcToSegment({
|
||||
prevSegment: sketchGroup.value[index - 1],
|
||||
@ -365,7 +314,6 @@ class SceneEntities {
|
||||
pathToNode: segPathToNode,
|
||||
isDraftSegment,
|
||||
scale: factor,
|
||||
callExpName,
|
||||
})
|
||||
}
|
||||
seg.layers.set(SKETCH_LAYER)
|
||||
@ -384,22 +332,69 @@ class SceneEntities {
|
||||
this.currentSketchQuaternion
|
||||
)
|
||||
|
||||
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
||||
|
||||
this.scene.add(group)
|
||||
if (!draftSegment) {
|
||||
if (!draftSegment && !skipListeners) {
|
||||
sceneInfra.setCallbacks({
|
||||
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
|
||||
if (mouseEvent.which !== 1) return
|
||||
onDragEnd: async () => {
|
||||
if (addingNewSegmentStatus !== 'nothing') {
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
this.setupSketch({ sketchPathToNode })
|
||||
}
|
||||
},
|
||||
onDrag: async (args) => {
|
||||
if (args.event.which !== 1) return
|
||||
const group = getParentGroup(args.object, [EXTRA_SEGMENT_HANDLE])
|
||||
if (group?.name === EXTRA_SEGMENT_HANDLE) {
|
||||
const segGroup = getParentGroup(args.object)
|
||||
const pathToNode: PathToNode = segGroup?.userData?.pathToNode
|
||||
const pathToNodeIndex = pathToNode.findIndex(
|
||||
(x) => x[1] === 'PipeExpression'
|
||||
)
|
||||
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
|
||||
if (addingNewSegmentStatus === 'nothing') {
|
||||
const prevSegment = sketchGroup.value[pipeIndex - 2]
|
||||
const yo = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [args.intersection2d.x, args.intersection2d.y],
|
||||
from: [prevSegment.from[0], prevSegment.from[1]],
|
||||
fnName:
|
||||
prevSegment.type === 'TangentialArcTo'
|
||||
? 'tangentialArcTo'
|
||||
: 'line',
|
||||
pathToNode: pathToNode,
|
||||
})
|
||||
addingNewSegmentStatus = 'pending'
|
||||
await kclManager.executeAstMock(yo.modifiedAst, {
|
||||
updates: 'code',
|
||||
})
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
this.setupSketch({ sketchPathToNode, skipListeners: true })
|
||||
addingNewSegmentStatus = 'added'
|
||||
} else if (addingNewSegmentStatus === 'added') {
|
||||
const pathToNodeForNewSegment = pathToNode.slice(
|
||||
0,
|
||||
pathToNodeIndex
|
||||
)
|
||||
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
|
||||
this.onDragSegment({
|
||||
...args,
|
||||
sketchPathToNode: pathToNodeForNewSegment,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
this.onDragSegment({
|
||||
object: selected,
|
||||
intersection2d: intersectionPoint.twoD,
|
||||
intersects,
|
||||
...args,
|
||||
sketchPathToNode,
|
||||
})
|
||||
},
|
||||
onMove: () => {},
|
||||
onClick: (args) => {
|
||||
if (args?.mouseEvent.which !== 1) return
|
||||
if (!args || !args.selected) {
|
||||
if (args?.event.which !== 1) return
|
||||
if (!args || !args.object) {
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
@ -408,32 +403,73 @@ class SceneEntities {
|
||||
})
|
||||
return
|
||||
}
|
||||
const { selected } = args
|
||||
const event = getEventForSegmentSelection(selected)
|
||||
const { object } = args
|
||||
const event = getEventForSegmentSelection(object)
|
||||
if (!event) return
|
||||
sceneInfra.modelingSend(event)
|
||||
},
|
||||
...mouseEnterLeaveCallbacks(),
|
||||
onMouseEnter: ({ object }) => {
|
||||
// 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 if (draftSegment && !skipListeners) {
|
||||
sceneInfra.setCallbacks({
|
||||
onDrag: () => {},
|
||||
onClick: async (args) => {
|
||||
if (!args) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
const { intersectionPoint } = args
|
||||
let intersection2d = intersectionPoint?.twoD
|
||||
const profileStart = args.intersects
|
||||
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
||||
.find((a) => a?.name === PROFILE_START)
|
||||
if (args.event.which !== 1) return
|
||||
const { intersection2d } = args
|
||||
if (!intersection2d) return
|
||||
|
||||
const firstSeg = sketchGroup.value[0]
|
||||
const isClosingSketch = compareVec2Epsilon2(
|
||||
firstSeg.from,
|
||||
[intersection2d.x, intersection2d.y],
|
||||
1
|
||||
)
|
||||
let modifiedAst
|
||||
if (profileStart) {
|
||||
if (isClosingSketch) {
|
||||
// TODO close needs a better UX
|
||||
modifiedAst = addCloseToPipe({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
pathToNode: sketchPathToNode,
|
||||
})
|
||||
} else if (intersection2d) {
|
||||
} else {
|
||||
const lastSegment = sketchGroup.value.slice(-1)[0]
|
||||
modifiedAst = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
@ -446,9 +482,6 @@ class SceneEntities {
|
||||
: 'line',
|
||||
pathToNode: sketchPathToNode,
|
||||
}).modifiedAst
|
||||
} else {
|
||||
// return early as we didn't modify the ast
|
||||
return
|
||||
}
|
||||
|
||||
kclManager.executeAstMock(modifiedAst, { updates: 'code' })
|
||||
@ -457,9 +490,8 @@ class SceneEntities {
|
||||
},
|
||||
onMove: (args) => {
|
||||
this.onDragSegment({
|
||||
intersection2d: args.intersectionPoint.twoD,
|
||||
...args,
|
||||
object: Object.values(this.activeSegments).slice(-1)[0],
|
||||
intersects: args.intersects,
|
||||
sketchPathToNode,
|
||||
draftInfo: {
|
||||
draftSegment,
|
||||
@ -469,7 +501,6 @@ class SceneEntities {
|
||||
},
|
||||
})
|
||||
},
|
||||
...mouseEnterLeaveCallbacks(),
|
||||
})
|
||||
}
|
||||
sceneInfra.camControls.enableRotate = false
|
||||
@ -506,15 +537,17 @@ class SceneEntities {
|
||||
)
|
||||
onDragSegment({
|
||||
object,
|
||||
intersection2d: _intersection2d,
|
||||
event,
|
||||
intersectPoint,
|
||||
intersection2d,
|
||||
sketchPathToNode,
|
||||
draftInfo,
|
||||
intersects,
|
||||
}: {
|
||||
object: any
|
||||
event: any
|
||||
intersectPoint: Vector3
|
||||
intersection2d: Vector2
|
||||
sketchPathToNode: PathToNode
|
||||
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
||||
draftInfo?: {
|
||||
draftSegment: DraftSegment
|
||||
truncatedAst: Program
|
||||
@ -522,20 +555,8 @@ class SceneEntities {
|
||||
variableDeclarationName: string
|
||||
}
|
||||
}) {
|
||||
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 (object.name === STRAIGHT_SEGMENT_BODY) return
|
||||
const group = getParentGroup(object)
|
||||
if (!group) return
|
||||
const pathToNode: PathToNode = JSON.parse(
|
||||
JSON.stringify(group.userData.pathToNode)
|
||||
@ -559,28 +580,13 @@ class SceneEntities {
|
||||
).node
|
||||
if (node.type !== 'CallExpression') return
|
||||
|
||||
let modded: {
|
||||
modifiedAst: Program
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
if (group.name === PROFILE_START) {
|
||||
modded = updateStartProfileAtArgs({
|
||||
node: modifiedAst,
|
||||
pathToNode,
|
||||
to,
|
||||
from,
|
||||
previousProgramMemory: kclManager.programMemory,
|
||||
})
|
||||
} else {
|
||||
modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
to,
|
||||
from
|
||||
)
|
||||
}
|
||||
|
||||
const modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
to,
|
||||
from
|
||||
)
|
||||
modifiedAst = modded.modifiedAst
|
||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||
draftInfo
|
||||
@ -599,16 +605,10 @@ class SceneEntities {
|
||||
programMemoryOverride,
|
||||
})
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = programMemory.root[
|
||||
variableDeclarationName
|
||||
] as SketchGroup
|
||||
const sgPaths = sketchGroup.value
|
||||
const sketchGroup = programMemory.root[variableDeclarationName]
|
||||
.value as Path[]
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
const updateSegment = (
|
||||
segment: Path | SketchGroup['start'],
|
||||
index: number
|
||||
) => {
|
||||
sketchGroup.forEach((segment, index) => {
|
||||
const segPathToNode = getNodePathFromSourceRange(
|
||||
modifiedAst,
|
||||
segment.__geoMeta.sourceRange
|
||||
@ -623,13 +623,12 @@ class SceneEntities {
|
||||
// const prevSegment = sketchGroup.slice(index - 1)[0]
|
||||
const type = group?.userData?.type
|
||||
const factor =
|
||||
(sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
sceneInfra.camControls.camera instanceof OrthographicCamera
|
||||
? orthoFactor
|
||||
: perspScale(sceneInfra.camControls.camera, group)) /
|
||||
sceneInfra._baseUnitMultiplier
|
||||
: perspScale(sceneInfra.camControls.camera, group)
|
||||
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
||||
this.updateTangentialArcToSegment({
|
||||
prevSegment: sgPaths[index - 1],
|
||||
prevSegment: sketchGroup[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
group: group,
|
||||
@ -642,13 +641,8 @@ class SceneEntities {
|
||||
group: group,
|
||||
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)
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
@ -745,18 +739,42 @@ class SceneEntities {
|
||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
|
||||
if (arrowGroup) {
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
|
||||
const dir = new Vector3()
|
||||
.subVectors(
|
||||
new Vector3(to[0], to[1], 0),
|
||||
new Vector3(from[0], from[1], 0)
|
||||
)
|
||||
.normalize()
|
||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
}
|
||||
const dir = new Vector3()
|
||||
.subVectors(
|
||||
new Vector3(to[0], to[1], 0),
|
||||
new Vector3(from[0], from[1], 0)
|
||||
)
|
||||
.normalize()
|
||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
|
||||
// TODO this should be created in setupSketch, not updateStraightSegment
|
||||
// it should only be updated here
|
||||
const extraSegmentHandle = (group.getObjectByName(EXTRA_SEGMENT_HANDLE) ||
|
||||
(() => {
|
||||
const mat = new MeshBasicMaterial({ color: 0xffffff })
|
||||
const sphereMesh = new Mesh(new SphereGeometry(0.6, 12, 12), mat)
|
||||
|
||||
const handleGroup = new Group()
|
||||
handleGroup.userData.type = EXTRA_SEGMENT_HANDLE
|
||||
handleGroup.name = EXTRA_SEGMENT_HANDLE
|
||||
handleGroup.add(sphereMesh)
|
||||
handleGroup.layers.set(SKETCH_LAYER)
|
||||
handleGroup.traverse((child) => {
|
||||
child.layers.set(SKETCH_LAYER)
|
||||
})
|
||||
return handleGroup
|
||||
})()) as Group
|
||||
|
||||
extraSegmentHandle.position.set(
|
||||
from[0] + 0.08 * (to[0] - from[0]),
|
||||
from[1] + 0.08 * (to[1] - from[1]),
|
||||
0
|
||||
)
|
||||
extraSegmentHandle.scale.set(scale, scale, scale)
|
||||
group.add(extraSegmentHandle)
|
||||
|
||||
const straightSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY
|
||||
@ -841,24 +859,22 @@ class SceneEntities {
|
||||
}
|
||||
setupDefaultPlaneHover() {
|
||||
sceneInfra.setCallbacks({
|
||||
onMouseEnter: ({ selected }) => {
|
||||
if (!(selected instanceof Mesh && selected.parent)) return
|
||||
if (selected.parent.userData.type !== DEFAULT_PLANES) return
|
||||
const type: DefaultPlane = selected.userData.type
|
||||
selected.material.color = defaultPlaneColor(type, 0.5, 1)
|
||||
onMouseEnter: ({ object }) => {
|
||||
if (object.parent.userData.type !== DEFAULT_PLANES) return
|
||||
const type: DefaultPlane = object.userData.type
|
||||
object.material.color = defaultPlaneColor(type, 0.5, 1)
|
||||
},
|
||||
onMouseLeave: ({ selected }) => {
|
||||
if (!(selected instanceof Mesh && selected.parent)) return
|
||||
if (selected.parent.userData.type !== DEFAULT_PLANES) return
|
||||
const type: DefaultPlane = selected.userData.type
|
||||
selected.material.color = defaultPlaneColor(type)
|
||||
onMouseLeave: ({ object }) => {
|
||||
if (object.parent.userData.type !== DEFAULT_PLANES) return
|
||||
const type: DefaultPlane = object.userData.type
|
||||
object.material.color = defaultPlaneColor(type)
|
||||
},
|
||||
onClick: (args) => {
|
||||
if (!args || !args.intersects?.[0]) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
const { intersects } = args
|
||||
const type = intersects?.[0].object.name || ''
|
||||
const posNorm = Number(intersects?.[0]?.normal?.z) > 0
|
||||
if (!args || !args.object) return
|
||||
if (args.event.which !== 1) return
|
||||
const { intersection } = args
|
||||
const type = intersection.object.name || ''
|
||||
const posNorm = Number(intersection.normal?.z) > 0
|
||||
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
|
||||
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]
|
||||
if (type === YZ_PLANE) {
|
||||
@ -1030,9 +1046,9 @@ export function quaternionFromSketchGroup(
|
||||
}
|
||||
|
||||
function colorSegment(object: any, color: number) {
|
||||
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
|
||||
if (segmentHead) {
|
||||
segmentHead.traverse((child) => {
|
||||
const arrowHead = getParentGroup(object, [ARROWHEAD])
|
||||
if (arrowHead) {
|
||||
arrowHead.traverse((child) => {
|
||||
if (child instanceof Mesh) {
|
||||
child.material.color.set(color)
|
||||
}
|
||||
@ -1088,53 +1104,3 @@ function massageFormats(a: any): Vector3 {
|
||||
? new Vector3(a[0], a[1], a[2])
|
||||
: 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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
Intersection,
|
||||
Object3D,
|
||||
Object3DEventMap,
|
||||
BoxGeometry,
|
||||
} from 'three'
|
||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
@ -48,36 +49,31 @@ export const AXIS_GROUP = 'axisGroup'
|
||||
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||
export const ARROWHEAD = 'arrowhead'
|
||||
|
||||
export interface OnMouseEnterLeaveArgs {
|
||||
selected: Object3D<Object3DEventMap>
|
||||
mouseEvent: MouseEvent
|
||||
interface BaseCallbackArgs2 {
|
||||
object: any
|
||||
event: any
|
||||
}
|
||||
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 OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
|
||||
intersectionPoint: {
|
||||
twoD: Vector2
|
||||
threeD: Vector3
|
||||
}
|
||||
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>
|
||||
interface onMoveCallbackArgs {
|
||||
event: any
|
||||
intersection2d: Vector2
|
||||
intersectPoint: Vector3
|
||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||
}
|
||||
|
||||
// This singleton class is responsible for all of the under the hood setup for the client side scene.
|
||||
@ -92,20 +88,25 @@ class SceneInfra {
|
||||
fov = 45
|
||||
fovBeforeAnimate = 45
|
||||
isFovAnimationInProgress = false
|
||||
_baseUnit: BaseUnit = 'mm'
|
||||
_baseUnitMultiplier = 1
|
||||
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
|
||||
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
|
||||
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
|
||||
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||
onMouseEnter: (arg: BaseCallbackArgs2) => void = () => {}
|
||||
onMouseLeave: (arg: BaseCallbackArgs2) => void = () => {}
|
||||
setCallbacks = (callbacks: {
|
||||
onDragStart?: (arg: OnDragCallbackArgs) => void
|
||||
onDragEnd?: (arg: OnDragCallbackArgs) => void
|
||||
onDrag?: (arg: OnDragCallbackArgs) => void
|
||||
onMove?: (arg: OnMoveCallbackArgs) => void
|
||||
onMove?: (arg: onMoveCallbackArgs) => void
|
||||
onClick?: (arg?: OnClickCallbackArgs) => void
|
||||
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
|
||||
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
|
||||
onMouseEnter?: (arg: BaseCallbackArgs2) => void
|
||||
onMouseLeave?: (arg: BaseCallbackArgs2) => void
|
||||
}) => {
|
||||
console.trace('setting callbacks')
|
||||
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
|
||||
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback
|
||||
this.onDragCallback = callbacks.onDrag || this.onDragCallback
|
||||
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
||||
this.onClickCallback = callbacks.onClick || this.onClickCallback
|
||||
@ -113,17 +114,10 @@ class SceneInfra {
|
||||
this.onMouseLeave = callbacks.onMouseLeave || this.onMouseLeave
|
||||
this.selected = null // following selections between callbacks being set is too tricky
|
||||
}
|
||||
set baseUnit(unit: BaseUnit) {
|
||||
this._baseUnit = unit
|
||||
this._baseUnitMultiplier = baseUnitTomm(unit)
|
||||
this.scene.scale.set(
|
||||
this._baseUnitMultiplier,
|
||||
this._baseUnitMultiplier,
|
||||
this._baseUnitMultiplier
|
||||
)
|
||||
}
|
||||
resetMouseListeners = () => {
|
||||
sceneInfra.setCallbacks({
|
||||
onDragStart: () => {},
|
||||
onDragEnd: () => {},
|
||||
onDrag: () => {},
|
||||
onMove: () => {},
|
||||
onClick: () => {},
|
||||
@ -147,9 +141,10 @@ class SceneInfra {
|
||||
currentMouseVector = new Vector2()
|
||||
selected: {
|
||||
mouseDownVector: Vector2
|
||||
object: Object3D<Object3DEventMap>
|
||||
object: any
|
||||
hasBeenDragged: boolean
|
||||
} | null = null
|
||||
selectedObject: null | any = null
|
||||
mouseDownVector: null | Vector2 = null
|
||||
|
||||
constructor() {
|
||||
@ -216,12 +211,7 @@ class SceneInfra {
|
||||
const axisGroup = this.scene
|
||||
.getObjectByName(AXIS_GROUP)
|
||||
?.getObjectByName('gridHelper')
|
||||
planesGroup &&
|
||||
planesGroup.scale.set(
|
||||
scale / sceneInfra._baseUnitMultiplier,
|
||||
scale / sceneInfra._baseUnitMultiplier,
|
||||
scale / sceneInfra._baseUnitMultiplier
|
||||
)
|
||||
planesGroup && planesGroup.scale.set(scale, scale, scale)
|
||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||
}
|
||||
|
||||
@ -246,8 +236,8 @@ class SceneInfra {
|
||||
// Dispose of any other resources like geometries, materials, textures
|
||||
}
|
||||
getPlaneIntersectPoint = (): {
|
||||
twoD?: Vector2
|
||||
threeD?: Vector3
|
||||
intersection2d?: Vector2
|
||||
intersectPoint: Vector3
|
||||
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||
} | null => {
|
||||
this.planeRaycaster.setFromCamera(
|
||||
@ -258,11 +248,23 @@ class SceneInfra {
|
||||
this.scene.children,
|
||||
true
|
||||
)
|
||||
const recastablePlaneIntersect = planeIntersects.find(
|
||||
(intersect) => intersect.object.name === RAYCASTABLE_PLANE
|
||||
if (
|
||||
planeIntersects.length > 0 &&
|
||||
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
|
||||
)
|
||||
)
|
||||
if (!planeIntersects.length) return null
|
||||
if (!recastablePlaneIntersect) return { intersection: planeIntersects[0] }
|
||||
return null
|
||||
const planePosition = planeIntersects[0].object.position
|
||||
const inversePlaneQuaternion = planeIntersects[0].object.quaternion
|
||||
.clone()
|
||||
@ -277,21 +279,16 @@ class SceneInfra {
|
||||
}
|
||||
|
||||
return {
|
||||
twoD: new Vector2(
|
||||
transformedPoint.x / this._baseUnitMultiplier,
|
||||
transformedPoint.y / this._baseUnitMultiplier
|
||||
), // z should be 0
|
||||
threeD: intersectPoint.divideScalar(this._baseUnitMultiplier),
|
||||
intersection2d: new Vector2(transformedPoint.x, transformedPoint.y), // z should be 0
|
||||
intersectPoint,
|
||||
intersection: planeIntersects[0],
|
||||
}
|
||||
}
|
||||
onMouseMove = (mouseEvent: MouseEvent) => {
|
||||
this.currentMouseVector.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
|
||||
this.currentMouseVector.y =
|
||||
-(mouseEvent.clientY / window.innerHeight) * 2 + 1
|
||||
onMouseMove = (event: MouseEvent) => {
|
||||
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
||||
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||
|
||||
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
||||
const intersects = this.raycastRing()
|
||||
|
||||
if (this.selected) {
|
||||
const hasBeenDragged = !compareVec2Epsilon2(
|
||||
@ -307,56 +304,47 @@ class SceneInfra {
|
||||
if (
|
||||
hasBeenDragged &&
|
||||
planeIntersectPoint &&
|
||||
planeIntersectPoint.twoD &&
|
||||
planeIntersectPoint.threeD
|
||||
planeIntersectPoint.intersection2d
|
||||
) {
|
||||
// // console.log('onDrag', this.selected)
|
||||
|
||||
this.onDragCallback({
|
||||
mouseEvent,
|
||||
intersectionPoint: {
|
||||
twoD: planeIntersectPoint.twoD,
|
||||
threeD: planeIntersectPoint.threeD,
|
||||
},
|
||||
intersects,
|
||||
selected: this.selected.object,
|
||||
object: this.selected.object,
|
||||
event,
|
||||
intersection2d: planeIntersectPoint.intersection2d,
|
||||
...planeIntersectPoint,
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
planeIntersectPoint &&
|
||||
planeIntersectPoint.twoD &&
|
||||
planeIntersectPoint.threeD
|
||||
) {
|
||||
} else if (planeIntersectPoint && planeIntersectPoint.intersection2d) {
|
||||
this.onMoveCallback({
|
||||
mouseEvent,
|
||||
intersectionPoint: {
|
||||
twoD: planeIntersectPoint.twoD,
|
||||
threeD: planeIntersectPoint.threeD,
|
||||
},
|
||||
intersects,
|
||||
event,
|
||||
intersection2d: planeIntersectPoint.intersection2d,
|
||||
...planeIntersectPoint,
|
||||
})
|
||||
}
|
||||
|
||||
if (intersects[0]) {
|
||||
const firstIntersectObject = intersects[0].object
|
||||
const intersect = this.raycastRing()
|
||||
|
||||
if (intersect) {
|
||||
const firstIntersectObject = intersect.object
|
||||
if (this.hoveredObject !== firstIntersectObject) {
|
||||
if (this.hoveredObject) {
|
||||
this.onMouseLeave({
|
||||
selected: this.hoveredObject,
|
||||
mouseEvent: mouseEvent,
|
||||
object: this.hoveredObject,
|
||||
event,
|
||||
})
|
||||
}
|
||||
this.hoveredObject = firstIntersectObject
|
||||
this.onMouseEnter({
|
||||
selected: this.hoveredObject,
|
||||
mouseEvent: mouseEvent,
|
||||
object: this.hoveredObject,
|
||||
event,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (this.hoveredObject) {
|
||||
this.onMouseLeave({
|
||||
selected: this.hoveredObject,
|
||||
mouseEvent: mouseEvent,
|
||||
object: this.hoveredObject,
|
||||
event,
|
||||
})
|
||||
this.hoveredObject = null
|
||||
}
|
||||
@ -366,38 +354,41 @@ class SceneInfra {
|
||||
raycastRing = (
|
||||
pixelRadius = 8,
|
||||
rayRingCount = 32
|
||||
): Intersection<Object3D<Object3DEventMap>>[] => {
|
||||
): Intersection<Object3D<Object3DEventMap>> | undefined => {
|
||||
const mouseDownVector = this.currentMouseVector.clone()
|
||||
const intersectionsMap = new Map<
|
||||
Object3D,
|
||||
Intersection<Object3D<Object3DEventMap>>
|
||||
>()
|
||||
let closestIntersection:
|
||||
| Intersection<Object3D<Object3DEventMap>>
|
||||
| undefined = undefined
|
||||
let closestDistance = Infinity
|
||||
|
||||
const updateIntersectionsMap = (
|
||||
const updateClosestIntersection = (
|
||||
intersections: Intersection<Object3D<Object3DEventMap>>[]
|
||||
) => {
|
||||
intersections.forEach((intersection) => {
|
||||
if (intersection.object.type !== 'GridHelper') {
|
||||
const existingIntersection = intersectionsMap.get(intersection.object)
|
||||
if (
|
||||
!existingIntersection ||
|
||||
existingIntersection.distance > intersection.distance
|
||||
) {
|
||||
intersectionsMap.set(intersection.object, intersection)
|
||||
}
|
||||
let intersection = null
|
||||
for (let i = 0; i < intersections.length; i++) {
|
||||
if (intersections[i].object.type !== 'GridHelper') {
|
||||
intersection = intersections[i]
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!intersection) return
|
||||
|
||||
if (intersection.distance < closestDistance) {
|
||||
closestDistance = intersection.distance
|
||||
closestIntersection = intersection
|
||||
}
|
||||
}
|
||||
|
||||
// Check the center point
|
||||
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
|
||||
updateIntersectionsMap(
|
||||
updateClosestIntersection(
|
||||
this.raycaster.intersectObjects(this.scene.children, true)
|
||||
)
|
||||
|
||||
// Check the ring points
|
||||
for (let i = 0; i < rayRingCount; i++) {
|
||||
const angle = (i / rayRingCount) * Math.PI * 2
|
||||
|
||||
const offsetX = ((pixelRadius * Math.cos(angle)) / window.innerWidth) * 2
|
||||
const offsetY = ((pixelRadius * Math.sin(angle)) / window.innerHeight) * 2
|
||||
const ringVector = new Vector2(
|
||||
@ -405,15 +396,11 @@ class SceneInfra {
|
||||
mouseDownVector.y - offsetY
|
||||
)
|
||||
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
|
||||
updateIntersectionsMap(
|
||||
updateClosestIntersection(
|
||||
this.raycaster.intersectObjects(this.scene.children, true)
|
||||
)
|
||||
}
|
||||
|
||||
// Convert the map values to an array and sort by distance
|
||||
return Array.from(intersectionsMap.values()).sort(
|
||||
(a, b) => a.distance - b.distance
|
||||
)
|
||||
return closestIntersection
|
||||
}
|
||||
|
||||
onMouseDown = (event: MouseEvent) => {
|
||||
@ -421,60 +408,50 @@ class SceneInfra {
|
||||
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||
|
||||
const mouseDownVector = this.currentMouseVector.clone()
|
||||
const intersect = this.raycastRing()[0]
|
||||
const intersect = this.raycastRing()
|
||||
|
||||
if (intersect) {
|
||||
const intersectParent = intersect?.object?.parent as Group
|
||||
this.selected = intersectParent.isGroup
|
||||
? {
|
||||
mouseDownVector,
|
||||
object: intersect.object,
|
||||
object: intersect?.object,
|
||||
hasBeenDragged: false,
|
||||
}
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp = (mouseEvent: MouseEvent) => {
|
||||
this.currentMouseVector.x = (mouseEvent.clientX / window.innerWidth) * 2 - 1
|
||||
this.currentMouseVector.y =
|
||||
-(mouseEvent.clientY / window.innerHeight) * 2 + 1
|
||||
onMouseUp = (event: MouseEvent) => {
|
||||
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
||||
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
||||
const intersects = this.raycastRing()
|
||||
|
||||
if (this.selected) {
|
||||
if (this.selected.hasBeenDragged) {
|
||||
// this is where we could fire a onDragEnd event
|
||||
// console.log('onDragEnd', this.selected)
|
||||
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
|
||||
// TODO do the types properly here
|
||||
this.onDragEndCallback({
|
||||
object: this.selected.object,
|
||||
event,
|
||||
intersection2d: planeIntersectPoint?.intersection2d,
|
||||
...planeIntersectPoint,
|
||||
} as any)
|
||||
} else if (planeIntersectPoint) {
|
||||
// fire onClick event as there was no drags
|
||||
this.onClickCallback({
|
||||
mouseEvent,
|
||||
intersectionPoint: {
|
||||
twoD: planeIntersectPoint.twoD,
|
||||
threeD: planeIntersectPoint.threeD,
|
||||
},
|
||||
intersects,
|
||||
selected: this.selected.object,
|
||||
})
|
||||
} else if (planeIntersectPoint) {
|
||||
this.onClickCallback({
|
||||
mouseEvent,
|
||||
intersects,
|
||||
object: this.selected?.object,
|
||||
event,
|
||||
...planeIntersectPoint,
|
||||
})
|
||||
} else {
|
||||
this.onClickCallback()
|
||||
}
|
||||
// Clear the selected state whether it was dragged or not
|
||||
this.selected = null
|
||||
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
|
||||
} else if (planeIntersectPoint) {
|
||||
this.onClickCallback({
|
||||
mouseEvent,
|
||||
intersectionPoint: {
|
||||
twoD: planeIntersectPoint.twoD,
|
||||
threeD: planeIntersectPoint.threeD,
|
||||
},
|
||||
intersects,
|
||||
event,
|
||||
...planeIntersectPoint,
|
||||
})
|
||||
} else {
|
||||
this.onClickCallback()
|
||||
@ -520,11 +497,7 @@ class SceneInfra {
|
||||
this.camControls.camera,
|
||||
this.camControls.target
|
||||
)
|
||||
planesGroup.scale.set(
|
||||
sceneScale / sceneInfra._baseUnitMultiplier,
|
||||
sceneScale / sceneInfra._baseUnitMultiplier,
|
||||
sceneScale / sceneInfra._baseUnitMultiplier
|
||||
)
|
||||
planesGroup.scale.set(sceneScale, sceneScale, sceneScale)
|
||||
this.scene.add(planesGroup)
|
||||
}
|
||||
removeDefaultPlanes() {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Coords2d } from 'lang/std/sketch'
|
||||
import {
|
||||
BoxGeometry,
|
||||
BufferGeometry,
|
||||
CatmullRomCurve3,
|
||||
ConeGeometry,
|
||||
@ -20,7 +19,6 @@ import {
|
||||
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
||||
import {
|
||||
PROFILE_START,
|
||||
STRAIGHT_SEGMENT,
|
||||
STRAIGHT_SEGMENT_BODY,
|
||||
STRAIGHT_SEGMENT_DASH,
|
||||
@ -31,38 +29,6 @@ import {
|
||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
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({
|
||||
from,
|
||||
to,
|
||||
@ -70,7 +36,6 @@ export function straightSegment({
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
callExpName,
|
||||
}: {
|
||||
from: Coords2d
|
||||
to: Coords2d
|
||||
@ -78,7 +43,6 @@ export function straightSegment({
|
||||
pathToNode: PathToNode
|
||||
isDraftSegment?: boolean
|
||||
scale?: number
|
||||
callExpName: string
|
||||
}): Group {
|
||||
const group = new Group()
|
||||
|
||||
@ -102,8 +66,7 @@ export function straightSegment({
|
||||
})
|
||||
}
|
||||
|
||||
const baseColor = callExpName === 'close' ? 0x444444 : 0xffffff
|
||||
const body = new MeshBasicMaterial({ color: baseColor })
|
||||
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
mesh.userData.type = isDraftSegment
|
||||
? STRAIGHT_SEGMENT_DASH
|
||||
@ -117,8 +80,6 @@ export function straightSegment({
|
||||
to,
|
||||
pathToNode,
|
||||
isSelected: false,
|
||||
callExpName,
|
||||
baseColor,
|
||||
}
|
||||
group.name = STRAIGHT_SEGMENT
|
||||
|
||||
@ -129,8 +90,7 @@ export function straightSegment({
|
||||
.normalize()
|
||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||
|
||||
group.add(mesh)
|
||||
if (callExpName !== 'close') group.add(arrowGroup)
|
||||
group.add(mesh, arrowGroup)
|
||||
|
||||
return group
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Combobox } from '@headlessui/react'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CommandArgumentOption } from 'lib/commandTypes'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
function CommandArgOptionInput({
|
||||
options,
|
||||
@ -11,89 +11,51 @@ function CommandArgOptionInput({
|
||||
onSubmit,
|
||||
placeholder,
|
||||
}: {
|
||||
options: (CommandArgument<unknown> & { inputType: 'options' })['options']
|
||||
options: CommandArgumentOption<unknown>[]
|
||||
argName: string
|
||||
stepBack: () => void
|
||||
onSubmit: (data: unknown) => void
|
||||
placeholder?: string
|
||||
}) {
|
||||
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 formRef = useRef<HTMLFormElement>(null)
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
CommandArgumentOption<unknown>
|
||||
>(currentOption || resolvedOptions[0])
|
||||
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 [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
|
||||
options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
|
||||
commandBarState.context.argumentsToSubmit[argName] ||
|
||||
options[0].value
|
||||
)
|
||||
const [query, setQuery] = useState('')
|
||||
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
||||
|
||||
// Reset the query and selected option when the argName changes
|
||||
useEffect(() => {
|
||||
setQuery(initialQuery)
|
||||
setSelectedOption(currentOption || resolvedOptions[0])
|
||||
}, [argName])
|
||||
const fuse = new Fuse(options, {
|
||||
keys: ['name', 'description'],
|
||||
threshold: 0.3,
|
||||
})
|
||||
|
||||
// Auto focus and select the input when the component mounts
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
inputRef.current?.select()
|
||||
}, [inputRef])
|
||||
|
||||
// Filter the options based on the query,
|
||||
// resetting the query when the options change
|
||||
useEffect(() => {
|
||||
const results = fuse.search(query).map((result) => result.item)
|
||||
setFilteredOptions(query.length > 0 ? results : resolvedOptions)
|
||||
}, [query, resolvedOptions, fuse])
|
||||
setFilteredOptions(query.length > 0 ? results : options)
|
||||
}, [query])
|
||||
|
||||
function handleSelectOption(option: CommandArgumentOption<unknown>) {
|
||||
// We deal with the whole option object internally
|
||||
setSelectedOption(option)
|
||||
|
||||
// But we only submit the value
|
||||
setArgValue(option)
|
||||
onSubmit(option.value)
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
|
||||
// We submit the value of the selected option, not the whole object
|
||||
onSubmit(selectedOption.value)
|
||||
onSubmit(argValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
|
||||
<Combobox
|
||||
value={selectedOption}
|
||||
onChange={handleSelectOption}
|
||||
name="options"
|
||||
>
|
||||
<Combobox value={argValue} onChange={handleSelectOption} name="options">
|
||||
<div className="flex items-center mx-4 mt-4 mb-2">
|
||||
<label
|
||||
htmlFor="option-input"
|
||||
@ -113,12 +75,10 @@ function CommandArgOptionInput({
|
||||
stepBack()
|
||||
}
|
||||
}}
|
||||
value={query}
|
||||
placeholder={
|
||||
currentOption?.name ||
|
||||
(argValue as CommandArgumentOption<unknown>)?.name ||
|
||||
placeholder ||
|
||||
argName ||
|
||||
'Select an option'
|
||||
'Select an option for ' + argName
|
||||
}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
@ -138,7 +98,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"
|
||||
>
|
||||
<p className="flex-grow">{option.name} </p>
|
||||
{option.value === currentOption?.value && (
|
||||
{'isCurrent' in option && option.isCurrent && (
|
||||
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
||||
current
|
||||
</small>
|
||||
|
@ -29,6 +29,12 @@ export const CommandBarProvider = ({
|
||||
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
||||
devTools: true,
|
||||
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) => {
|
||||
return (
|
||||
!context.selectedCommand?.args ||
|
||||
@ -75,12 +81,7 @@ export const CommandBar = () => {
|
||||
function stepBack() {
|
||||
if (!currentArgument) {
|
||||
if (commandBarState.matches('Review')) {
|
||||
const entries = Object.entries(selectedCommand?.args || {}).filter(
|
||||
([_, argConfig]) =>
|
||||
typeof argConfig.required === 'function'
|
||||
? argConfig.required(commandBarState.context)
|
||||
: argConfig.required
|
||||
)
|
||||
const entries = Object.entries(selectedCommand?.args || {})
|
||||
|
||||
const currentArgName = entries[entries.length - 1][0]
|
||||
const currentArg = {
|
||||
@ -88,12 +89,19 @@ export const CommandBar = () => {
|
||||
...entries[entries.length - 1][1],
|
||||
}
|
||||
|
||||
commandBarSend({
|
||||
type: 'Edit argument',
|
||||
data: {
|
||||
arg: currentArg,
|
||||
},
|
||||
})
|
||||
if (commandBarState.matches('Review')) {
|
||||
commandBarSend({
|
||||
type: 'Edit argument',
|
||||
data: {
|
||||
arg: currentArg,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
commandBarSend({
|
||||
type: 'Remove argument',
|
||||
data: { [currentArgName]: currentArg },
|
||||
})
|
||||
}
|
||||
} else {
|
||||
commandBarSend({ type: 'Deselect command' })
|
||||
}
|
||||
@ -116,6 +124,11 @@ export const CommandBar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => console.log(commandBarState.context.argumentsToSubmit),
|
||||
[commandBarState.context.argumentsToSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={!commandBarState.matches('Closed') || false}
|
||||
|
@ -76,82 +76,72 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
)}
|
||||
{selectedCommand?.name}
|
||||
</p>
|
||||
{Object.entries(selectedCommand?.args || {})
|
||||
.filter(([_, argConfig]) =>
|
||||
typeof argConfig.required === 'function'
|
||||
? argConfig.required(commandBarState.context)
|
||||
: argConfig.required
|
||||
)
|
||||
.map(([argName, arg], i) => {
|
||||
const argValue =
|
||||
(typeof argumentsToSubmit[argName] === 'function'
|
||||
? (argumentsToSubmit[argName] as Function)(
|
||||
commandBarState.context
|
||||
{Object.entries(selectedCommand?.args || {}).map(
|
||||
([argName, arg], i) => (
|
||||
<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>
|
||||
{argumentsToSubmit[argName] ? (
|
||||
arg.inputType === 'selection' ? (
|
||||
getSelectionTypeDisplayText(
|
||||
argumentsToSubmit[argName] as Selections
|
||||
)
|
||||
: 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>
|
||||
) : arg.inputType === 'kcl' ? (
|
||||
roundOff(
|
||||
Number(
|
||||
(argumentsToSubmit[argName] as KclCommandValue)
|
||||
.valueCalculated
|
||||
),
|
||||
4
|
||||
)
|
||||
) : 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>
|
||||
) : 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>
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
|
||||
</div>
|
||||
|
@ -48,8 +48,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||
if (!arg) return
|
||||
})
|
||||
|
||||
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
function submitCommand() {
|
||||
commandBarSend({
|
||||
type: 'Submit command',
|
||||
data: argumentsToSubmit,
|
||||
|
@ -29,7 +29,7 @@ function CommandBarSelectionInput({
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||
const selection = useSelector(arg.actor, selectionSelector)
|
||||
const [selectionsByType, setSelectionsByType] = useState<
|
||||
'none' | ResolvedSelectionType[]
|
||||
>(
|
||||
|
@ -9,7 +9,6 @@ export type CustomIconName =
|
||||
| 'clipboardCheckmark'
|
||||
| 'close'
|
||||
| 'equal'
|
||||
| 'exportFile'
|
||||
| 'extrude'
|
||||
| 'file'
|
||||
| 'filePlus'
|
||||
@ -195,22 +194,6 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</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':
|
||||
return (
|
||||
<svg
|
||||
|
238
src/components/ExportButton.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -18,7 +18,6 @@ import {
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -57,30 +56,27 @@ export const GlobalStateProvider = ({
|
||||
>
|
||||
)
|
||||
|
||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||
settingsMachine,
|
||||
{
|
||||
context: persistedSettings,
|
||||
actions: {
|
||||
toastSuccess: (context, event) => {
|
||||
const truncatedNewValue =
|
||||
'data' in event && event.data instanceof Object
|
||||
? (String(
|
||||
context[Object.keys(event.data)[0] as keyof typeof context]
|
||||
).substring(0, 28) as any)
|
||||
: undefined
|
||||
toast.success(
|
||||
event.type +
|
||||
(truncatedNewValue
|
||||
? ` to "${truncatedNewValue}${
|
||||
truncatedNewValue.length === 28 ? '...' : ''
|
||||
}"`
|
||||
: '')
|
||||
)
|
||||
},
|
||||
const [settingsState, settingsSend] = useMachine(settingsMachine, {
|
||||
context: persistedSettings,
|
||||
actions: {
|
||||
toastSuccess: (context, event) => {
|
||||
const truncatedNewValue =
|
||||
'data' in event && event.data instanceof Object
|
||||
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
||||
.toString()
|
||||
.substring(0, 28) as any)
|
||||
: undefined
|
||||
toast.success(
|
||||
event.type +
|
||||
(truncatedNewValue
|
||||
? ` to "${truncatedNewValue}${
|
||||
truncatedNewValue.length === 28 ? '...' : ''
|
||||
}"`
|
||||
: '')
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
settingsStateRef = settingsState.context
|
||||
|
||||
useStateMachineCommands({
|
||||
@ -88,7 +84,6 @@ export const GlobalStateProvider = ({
|
||||
state: settingsState,
|
||||
send: settingsSend,
|
||||
commandBarConfig: settingsCommandBarConfig,
|
||||
actor: settingsActor,
|
||||
})
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
@ -102,14 +97,13 @@ export const GlobalStateProvider = ({
|
||||
if (settingsState.context.theme !== 'system') return
|
||||
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
|
||||
}
|
||||
sceneInfra.baseUnit = settingsState?.context?.baseUnit || 'mm'
|
||||
|
||||
matcher.addEventListener('change', listener)
|
||||
return () => matcher.removeEventListener('change', listener)
|
||||
}, [settingsState.context])
|
||||
|
||||
// Auth machine setup
|
||||
const [authState, authSend, authActor] = useMachine(authMachine, {
|
||||
const [authState, authSend] = useMachine(authMachine, {
|
||||
actions: {
|
||||
goToSignInPage: () => {
|
||||
navigate(paths.SIGN_IN)
|
||||
@ -129,7 +123,6 @@ export const GlobalStateProvider = ({
|
||||
state: authState,
|
||||
send: authSend,
|
||||
commandBarConfig: authCommandBarConfig,
|
||||
actor: authActor,
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -25,7 +25,8 @@ describe('processMemory', () => {
|
||||
|> lineTo([-3.35, 0.17], %)
|
||||
|> lineTo([0.98, 5.16], %)
|
||||
|> lineTo([2.15, 4.32], %)
|
||||
// |> rx(90, %)`
|
||||
// |> rx(90, %)
|
||||
show(theExtrude, theSketch)`
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {},
|
||||
|
@ -38,10 +38,6 @@ import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
|
||||
import { startSketchOnDefault } from 'lang/modifyAst'
|
||||
import { Program } from 'lang/wasm'
|
||||
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> = {
|
||||
state: StateFrom<T>
|
||||
@ -58,12 +54,7 @@ export const ModelingMachineProvider = ({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const {
|
||||
auth,
|
||||
settings: {
|
||||
context: { baseUnit },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const { auth } = useGlobalStateContext()
|
||||
const { code } = useKclContext()
|
||||
const token = auth?.context?.token
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
@ -179,56 +170,6 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
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: {
|
||||
'has valid extrude selection': ({ selectionRanges }) => {
|
||||
@ -251,8 +192,6 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges
|
||||
)
|
||||
},
|
||||
'Has exportable geometry': () =>
|
||||
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
|
||||
},
|
||||
services: {
|
||||
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
|
||||
|
@ -5,6 +5,7 @@ import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { vi } from 'vitest'
|
||||
import { ExportButtonProps } from './ExportButton'
|
||||
|
||||
const now = new Date()
|
||||
const projectWellFormed = {
|
||||
@ -37,6 +38,15 @@ const projectWellFormed = {
|
||||
},
|
||||
} 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', () => {
|
||||
test('Renders the project name', () => {
|
||||
render(
|
||||
|
@ -5,12 +5,12 @@ import { type IndexLoaderData } from 'lib/types'
|
||||
import { paths } from 'lib/paths'
|
||||
import { isTauri } from '../lib/isTauri'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ExportButton } from './ExportButton'
|
||||
import { Fragment } from 'react'
|
||||
import { FileTree } from './FileTree'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { Logo } from './Logo'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
@ -21,8 +21,6 @@ const ProjectSidebarMenu = ({
|
||||
project?: IndexLoaderData['project']
|
||||
file?: IndexLoaderData['file']
|
||||
}) => {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
|
||||
return renderAsLink ? (
|
||||
<Link
|
||||
to={paths.HOME}
|
||||
@ -114,19 +112,13 @@ const ProjectSidebarMenu = ({
|
||||
<div className="flex-1 overflow-hidden" />
|
||||
)}
|
||||
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
icon={{ icon: 'exportFile', className: 'p-1' }}
|
||||
className="border-transparent dark:border-transparent"
|
||||
onClick={() =>
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Export', ownerMachine: 'modeling' },
|
||||
})
|
||||
}
|
||||
<ExportButton
|
||||
className={{
|
||||
button: 'border-transparent dark:border-transparent',
|
||||
}}
|
||||
>
|
||||
Export Part
|
||||
</ActionButton>
|
||||
Export Model
|
||||
</ExportButton>
|
||||
{isTauri() && (
|
||||
<ActionButton
|
||||
Element="link"
|
||||
|
@ -1,8 +1,15 @@
|
||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
MouseEventHandler,
|
||||
WheelEventHandler,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '../useStore'
|
||||
import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import { getNormalisedCoordinates, throttle } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
@ -29,6 +36,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
streamDimensions: s.streamDimensions,
|
||||
}))
|
||||
const { settings } = useGlobalStateContext()
|
||||
const cameraControls = settings?.context?.cameraControls
|
||||
const { state } = useModelingContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { overallState } = useNetworkStatus()
|
||||
@ -60,6 +68,19 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
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> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
@ -138,6 +159,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
muted
|
||||
autoPlay
|
||||
controls={false}
|
||||
onWheel={handleScroll}
|
||||
onPlay={() => setIsLoading(false)}
|
||||
onMouseMoveCapture={handleMouseMove}
|
||||
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
|
||||
|
@ -27,8 +27,6 @@ describe('UserSidebarMenu tests', () => {
|
||||
phone: '555-555-5555',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
can_train_on_data: false,
|
||||
is_service_account: false,
|
||||
}
|
||||
|
||||
render(
|
||||
@ -59,8 +57,6 @@ describe('UserSidebarMenu tests', () => {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
name: '',
|
||||
can_train_on_data: false,
|
||||
is_service_account: false,
|
||||
}
|
||||
|
||||
render(
|
||||
@ -88,8 +84,6 @@ describe('UserSidebarMenu tests', () => {
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
image: '',
|
||||
can_train_on_data: false,
|
||||
is_service_account: false,
|
||||
}
|
||||
|
||||
render(
|
||||
|
@ -4,7 +4,6 @@ import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
|
||||
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
||||
import { offsetToPos } from 'editor/plugins/lsp/util'
|
||||
import { LanguageServerOptions } from 'editor/plugins/lsp'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import {
|
||||
LanguageServerPlugin,
|
||||
documentUri,
|
||||
@ -41,14 +40,6 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||
if (plugin == null) return null
|
||||
|
||||
const { state, pos, explicit } = context
|
||||
|
||||
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
|
||||
if (
|
||||
nodeBefore.name === 'BlockComment' ||
|
||||
nodeBefore.name === 'LineComment'
|
||||
)
|
||||
return null
|
||||
|
||||
const line = state.doc.lineAt(pos)
|
||||
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
|
||||
let trigChar: string | undefined
|
||||
@ -69,7 +60,6 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return await plugin.requestCompletion(
|
||||
context,
|
||||
offsetToPos(state.doc, pos),
|
||||
|
@ -7,5 +7,6 @@ export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
||||
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
||||
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
||||
export const TEST = import.meta.env.TEST
|
||||
export const DEV = import.meta.env.DEV
|
||||
|
@ -28,7 +28,7 @@ interface UseStateMachineCommandsArgs<
|
||||
machineId: T['id']
|
||||
state: StateFrom<T>
|
||||
send: Function
|
||||
actor: InterpreterFrom<T>
|
||||
actor?: InterpreterFrom<T>
|
||||
commandBarConfig?: CommandSetConfig<T, S>
|
||||
allCommandsRequireNetwork?: boolean
|
||||
onCancel?: () => void
|
||||
|
@ -93,7 +93,7 @@ class KclManager {
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
this._params.id &&
|
||||
writeTextFile(this._params.id, code).catch((err) => {
|
||||
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
})
|
||||
|
@ -11,53 +11,59 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)`
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const sketch001 = programMemory?.root?.mySketch001
|
||||
expect(sketch001).toEqual({
|
||||
type: 'SketchGroup',
|
||||
on: expect.any(Object),
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [46, 71],
|
||||
},
|
||||
},
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
name: '',
|
||||
to: [-1.59, -1.54],
|
||||
const shown = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
(a) => programMemory?.root?.[a.name]
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'SketchGroup',
|
||||
on: expect.any(Object),
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [77, 102],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
to: [0.46, -5.82],
|
||||
from: [-1.59, -1.54],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [108, 132],
|
||||
id: expect.any(String),
|
||||
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] }],
|
||||
})
|
||||
value: [
|
||||
{
|
||||
type: 'ToPoint',
|
||||
name: '',
|
||||
to: [-1.59, -1.54],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [77, 102],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ToPoint',
|
||||
to: [0.46, -5.82],
|
||||
from: [-1.59, -1.54],
|
||||
name: '',
|
||||
__geoMeta: {
|
||||
sourceRange: [108, 132],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
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 () => {
|
||||
// Enable rotations #152
|
||||
@ -67,24 +73,30 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
|> extrude(2, %)`
|
||||
|> extrude(2, %)
|
||||
show(mySketch001)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const sketch001 = programMemory?.root?.mySketch001
|
||||
expect(sketch001).toEqual({
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
endCapId: null,
|
||||
startCapId: null,
|
||||
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] }],
|
||||
})
|
||||
const shown = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
(a) => programMemory?.root?.[a.name]
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0, 1],
|
||||
endCapId: null,
|
||||
startCapId: null,
|
||||
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 () => {
|
||||
// Enable rotations #152
|
||||
@ -108,10 +120,14 @@ const sk2 = startSketchOn('XY')
|
||||
// |> transform(theTransf, %)
|
||||
|> extrude(2, %)
|
||||
|
||||
`
|
||||
|
||||
show(theExtrude, sk2)`
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
// @ts-ignore
|
||||
const geos = [programMemory?.root?.theExtrude, programMemory?.root?.sk2]
|
||||
const geos = programMemory?.return?.map(
|
||||
// @ts-ignore
|
||||
({ name }) => programMemory?.root?.[name]
|
||||
)
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'ExtrudeGroup',
|
||||
|
@ -47,8 +47,9 @@ const newVar = myVar + 1`
|
||||
|> lineTo([2,3], %)
|
||||
|> lineTo({ to: [5,-1], tag: "rightPath" }, %)
|
||||
// |> close(%)
|
||||
show(mySketch)
|
||||
`
|
||||
const { root } = await exe(code)
|
||||
const { root, return: _return } = await exe(code)
|
||||
// geo is three js buffer geometry and is very bloated to have in tests
|
||||
const minusGeo = root.mySketch.value
|
||||
expect(minusGeo).toEqual([
|
||||
@ -83,6 +84,15 @@ const newVar = myVar + 1`
|
||||
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 () => {
|
||||
@ -347,6 +357,7 @@ describe('testing math operators', () => {
|
||||
` -legLen(segLen('seg01', %), myVar)`,
|
||||
`], %)`,
|
||||
``,
|
||||
`show(part001)`,
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
@ -381,7 +392,8 @@ const theExtrude = startSketchOn('XY')
|
||||
|> line([-0.76], myVarZ, %)
|
||||
|> line([5,5], %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)`
|
||||
|> extrude(4, %)
|
||||
show(theExtrude)`
|
||||
await expect(exe(code)).rejects.toEqual(
|
||||
new KCLError(
|
||||
'undefined_value',
|
||||
|
@ -122,6 +122,7 @@ describe('Testing addSketchTo', () => {
|
||||
expect(str).toBe(`const part001 = startSketchOn('YZ')
|
||||
|> startProfileAt('default', %)
|
||||
|> line('default', %)
|
||||
show(part001)
|
||||
`)
|
||||
})
|
||||
})
|
||||
@ -146,7 +147,8 @@ describe('Testing giveSketchFnCallTag', () => {
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([-2.57, -0.13], %)
|
||||
|> 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', () => {
|
||||
const { newCode, tag, isTagExisting } = giveSketchFnCallTagTestHelper(
|
||||
code,
|
||||
@ -202,7 +204,8 @@ const part001 = startSketchOn('XY')
|
||||
|> angledLine([def(yo), 3.09], %)
|
||||
|> angledLine([ghi(%), 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 () => {
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
PipeExpression,
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
ExpressionStatement,
|
||||
Value,
|
||||
Literal,
|
||||
PipeSubstitution,
|
||||
@ -127,8 +128,16 @@ export function addSketchTo(
|
||||
createPipeExpression(pipeBody)
|
||||
)
|
||||
|
||||
_node.body = [...node.body, variableDeclaration]
|
||||
let sketchIndex = _node.body.length - 1
|
||||
const showCallIndex = getShowIndex(_node)
|
||||
let sketchIndex = showCallIndex
|
||||
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 = [
|
||||
['body', ''],
|
||||
[sketchIndex, 'index'],
|
||||
@ -141,7 +150,7 @@ export function addSketchTo(
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
modifiedAst: addToShow(_node, _name),
|
||||
id: _name,
|
||||
pathToNode,
|
||||
}
|
||||
@ -182,6 +191,44 @@ export function findUniqueName(
|
||||
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(
|
||||
node: Value,
|
||||
updateWith: ArrayExpression
|
||||
@ -301,10 +348,15 @@ export function extrudeSketch(
|
||||
}
|
||||
const name = findUniqueName(node, 'part')
|
||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||
_node.body.splice(_node.body.length, 0, VariableDeclaration)
|
||||
let showCallIndex = getShowIndex(_node)
|
||||
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 = [
|
||||
['body', ''],
|
||||
[_node.body.length, 'index'],
|
||||
[showCallIndex, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
@ -313,7 +365,7 @@ export function extrudeSketch(
|
||||
]
|
||||
return {
|
||||
modifiedAst: node,
|
||||
pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']],
|
||||
pathToNode: [...pathToNode.slice(0, -1), [showCallIndex, 'index']],
|
||||
pathToExtrudeArg,
|
||||
}
|
||||
}
|
||||
@ -373,7 +425,7 @@ export function sketchOnExtrudedFace(
|
||||
_node.body.splice(expressionIndex + 1, 0, newSketch)
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
modifiedAst: addToShow(_node, newSketchName),
|
||||
pathToNode: [...pathToNode.slice(0, -1), [expressionIndex, 'index']],
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,8 @@ const part001 = startSketchOn('XY')
|
||||
|> xLine(3.84, %) // selection-range-7ish-before-this
|
||||
|
||||
const variableBelowShouldNotBeIncluded = 3
|
||||
`
|
||||
|
||||
show(part001)`
|
||||
const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
@ -68,7 +69,8 @@ describe('testing argIsNotIdentifier', () => {
|
||||
|> angledLine([ghi(%), 3.09], %)
|
||||
|> angledLine([jkl('yo') + 2, 3.09], %)
|
||||
const yo = 5 + 6
|
||||
const yo2 = hmm([identifierGuy + 5])`
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)`
|
||||
it('find a safe binaryExpression', () => {
|
||||
const ast = parse(code)
|
||||
const rangeStart = code.indexOf('100 + 100') + 2
|
||||
@ -199,7 +201,8 @@ describe('testing getNodePathFromSourceRange', () => {
|
||||
const code = `const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([0.39, -0.05], %)
|
||||
|> 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', () => {
|
||||
const searchLn = `line([0.94, 2.61], %)`
|
||||
const sourceIndex = code.indexOf(searchLn) + searchLn.length
|
||||
|
@ -68,6 +68,8 @@ log(5, myVar)
|
||||
|> lineTo([1, 1], %)
|
||||
|> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|
||||
|> close(%)
|
||||
|
||||
show(mySketch)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
@ -329,6 +331,7 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
|
||||
intersectTag: 'seg01'
|
||||
}, %)
|
||||
|> line([-0.42, -1.72], %)
|
||||
show(part001)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
|
@ -3,6 +3,7 @@ import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
|
||||
@ -289,6 +290,12 @@ class EngineConnection {
|
||||
}
|
||||
}
|
||||
|
||||
// shouldTrace will return true when Sentry should be used to instrument
|
||||
// the Engine.
|
||||
shouldTrace() {
|
||||
return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports
|
||||
}
|
||||
|
||||
// connect will attempt to connect to the Engine over a WebSocket, and
|
||||
// establish the WebRTC connections.
|
||||
//
|
||||
@ -301,6 +308,41 @@ class EngineConnection {
|
||||
|
||||
// Information on the connect transaction
|
||||
|
||||
class SpanPromise {
|
||||
span: Sentry.Span
|
||||
promise: Promise<void>
|
||||
resolve?: (v: void) => void
|
||||
|
||||
constructor(span: Sentry.Span) {
|
||||
this.span = span
|
||||
this.promise = new Promise((resolve) => {
|
||||
this.resolve = (v: void) => {
|
||||
// here we're going to invoke finish before resolving the
|
||||
// promise so that a `.then()` will order strictly after
|
||||
// all spans have -- for sure -- been resolved, rather than
|
||||
// doing a `then` on this promise.
|
||||
this.span.finish()
|
||||
resolve(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let webrtcMediaTransaction: Sentry.Transaction
|
||||
let websocketSpan: SpanPromise
|
||||
let mediaTrackSpan: SpanPromise
|
||||
let dataChannelSpan: SpanPromise
|
||||
let handshakeSpan: SpanPromise
|
||||
let iceSpan: SpanPromise
|
||||
|
||||
const spanStart = (op: string) =>
|
||||
new SpanPromise(webrtcMediaTransaction.startChild({ op }))
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
webrtcMediaTransaction = Sentry.startTransaction({ name: 'webrtc-media' })
|
||||
websocketSpan = spanStart('websocket')
|
||||
}
|
||||
|
||||
const createPeerConnection = () => {
|
||||
this.pc = new RTCPeerConnection()
|
||||
|
||||
@ -351,6 +393,10 @@ class EngineConnection {
|
||||
// From what I understand, only after have we done the ICE song and
|
||||
// dance is it safest to connect the video tracks / stream
|
||||
case 'connected':
|
||||
if (this.shouldTrace()) {
|
||||
iceSpan.resolve?.()
|
||||
}
|
||||
|
||||
// Let the browser attach to the video stream now
|
||||
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
|
||||
break
|
||||
@ -383,6 +429,17 @@ class EngineConnection {
|
||||
},
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
let mediaStreamTrack = mediaStream.getVideoTracks()[0]
|
||||
mediaStreamTrack.addEventListener('unmute', () => {
|
||||
// let settings = mediaStreamTrack.getSettings()
|
||||
// mediaTrackSpan.span.setTag("fps", settings.frameRate)
|
||||
// mediaTrackSpan.span.setTag("width", settings.width)
|
||||
// mediaTrackSpan.span.setTag("height", settings.height)
|
||||
mediaTrackSpan.resolve?.()
|
||||
})
|
||||
}
|
||||
|
||||
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (mediaStream.getVideoTracks().length !== 1) {
|
||||
@ -465,6 +522,10 @@ class EngineConnection {
|
||||
},
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
dataChannelSpan.resolve?.()
|
||||
}
|
||||
|
||||
// Everything is now connected.
|
||||
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
|
||||
|
||||
@ -516,6 +577,27 @@ class EngineConnection {
|
||||
if (this.token) {
|
||||
this.send({ headers: { Authorization: `Bearer ${this.token}` } })
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
websocketSpan.resolve?.()
|
||||
|
||||
handshakeSpan = spanStart('handshake')
|
||||
iceSpan = spanStart('ice')
|
||||
dataChannelSpan = spanStart('data-channel')
|
||||
mediaTrackSpan = spanStart('media-track')
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
void Promise.all([
|
||||
handshakeSpan.promise,
|
||||
iceSpan.promise,
|
||||
dataChannelSpan.promise,
|
||||
mediaTrackSpan.promise,
|
||||
]).then(() => {
|
||||
console.log('All spans finished, reporting')
|
||||
webrtcMediaTransaction?.finish()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.websocket.addEventListener('close', (event) => {
|
||||
@ -704,6 +786,13 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
type: ConnectingType.WebRTCConnecting,
|
||||
},
|
||||
}
|
||||
|
||||
if (this.shouldTrace()) {
|
||||
// When both ends have a local and remote SDP, we've been able to
|
||||
// set up successfully. We'll still need to find the right ICE
|
||||
// servers, but this is hand-shook.
|
||||
handshakeSpan.resolve?.()
|
||||
}
|
||||
break
|
||||
|
||||
case 'trickle_ice':
|
||||
@ -796,7 +885,7 @@ interface UnreliableSubscription<T extends UnreliableResponses['type']> {
|
||||
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
||||
}
|
||||
|
||||
export interface Subscription<T extends ModelTypes> {
|
||||
interface Subscription<T extends ModelTypes> {
|
||||
event: T
|
||||
callback: (
|
||||
data: Extract<Models['OkModelingCmdResponse_type'], { type: T }>
|
||||
@ -926,15 +1015,6 @@ export class EngineCommandManager {
|
||||
},
|
||||
})
|
||||
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.resolveReady()
|
||||
|
@ -101,6 +101,7 @@ describe('testing changeSketchArguments', () => {
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(lineToChange)
|
||||
const expectedCode = genCode(lineAfterChange)
|
||||
@ -127,7 +128,8 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
// |> rx(45, %)
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)`
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
show(mySketch001)`
|
||||
const ast = parse(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const sourceStart = code.indexOf(lineToChange)
|
||||
@ -153,6 +155,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
|> lineTo([2, 3], %)
|
||||
show(mySketch001)
|
||||
`
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
|
||||
@ -174,6 +177,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
|> close(%)
|
||||
show(mySketch001)
|
||||
`
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
})
|
||||
@ -188,6 +192,7 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
// |> rx(45, %)
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(originalLine)
|
||||
const ast = parse(code)
|
||||
|
@ -91,6 +91,12 @@ export function createFirstArg(
|
||||
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 = {
|
||||
add: ({
|
||||
node,
|
||||
@ -175,6 +181,10 @@ export const line: SketchLineHelper = {
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
const { node: callExpression } = getNodeFromPath<
|
||||
PipeExpression | CallExpression
|
||||
>(_node, pathToNode, 'CallExpression')
|
||||
|
||||
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
@ -184,6 +194,38 @@ export const line: SketchLineHelper = {
|
||||
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
|
||||
const isAddingSegmentBetween =
|
||||
callExpression.type === 'CallExpression' &&
|
||||
callExpression.start >= pipe.start &&
|
||||
callExpression.end <= pipe.end
|
||||
if (
|
||||
isAddingSegmentBetween &&
|
||||
!createCallback &&
|
||||
pipe.type === 'PipeExpression'
|
||||
) {
|
||||
const callExp = createCallExpression('line', [
|
||||
createArrayExpression([newXVal, newYVal]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
const pathToNodeIndex = pathToNode.findIndex(
|
||||
(x) => x[1] === 'PipeExpression'
|
||||
)
|
||||
const pipeIndex = pathToNode[pathToNodeIndex + 1][0]
|
||||
if (typeof pipeIndex === 'undefined' || typeof pipeIndex === 'string') {
|
||||
console.warn('pipeIndex is undefined')
|
||||
return
|
||||
}
|
||||
pipe.body = [
|
||||
...pipe.body.slice(0, pipeIndex),
|
||||
callExp,
|
||||
...pipe.body.slice(pipeIndex),
|
||||
]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
@ -960,30 +1002,6 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
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 } = {
|
||||
line,
|
||||
lineTo,
|
||||
@ -1029,15 +1047,6 @@ export function changeSketchArguments(
|
||||
throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
||||
}
|
||||
|
||||
interface CreateLineFnCallArgs {
|
||||
node: Program
|
||||
programMemory: ProgramMemory
|
||||
to: [number, number]
|
||||
from: [number, number]
|
||||
fnName: ToolTip
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
export function compareVec2Epsilon(
|
||||
vec1: [number, number],
|
||||
vec2: [number, number],
|
||||
@ -1062,6 +1071,15 @@ export function compareVec2Epsilon2(
|
||||
return distance < compareEpsilon
|
||||
}
|
||||
|
||||
interface CreateLineFnCallArgs {
|
||||
node: Program
|
||||
programMemory: ProgramMemory
|
||||
to: [number, number]
|
||||
from: [number, number]
|
||||
fnName: ToolTip
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
export function addNewSketchLn({
|
||||
node: _node,
|
||||
programMemory: previousProgramMemory,
|
||||
|
@ -88,6 +88,7 @@ describe('testing swapping out sketch calls with xLine/xLineTo', () => {
|
||||
` |> yLine(-1.07, %)`,
|
||||
` |> xLineTo(3.27, %)`,
|
||||
` |> yLineTo(2.14, %)`,
|
||||
`show(part001)`,
|
||||
]
|
||||
const bigExample = bigExampleArr.join('\n')
|
||||
it('line with tag converts to xLine', async () => {
|
||||
@ -289,6 +290,7 @@ describe('testing swapping out sketch calls with xLine/xLineTo while keeping var
|
||||
` |> angledLineToX([330, angledLineToXx], %)`,
|
||||
` |> angledLineToY([217, angledLineToYy], %)`,
|
||||
` |> line([0.89, -0.1], %)`,
|
||||
`show(part001)`,
|
||||
]
|
||||
const varExample = variablesExampleArr.join('\n')
|
||||
it('line keeps variable when converted to xLine', async () => {
|
||||
@ -376,7 +378,8 @@ const part001 = startSketchOn('XY')
|
||||
|> line([0, 0.4], %)
|
||||
|> xLine(3.48, %)
|
||||
|> line([2.14, 1.35], %) // normal-segment
|
||||
|> xLine(3.54, %)`
|
||||
|> xLine(3.54, %)
|
||||
show(part001)`
|
||||
it('normal case works', async () => {
|
||||
const programMemory = await enginelessExecutor(parse(code))
|
||||
const index = code.indexOf('// normal-segment') - 7
|
||||
|
@ -123,6 +123,7 @@ const part001 = startSketchOn('XY')
|
||||
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|
||||
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|
||||
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
|
||||
show(part001)
|
||||
`
|
||||
const expectModifiedScript = `const myVar = 3
|
||||
const myVar2 = 5
|
||||
@ -195,6 +196,7 @@ const part001 = startSketchOn('XY')
|
||||
|> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
|
||||
|> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
|
||||
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
|
||||
show(part001)
|
||||
`
|
||||
it('should transform the ast', async () => {
|
||||
const ast = parse(inputScript)
|
||||
@ -255,6 +257,7 @@ const part001 = startSketchOn('XY')
|
||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||
show(part001)
|
||||
`
|
||||
it('should transform horizontal lines the ast', async () => {
|
||||
const expectModifiedScript = `const myVar = 2
|
||||
@ -283,6 +286,7 @@ const part001 = startSketchOn('XY')
|
||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|
||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||
show(part001)
|
||||
`
|
||||
const ast = parse(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
@ -341,6 +345,7 @@ const part001 = startSketchOn('XY')
|
||||
|> yLineTo(7.68, %) // select for vertical constraint 9
|
||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||
|> yLineTo(myVar, %) // select for vertical constraint 10
|
||||
show(part001)
|
||||
`
|
||||
const ast = parse(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
@ -384,6 +389,7 @@ const part001 = startSketchOn('XY')
|
||||
|> line([0.45, 1.46], %) // free
|
||||
|> line([myVar, 0.01], %) // xRelative
|
||||
|> line([0.7, myVar], %) // yRelative
|
||||
show(part001)
|
||||
`
|
||||
it('testing for free to horizontal and vertical distance', async () => {
|
||||
const expectedHorizontalCode = await helperThing(
|
||||
@ -495,7 +501,8 @@ const part001 = startSketchOn('XY')
|
||||
|> xLine(3.36, %) // partial
|
||||
|> line([-1.49, 1.06], %) // free
|
||||
|> 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 constraintLevels: ReturnType<
|
||||
typeof getConstraintLevelFromSourceRange
|
||||
|
@ -15,7 +15,8 @@ describe('testing angledLineThatIntersects', () => {
|
||||
offset: ${offset},
|
||||
tag: "yo2"
|
||||
}, %)
|
||||
const intersect = segEndX('yo2', part001)`
|
||||
const intersect = segEndX('yo2', part001)
|
||||
show(part001)`
|
||||
const { root } = await enginelessExecutor(parse(code('-1')))
|
||||
expect(root.intersect.value).toBe(1 + Math.sqrt(2))
|
||||
const { root: noOffset } = await enginelessExecutor(parse(code('0')))
|
||||
|
@ -40,9 +40,9 @@ export interface MouseGuard {
|
||||
}
|
||||
|
||||
const butName = (e: React.MouseEvent) => ({
|
||||
middle: !!(e.buttons & 4) || e.button === 1,
|
||||
right: !!(e.buttons & 2) || e.button === 2,
|
||||
left: !!(e.buttons & 1) || e.button === 0,
|
||||
middle: !!(e.buttons & 4),
|
||||
right: !!(e.buttons & 2),
|
||||
left: !!(e.buttons & 1),
|
||||
})
|
||||
|
||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||
|
@ -28,8 +28,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
||||
name: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: [],
|
||||
optionsFromContext: (context) =>
|
||||
options: (context) =>
|
||||
context.projects.map((p) => ({
|
||||
name: p.name!,
|
||||
value: p.name!,
|
||||
@ -44,7 +43,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
||||
name: {
|
||||
inputType: 'string',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||
defaultValue: (context) => context.defaultProjectName,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -56,8 +55,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
||||
name: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: [],
|
||||
optionsFromContext: (context) =>
|
||||
options: (context) =>
|
||||
context.projects.map((p) => ({
|
||||
name: p.name!,
|
||||
value: p.name!,
|
||||
@ -73,8 +71,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
||||
oldName: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: [],
|
||||
optionsFromContext: (context) =>
|
||||
options: (context) =>
|
||||
context.projects.map((p) => ({
|
||||
name: p.name!,
|
||||
value: p.name!,
|
||||
@ -83,7 +80,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
||||
newName: {
|
||||
inputType: 'string',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||
defaultValue: (context) => context.defaultProjectName,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,13 +1,7 @@
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||
import { Selections } from 'lib/selections'
|
||||
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 = [
|
||||
'new',
|
||||
'add',
|
||||
@ -17,10 +11,6 @@ export const EXTRUSION_RESULTS = [
|
||||
|
||||
export type ModelingCommandSchema = {
|
||||
'Enter sketch': {}
|
||||
Export: {
|
||||
type: OutputTypeKey
|
||||
storage?: StorageUnion
|
||||
}
|
||||
Extrude: {
|
||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||
@ -36,80 +26,6 @@ export const modelingMachineConfig: CommandSetConfig<
|
||||
description: 'Enter sketch mode.',
|
||||
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: {
|
||||
description: 'Pull a sketch into 3D along its normal or perpendicular.',
|
||||
icon: 'extrude',
|
||||
|
@ -41,9 +41,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
||||
baseUnit: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.baseUnit,
|
||||
options: [],
|
||||
optionsFromContext: (context) =>
|
||||
defaultValue: (context) => context.baseUnit,
|
||||
options: (context) =>
|
||||
Object.values(baseUnitsUnion).map((v) => ({
|
||||
name: v,
|
||||
value: v,
|
||||
@ -58,9 +57,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
||||
cameraControls: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.cameraControls,
|
||||
options: [],
|
||||
optionsFromContext: (context) =>
|
||||
defaultValue: (context) => context.cameraControls,
|
||||
options: (context) =>
|
||||
Object.values(cameraSystems).map((v) => ({
|
||||
name: v,
|
||||
value: v,
|
||||
@ -76,7 +74,7 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
||||
defaultProjectName: {
|
||||
inputType: 'string',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||
defaultValue: (context) => context.defaultProjectName,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -86,9 +84,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
||||
textWrapping: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.textWrapping,
|
||||
options: [],
|
||||
optionsFromContext: (context) => [
|
||||
defaultValue: (context) => context.textWrapping,
|
||||
options: (context) => [
|
||||
{
|
||||
name: 'On',
|
||||
value: 'On' as Toggle,
|
||||
@ -109,9 +106,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
||||
theme: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.theme,
|
||||
options: [],
|
||||
optionsFromContext: (context) =>
|
||||
defaultValue: (context) => context.theme,
|
||||
options: (context) =>
|
||||
Object.values(Themes).map((v) => ({
|
||||
name: v,
|
||||
value: v,
|
||||
@ -126,9 +122,8 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
||||
unitSystem: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.unitSystem,
|
||||
options: [],
|
||||
optionsFromContext: (context) => [
|
||||
defaultValue: (context) => context.unitSystem,
|
||||
options: (context) => [
|
||||
{
|
||||
name: 'Imperial',
|
||||
value: 'imperial' as UnitSystem,
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
} from 'xstate'
|
||||
import { Selection } from './selections'
|
||||
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
|
||||
type Icon = CustomIconName
|
||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||
@ -94,31 +93,15 @@ export type CommandArgumentConfig<
|
||||
> =
|
||||
| {
|
||||
description?: string
|
||||
required:
|
||||
| boolean
|
||||
| ((
|
||||
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
required: boolean
|
||||
skip?: true
|
||||
} & (
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'options'>
|
||||
options:
|
||||
| CommandArgumentOption<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
|
||||
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||
}
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'selection'>
|
||||
@ -128,12 +111,7 @@ export type CommandArgumentConfig<
|
||||
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'string'>
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||
) => OutputType)
|
||||
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
|
||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||
}
|
||||
)
|
||||
|
||||
@ -143,42 +121,24 @@ export type CommandArgument<
|
||||
> =
|
||||
| {
|
||||
description?: string
|
||||
required:
|
||||
| boolean
|
||||
| ((
|
||||
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
machineActor: InterpreterFrom<T>
|
||||
required: boolean
|
||||
skip?: true
|
||||
} & (
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'options'>
|
||||
options:
|
||||
| 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)
|
||||
options: CommandArgumentOption<OutputType>[]
|
||||
defaultValue?: OutputType
|
||||
}
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'selection'>
|
||||
selectionTypes: Selection['type'][]
|
||||
actor: InterpreterFrom<T>
|
||||
multiple: boolean
|
||||
}
|
||||
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'string'>
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||
) => OutputType)
|
||||
defaultValue?: OutputType
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -17,7 +17,7 @@ interface CreateMachineCommandProps<
|
||||
ownerMachine: T['id']
|
||||
state: StateFrom<T>
|
||||
send: Function
|
||||
actor: InterpreterFrom<T>
|
||||
actor?: InterpreterFrom<T>
|
||||
commandBarConfig?: CommandSetConfig<T, S>
|
||||
onCancel?: () => void
|
||||
}
|
||||
@ -91,13 +91,13 @@ function buildCommandArguments<
|
||||
>(
|
||||
state: StateFrom<T>,
|
||||
args: CommandConfig<T, CommandName, S>['args'],
|
||||
machineActor: InterpreterFrom<T>
|
||||
actor?: InterpreterFrom<T>
|
||||
): NonNullable<Command<T, CommandName, S>['args']> {
|
||||
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
||||
|
||||
for (const arg in args) {
|
||||
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
||||
const newArg = buildCommandArgument(argConfig, arg, state, machineActor)
|
||||
const newArg = buildCommandArgument(argConfig, arg, state, actor)
|
||||
newArgs[arg] = newArg
|
||||
}
|
||||
|
||||
@ -111,36 +111,44 @@ function buildCommandArgument<
|
||||
arg: CommandArgumentConfig<O, T>,
|
||||
argName: string,
|
||||
state: StateFrom<T>,
|
||||
machineActor: InterpreterFrom<T>
|
||||
actor?: InterpreterFrom<T>
|
||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||
const baseCommandArgument = {
|
||||
description: arg.description,
|
||||
required: arg.required,
|
||||
skip: arg.skip,
|
||||
machineActor,
|
||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||
|
||||
if (arg.inputType === 'options') {
|
||||
if (!arg.options) {
|
||||
const options = 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')
|
||||
}
|
||||
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
...baseCommandArgument,
|
||||
defaultValue: arg.defaultValueFromContext
|
||||
? arg.defaultValueFromContext(state.context)
|
||||
: arg.defaultValue,
|
||||
options: arg.optionsFromContext
|
||||
? arg.optionsFromContext(state.context)
|
||||
: arg.options,
|
||||
defaultValue:
|
||||
arg.defaultValue instanceof Function
|
||||
? arg.defaultValue(state.context)
|
||||
: arg.defaultValue,
|
||||
options,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
||||
} else if (arg.inputType === 'selection') {
|
||||
if (!actor)
|
||||
throw new Error('Actor must be provided for selection input type')
|
||||
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
...baseCommandArgument,
|
||||
multiple: arg.multiple,
|
||||
selectionTypes: arg.selectionTypes,
|
||||
actor,
|
||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||
} else if (arg.inputType === 'kcl') {
|
||||
return {
|
||||
@ -151,7 +159,10 @@ function buildCommandArgument<
|
||||
} else {
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
defaultValue: arg.defaultValue,
|
||||
defaultValue:
|
||||
arg.defaultValue instanceof Function
|
||||
? arg.defaultValue(state.context)
|
||||
: arg.defaultValue,
|
||||
...baseCommandArgument,
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
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,7 +20,6 @@ import {
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
sceneEntitiesManager,
|
||||
getParentGroup,
|
||||
PROFILE_START,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import { Mesh } from 'three'
|
||||
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
|
||||
@ -189,11 +188,7 @@ export async function getEventForSelectWithPoint(
|
||||
export function getEventForSegmentSelection(
|
||||
obj: any
|
||||
): ModelingMachineEvent | null {
|
||||
const group = getParentGroup(obj, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
PROFILE_START,
|
||||
])
|
||||
const group = getParentGroup(obj)
|
||||
const axisGroup = getParentGroup(obj, [AXIS_GROUP])
|
||||
if (!group && !axisGroup) return null
|
||||
if (axisGroup?.userData.type === AXIS_GROUP) {
|
||||
@ -412,8 +407,8 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
|
||||
}
|
||||
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
|
||||
if (
|
||||
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT, PROFILE_START].includes(
|
||||
segmentGroup?.name
|
||||
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT].includes(
|
||||
segmentGroup?.userData?.type
|
||||
)
|
||||
)
|
||||
return
|
||||
@ -425,9 +420,7 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
|
||||
const groupHasCursor = codeBasedSelections.some((selection) => {
|
||||
return isOverlap(selection.range, [node.start, node.end])
|
||||
})
|
||||
const color = groupHasCursor
|
||||
? 0x0000ff
|
||||
: segmentGroup?.userData?.baseColor || 0xffffff
|
||||
const color = groupHasCursor ? 0x0000ff : 0xffffff
|
||||
segmentGroup.traverse(
|
||||
(child) => child instanceof Mesh && child.material.color.set(color)
|
||||
)
|
||||
|
@ -20,8 +20,6 @@ const LOCAL_USER: Models['User_type'] = {
|
||||
phone: '555-555-5555',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
can_train_on_data: false,
|
||||
is_service_account: false,
|
||||
}
|
||||
|
||||
export interface UserContext {
|
||||
|
@ -8,29 +8,21 @@ import {
|
||||
import { Selections } from 'lib/selections'
|
||||
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(
|
||||
{
|
||||
/** @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,
|
||||
/** @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 */
|
||||
context: {
|
||||
commands: [],
|
||||
selectedCommand: undefined,
|
||||
currentArgument: undefined,
|
||||
commands: [] as Command[],
|
||||
selectedCommand: undefined as Command | undefined,
|
||||
currentArgument: undefined as
|
||||
| (CommandArgument<unknown> & { name: string })
|
||||
| undefined,
|
||||
selectionRanges: {
|
||||
otherSelections: [],
|
||||
codeBasedSelections: [],
|
||||
},
|
||||
argumentsToSubmit: {},
|
||||
} as CommandBarContext,
|
||||
} as Selections,
|
||||
argumentsToSubmit: {} as { [x: string]: unknown },
|
||||
},
|
||||
id: 'Command Bar',
|
||||
initial: 'Closed',
|
||||
states: {
|
||||
@ -275,6 +267,7 @@ export const commandBarMachine = createMachine(
|
||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||
},
|
||||
},
|
||||
predictableActionArguments: true,
|
||||
preserveActionOrder: true,
|
||||
},
|
||||
{
|
||||
@ -286,45 +279,28 @@ export const commandBarMachine = createMachine(
|
||||
(selectedCommand?.args && event.type === 'Submit command') ||
|
||||
event.type === 'done.invoke.validateArguments'
|
||||
) {
|
||||
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)
|
||||
selectedCommand?.onSubmit(getCommandArgumentKclValuesOnly(event.data))
|
||||
} else {
|
||||
selectedCommand?.onSubmit()
|
||||
}
|
||||
},
|
||||
'Set current argument to first non-skippable': assign({
|
||||
currentArgument: (context, event) => {
|
||||
currentArgument: (context) => {
|
||||
const { selectedCommand } = context
|
||||
if (!(selectedCommand && selectedCommand.args)) return undefined
|
||||
const rejectedArg = 'data' in event && event.data.arg
|
||||
|
||||
// Find the first argument that is not to be skipped:
|
||||
// that is, the first argument that is not already in the argumentsToSubmit
|
||||
// or that is not undefined, or that is not marked as "skippable".
|
||||
// TODO validate the type of the existing arguments
|
||||
let argIndex = 0
|
||||
|
||||
while (argIndex < Object.keys(selectedCommand.args).length) {
|
||||
const [argName, argConfig] = Object.entries(selectedCommand.args)[
|
||||
argIndex
|
||||
]
|
||||
const argIsRequired =
|
||||
typeof argConfig.required === 'function'
|
||||
? argConfig.required(context)
|
||||
: argConfig.required
|
||||
const argName = Object.keys(selectedCommand.args)[argIndex]
|
||||
const mustNotSkipArg =
|
||||
argIsRequired &&
|
||||
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
||||
context.argumentsToSubmit[argName] === undefined ||
|
||||
(rejectedArg && rejectedArg.name === argName))
|
||||
|
||||
if (mustNotSkipArg === true) {
|
||||
!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
||||
context.argumentsToSubmit[argName] === undefined ||
|
||||
!selectedCommand.args[argName].skip
|
||||
if (mustNotSkipArg) {
|
||||
return {
|
||||
...selectedCommand.args[argName],
|
||||
name: argName,
|
||||
@ -332,10 +308,14 @@ export const commandBarMachine = createMachine(
|
||||
}
|
||||
argIndex++
|
||||
}
|
||||
|
||||
// Just show the last argument if all are skippable
|
||||
// TODO: use an XState service to continue onto review step
|
||||
// if all arguments are skippable and contain values.
|
||||
return undefined
|
||||
const argName = Object.keys(selectedCommand.args)[argIndex - 1]
|
||||
return {
|
||||
...selectedCommand.args[argName],
|
||||
name: argName,
|
||||
}
|
||||
},
|
||||
}),
|
||||
'Clear current argument': assign({
|
||||
@ -353,6 +333,8 @@ export const commandBarMachine = createMachine(
|
||||
'Set current argument': assign({
|
||||
currentArgument: (context, event) => {
|
||||
switch (event.type) {
|
||||
case 'error.platform.validateArguments':
|
||||
return event.data.arg
|
||||
case 'Edit argument':
|
||||
return event.data.arg
|
||||
default:
|
||||
@ -361,22 +343,27 @@ export const commandBarMachine = createMachine(
|
||||
},
|
||||
}),
|
||||
'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) => {
|
||||
if (
|
||||
event.type !== 'Change current argument' ||
|
||||
!context.currentArgument
|
||||
)
|
||||
return context.argumentsToSubmit
|
||||
const { name } = context.currentArgument
|
||||
const { name, required } = context.currentArgument
|
||||
if (required)
|
||||
return {
|
||||
[name]: undefined,
|
||||
...context.argumentsToSubmit,
|
||||
}
|
||||
|
||||
const { [name]: _, ...rest } = context.argumentsToSubmit
|
||||
return rest
|
||||
},
|
||||
currentArgument: (context, event) => {
|
||||
if (event.type !== 'Change current argument')
|
||||
return context.currentArgument
|
||||
return Object.values(event.data)[0]
|
||||
},
|
||||
}),
|
||||
'Clear argument data': assign({
|
||||
selectedCommand: undefined,
|
||||
@ -401,6 +388,11 @@ export const commandBarMachine = createMachine(
|
||||
}),
|
||||
'Initialize arguments to submit': assign({
|
||||
argumentsToSubmit: (c, e) => {
|
||||
if (
|
||||
e.type !== 'Select command' &&
|
||||
e.type !== 'Find and select command'
|
||||
)
|
||||
return c.argumentsToSubmit
|
||||
const command =
|
||||
'command' in e.data ? e.data.command : c.selectedCommand!
|
||||
if (!command.args) return {}
|
||||
@ -429,67 +421,38 @@ export const commandBarMachine = createMachine(
|
||||
},
|
||||
'Validate all arguments': (context, _) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
for (const [argName, argConfig] of Object.entries(
|
||||
context.selectedCommand!.args!
|
||||
for (const [argName, arg] of Object.entries(
|
||||
context.argumentsToSubmit
|
||||
)) {
|
||||
let arg = context.argumentsToSubmit[argName]
|
||||
let argValue = typeof arg === 'function' ? arg(context) : arg
|
||||
let argConfig = context.selectedCommand!.args![argName]
|
||||
|
||||
try {
|
||||
const isRequired =
|
||||
typeof argConfig.required === 'function'
|
||||
? argConfig.required(context)
|
||||
: argConfig.required
|
||||
if (
|
||||
('defaultValue' in argConfig &&
|
||||
argConfig.defaultValue &&
|
||||
typeof arg !== typeof argConfig.defaultValue &&
|
||||
argConfig.inputType !== 'kcl') ||
|
||||
(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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const resolvedDefaultValue =
|
||||
'defaultValue' in argConfig
|
||||
? typeof argConfig.defaultValue === 'function'
|
||||
? argConfig.defaultValue(context)
|
||||
: argConfig.defaultValue
|
||||
: 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
|
||||
if (!arg && argConfig.required) {
|
||||
return reject({
|
||||
message: 'Argument payload is falsy but is required',
|
||||
arg: {
|
||||
...argConfig,
|
||||
name: argName,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,6 @@ export type ModelingMachineEvent =
|
||||
| { type: 'Constrain parallel' }
|
||||
| { type: 'Constrain remove constraints' }
|
||||
| { type: 'Re-execute' }
|
||||
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
||||
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
||||
| { type: 'Equip Line tool' }
|
||||
| { type: 'Equip tangential arc to' }
|
||||
@ -120,7 +119,7 @@ export type MoveDesc = { line: number; snippet: string }
|
||||
|
||||
export const modelingMachine = createMachine(
|
||||
{
|
||||
/** @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 */
|
||||
/** @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 */
|
||||
id: 'Modeling',
|
||||
|
||||
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
||||
@ -171,13 +170,6 @@ export const modelingMachine = createMachine(
|
||||
actions: ['AST extrude'],
|
||||
internal: true,
|
||||
},
|
||||
|
||||
Export: {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
cond: 'Has exportable geometry',
|
||||
actions: 'Engine export',
|
||||
},
|
||||
},
|
||||
|
||||
entry: 'reset client scene mouse handlers',
|
||||
@ -489,7 +481,6 @@ export const modelingMachine = createMachine(
|
||||
'animate after sketch',
|
||||
'tear down client sketch',
|
||||
'remove sketch grid',
|
||||
'engineToClient cam sync direction',
|
||||
],
|
||||
|
||||
entry: ['add axis n grid', 'conditionally equip line tool'],
|
||||
@ -523,8 +514,6 @@ export const modelingMachine = createMachine(
|
||||
internal: true,
|
||||
},
|
||||
},
|
||||
|
||||
entry: 'clientToEngine cam sync direction',
|
||||
},
|
||||
|
||||
'animating to existing sketch': {
|
||||
@ -535,12 +524,7 @@ export const modelingMachine = createMachine(
|
||||
onDone: 'Sketch',
|
||||
},
|
||||
],
|
||||
|
||||
entry: 'clientToEngine cam sync direction',
|
||||
},
|
||||
|
||||
'animating to plane (copy)': {},
|
||||
'animating to plane (copy) (copy)': {},
|
||||
},
|
||||
|
||||
initial: 'idle',
|
||||
@ -840,13 +824,13 @@ export const modelingMachine = createMachine(
|
||||
sceneInfra.setCallbacks({
|
||||
onClick: async (args) => {
|
||||
if (!args) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
const { intersectionPoint } = args
|
||||
if (!intersectionPoint?.twoD || !sketchPathToNode) return
|
||||
if (args.event.which !== 1) return
|
||||
const { intersection2d } = args
|
||||
if (!intersection2d || !sketchPathToNode) return
|
||||
const { modifiedAst } = addStartProfileAt(
|
||||
kclManager.ast,
|
||||
sketchPathToNode,
|
||||
[intersectionPoint.twoD.x, intersectionPoint.twoD.y]
|
||||
[intersection2d.x, intersection2d.y]
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, false)
|
||||
sceneEntitiesManager.removeIntersectionPlane()
|
||||
@ -861,12 +845,6 @@ export const modelingMachine = createMachine(
|
||||
// (note the orbit controls are always active though)
|
||||
sceneInfra.resetMouseListeners()
|
||||
},
|
||||
'clientToEngine cam sync direction': () => {
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
},
|
||||
'engineToClient cam sync direction': () => {
|
||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||
},
|
||||
},
|
||||
// end actions
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ const Home = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const [state, send, actor] = useMachine(homeMachine, {
|
||||
const [state, send] = useMachine(homeMachine, {
|
||||
context: {
|
||||
projects: loadedProjects,
|
||||
defaultProjectName,
|
||||
@ -176,7 +176,6 @@ const Home = () => {
|
||||
send,
|
||||
state,
|
||||
commandBarConfig: homeCommandBarConfig,
|
||||
actor,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -21,7 +21,7 @@ export default function Export() {
|
||||
<section className="flex-1">
|
||||
<h2 className="text-2xl font-bold">Export</h2>
|
||||
<p className="my-4">
|
||||
Try opening the project menu and clicking "Export Part".
|
||||
Try opening the project menu and clicking "Export Model".
|
||||
</p>
|
||||
<p className="my-4">
|
||||
{APP_NAME} uses{' '}
|
||||
|
148
src/wasm-lib/Cargo.lock
generated
@ -246,7 +246,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -257,7 +257,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -574,9 +574,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.1"
|
||||
version = "4.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
|
||||
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -584,9 +584,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.1"
|
||||
version = "4.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
|
||||
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -606,7 +606,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -856,7 +856,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -897,7 +897,7 @@ checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
"synstructure 0.13.0",
|
||||
]
|
||||
|
||||
@ -949,7 +949,7 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -965,7 +965,7 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -977,7 +977,7 @@ dependencies = [
|
||||
"diesel_table_macro_syntax",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -986,7 +986,7 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
|
||||
dependencies = [
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1036,7 +1036,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1118,7 +1118,7 @@ checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1335,7 +1335,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1404,9 +1404,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.13.1"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
|
||||
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
@ -1440,7 +1440,7 @@ dependencies = [
|
||||
"inflections",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1460,17 +1460,13 @@ name = "grackle"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kcl-lib",
|
||||
"kittycad",
|
||||
"kittycad-execution-plan",
|
||||
"kittycad-execution-plan-macros",
|
||||
"kittycad-execution-plan-traits",
|
||||
"kittycad-modeling-cmds",
|
||||
"kittycad-modeling-session",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1715,9 +1711,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
version = "0.24.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
|
||||
checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
@ -1916,7 +1912,7 @@ dependencies = [
|
||||
"itertools 0.12.1",
|
||||
"js-sys",
|
||||
"kittycad",
|
||||
"kittycad-execution-plan-macros",
|
||||
"kittycad-execution-plan-macros 0.1.4 (git+https://github.com/KittyCAD/modeling-api?branch=main)",
|
||||
"kittycad-execution-plan-traits",
|
||||
"lazy_static",
|
||||
"parse-display 0.9.0",
|
||||
@ -1947,14 +1943,14 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.58"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "049c3881ffbe77bf1c3a968372a246ce906eceb79f61cd0bc5fa229bec3504cb"
|
||||
checksum = "13958174d876353f429ea8230dc92fe86f164819cea2e51bbf22e01a4c2a496e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1990,12 +1986,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#08f05d91062380fe3a69f4baa1f1301532d31977"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"insta",
|
||||
"kittycad",
|
||||
"kittycad-execution-plan-macros",
|
||||
"kittycad-execution-plan-traits",
|
||||
"kittycad-modeling-cmds",
|
||||
"kittycad-modeling-session",
|
||||
@ -2009,18 +2004,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan-macros"
|
||||
version = "0.1.8"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d31b689c944d00aadda2ef83d8422a6efff97e1be5654a61f9d95496f0c19e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan-macros"
|
||||
version = "0.1.4"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#632b75a0242400fa34373d7973b9149b0e08aa3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan-traits"
|
||||
version = "0.1.12"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3ec8efd57b59697eb140b63c0ffe7db44fdfe5a55f14e45513411eba2280ba5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
@ -2029,8 +2036,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-modeling-cmds"
|
||||
version = "0.1.28"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||
version = "0.1.18"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#08f05d91062380fe3a69f4baa1f1301532d31977"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -2040,9 +2047,8 @@ dependencies = [
|
||||
"enum-iterator-derive",
|
||||
"euler",
|
||||
"http 0.2.9",
|
||||
"kittycad-execution-plan-macros",
|
||||
"kittycad-execution-plan-macros 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"kittycad-execution-plan-traits",
|
||||
"kittycad-modeling-cmds-macros",
|
||||
"kittycad-unit-conversion-derive",
|
||||
"measurements",
|
||||
"parse-display 0.8.2",
|
||||
@ -2055,20 +2061,10 @@ dependencies = [
|
||||
"webrtc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-modeling-cmds-macros"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-modeling-session"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#03eb9c3763de56d7284c09dba678ddd6120bb523"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#08f05d91062380fe3a69f4baa1f1301532d31977"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"kittycad",
|
||||
@ -2500,7 +2496,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2657,7 +2653,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-syntax 0.7.5",
|
||||
"structmeta 0.2.0",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2671,7 +2667,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-syntax 0.8.2",
|
||||
"structmeta 0.3.0",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2737,7 +2733,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3605,7 +3601,7 @@ checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3639,7 +3635,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3660,7 +3656,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3917,7 +3913,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive 0.2.0",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3929,7 +3925,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive 0.3.0",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3940,7 +3936,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3951,7 +3947,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4023,9 +4019,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.52"
|
||||
version = "2.0.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
|
||||
checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -4058,7 +4054,7 @@ checksum = "285ba80e733fac80aa4270fbcdf83772a79b80aa35c97075320abfee4a915b06"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@ -4188,7 +4184,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4295,7 +4291,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4458,7 +4454,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4486,7 +4482,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4570,7 +4566,7 @@ dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
@ -4817,7 +4813,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -4852,7 +4848,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -5138,9 +5134,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
|
||||
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
@ -5470,7 +5466,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5490,7 +5486,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.49",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -21,7 +21,7 @@ wasm-bindgen-futures = "0.4.41"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
image = "0.24.9"
|
||||
image = "0.24.8"
|
||||
kittycad = { workspace = true, default-features = true }
|
||||
pretty_assertions = "1.4.0"
|
||||
reqwest = { version = "0.11.24", default-features = false }
|
||||
@ -58,12 +58,11 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
kittycad = { version = "0.2.58", default-features = false, features = ["js", "requests"] }
|
||||
kittycad = { version = "0.2.54", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
kittycad-execution-plan-traits = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
kittycad-modeling-cmds = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
kittycad-execution-plan-traits = "0.1.10"
|
||||
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
|
||||
[[test]]
|
||||
name = "executor"
|
||||
@ -74,9 +73,6 @@ name = "modify"
|
||||
path = "tests/modify/main.rs"
|
||||
|
||||
# Example: how to point modeling-api at a different repo (e.g. a branch or a local clone)
|
||||
#[patch."https://github.com/KittyCAD/modeling-api"]
|
||||
#kittycad-execution-plan = { path = "../../../modeling-api/execution-plan" }
|
||||
#kittycad-execution-plan-macros = { path = "../../../modeling-api/execution-plan-macros" }
|
||||
#kittycad-execution-plan-traits = { path = "../../../modeling-api/execution-plan-traits" }
|
||||
#kittycad-modeling-cmds = { path = "../../../modeling-api/modeling-cmds" }
|
||||
#kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }
|
||||
# [patch."https://github.com/KittyCAD/modeling-api"]
|
||||
# kittycad-execution-plan = { path = "../../../modeling-api/execution-plan" }
|
||||
# kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }
|
||||
|
@ -19,7 +19,7 @@ quote = "1"
|
||||
regex = "1.10"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_tokenstream = "0.2"
|
||||
syn = { version = "2.0.52", features = ["full"] }
|
||||
syn = { version = "2.0.49", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
expectorate = "1.1.0"
|
||||
|
@ -7,15 +7,11 @@ description = "A new executor for KCL which compiles to Execution Plans"
|
||||
|
||||
[dependencies]
|
||||
kcl-lib = { path = "../kcl" }
|
||||
kittycad = { workspace = true }
|
||||
kittycad-execution-plan = { workspace = true }
|
||||
kittycad-execution-plan-traits = { workspace = true }
|
||||
kittycad-execution-plan-macros = { workspace = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
kittycad-modeling-session = { workspace = true }
|
||||
thiserror = "1.0.57"
|
||||
tokio = { version = "1.36.0", features = ["macros", "rt"] }
|
||||
uuid = "1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
|
@ -103,11 +103,7 @@ impl BindingScope {
|
||||
("add".into(), EpBinding::from(KclFunction::Add(native_functions::Add))),
|
||||
(
|
||||
"startSketchAt".into(),
|
||||
EpBinding::from(KclFunction::StartSketchAt(native_functions::sketch::StartSketchAt)),
|
||||
),
|
||||
(
|
||||
"lineTo".into(),
|
||||
EpBinding::from(KclFunction::LineTo(native_functions::sketch::LineTo)),
|
||||
EpBinding::from(KclFunction::StartSketchAt(native_functions::StartSketchAt)),
|
||||
),
|
||||
]),
|
||||
parent: None,
|
||||
|
@ -45,13 +45,6 @@ pub enum CompileError {
|
||||
NoReturnStmt,
|
||||
#[error("You used the %, which means \"substitute this argument for the value to the left in this |> pipeline\". But there is no such value, because you're not calling a pipeline.")]
|
||||
NotInPipeline,
|
||||
#[error("The function '{fn_name}' expects a parameter of type {expected} as argument number {arg_number} but you supplied {actual}")]
|
||||
ArgWrongType {
|
||||
fn_name: &'static str,
|
||||
expected: &'static str,
|
||||
actual: String,
|
||||
arg_number: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -252,7 +252,6 @@ impl Planner {
|
||||
} = match callee {
|
||||
KclFunction::Id(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::UserDefined(f) => {
|
||||
let UserDefinedFunction {
|
||||
@ -619,8 +618,7 @@ impl Eq for UserDefinedFunction {}
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
enum KclFunction {
|
||||
Id(native_functions::Id),
|
||||
StartSketchAt(native_functions::sketch::StartSketchAt),
|
||||
LineTo(native_functions::sketch::LineTo),
|
||||
StartSketchAt(native_functions::StartSketchAt),
|
||||
Add(native_functions::Add),
|
||||
UserDefined(UserDefinedFunction),
|
||||
}
|
||||
|
@ -2,13 +2,12 @@
|
||||
//! This includes some of the stdlib, e.g. `startSketchAt`.
|
||||
//! But some other stdlib functions will be written in KCL.
|
||||
|
||||
use kcl_lib::std::sketch::PlaneData;
|
||||
use kittycad_execution_plan::{BinaryArithmetic, Destination, Instruction};
|
||||
use kittycad_execution_plan_traits::Address;
|
||||
use kittycad_execution_plan_traits::{Address, Value};
|
||||
|
||||
use crate::{CompileError, EpBinding, EvalPlan};
|
||||
|
||||
pub mod sketch;
|
||||
|
||||
/// The identity function. Always returns its first input.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
@ -42,6 +41,34 @@ impl Callable for Id {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
pub struct StartSketchAt;
|
||||
|
||||
impl Callable for StartSketchAt {
|
||||
fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
let mut instructions = Vec::new();
|
||||
// Store the plane.
|
||||
let plane = PlaneData::XY.into_parts();
|
||||
instructions.push(Instruction::SetValue {
|
||||
address: next_addr.offset_by(plane.len()),
|
||||
value_parts: plane,
|
||||
});
|
||||
// TODO: Get the plane ID from global context.
|
||||
// TODO: Send this command:
|
||||
// ModelingCmd::SketchModeEnable {
|
||||
// animated: false,
|
||||
// ortho: false,
|
||||
// plane_id: plane.id,
|
||||
// // We pass in the normal for the plane here.
|
||||
// disable_camera_with_plane: Some(plane.z_axis.clone().into()),
|
||||
// },
|
||||
// TODO: Send ModelingCmd::StartPath at the given point.
|
||||
// TODO (maybe): Store the SketchGroup in KCEP memory.
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// A test function that adds two numbers.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
|
@ -1,6 +0,0 @@
|
||||
//! Native functions for sketching on the plane.
|
||||
|
||||
pub mod helpers;
|
||||
pub mod stdlib_functions;
|
||||
|
||||
pub use stdlib_functions::{LineTo, StartSketchAt};
|
@ -1,137 +0,0 @@
|
||||
use kittycad_execution_plan::{api_request::ApiRequest, Destination, Instruction};
|
||||
use kittycad_execution_plan_traits::{Address, InMemory};
|
||||
use kittycad_modeling_cmds::{id::ModelingCmdId, ModelingCmdEndpoint};
|
||||
|
||||
use crate::{binding_scope::EpBinding, error::CompileError};
|
||||
|
||||
/// Emit instructions for an API call with no parameters.
|
||||
pub fn no_arg_api_call(instrs: &mut Vec<Instruction>, endpoint: ModelingCmdEndpoint, cmd_id: ModelingCmdId) {
|
||||
instrs.push(Instruction::ApiRequest(ApiRequest {
|
||||
endpoint,
|
||||
store_response: None,
|
||||
arguments: vec![],
|
||||
cmd_id,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Emit instructions for an API call with the given parameters.
|
||||
/// The API parameters are stored in the EP memory stack.
|
||||
/// So, they have to be pushed onto the stack in the right order,
|
||||
/// i.e. the reverse order in which the API call's Rust struct defines the fields.
|
||||
pub fn stack_api_call<const N: usize>(
|
||||
instrs: &mut Vec<Instruction>,
|
||||
endpoint: ModelingCmdEndpoint,
|
||||
store_response: Option<Address>,
|
||||
cmd_id: ModelingCmdId,
|
||||
data: [Vec<kittycad_execution_plan_traits::Primitive>; N],
|
||||
) {
|
||||
let arguments = vec![InMemory::StackPop; data.len()];
|
||||
instrs.extend(data.map(|data| Instruction::StackPush { data }));
|
||||
instrs.push(Instruction::ApiRequest(ApiRequest {
|
||||
endpoint,
|
||||
store_response,
|
||||
arguments,
|
||||
cmd_id,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn single_binding(
|
||||
b: EpBinding,
|
||||
fn_name: &'static str,
|
||||
expected: &'static str,
|
||||
arg_number: usize,
|
||||
) -> Result<Address, CompileError> {
|
||||
match b {
|
||||
EpBinding::Single(a) => Ok(a),
|
||||
EpBinding::Sequence { .. } => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "array".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
EpBinding::Map { .. } => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "object".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
EpBinding::Function(_) => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "function".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sequence_binding(
|
||||
b: EpBinding,
|
||||
fn_name: &'static str,
|
||||
expected: &'static str,
|
||||
arg_number: usize,
|
||||
) -> Result<Vec<EpBinding>, CompileError> {
|
||||
match b {
|
||||
EpBinding::Sequence { elements, .. } => Ok(elements),
|
||||
EpBinding::Single(_) => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "single".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
EpBinding::Map { .. } => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "object".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
EpBinding::Function(_) => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "function".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a 2D point from an argument to a Cabble.
|
||||
pub fn arg_point2d(
|
||||
arg: EpBinding,
|
||||
fn_name: &'static str,
|
||||
instructions: &mut Vec<Instruction>,
|
||||
next_addr: &mut Address,
|
||||
arg_number: usize,
|
||||
) -> Result<Address, CompileError> {
|
||||
let expected = "2D point (array with length 2)";
|
||||
let elements = sequence_binding(arg, "startSketchAt", "an array of length 2", arg_number)?;
|
||||
if elements.len() != 2 {
|
||||
return Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: format!("array of length {}", elements.len()),
|
||||
arg_number: 0,
|
||||
});
|
||||
}
|
||||
// KCL stores points as an array.
|
||||
// KC API stores them as Rust objects laid flat out in memory.
|
||||
let start = next_addr.offset_by(2);
|
||||
let start_x = start;
|
||||
let start_y = start + 1;
|
||||
let start_z = start + 2;
|
||||
instructions.extend([
|
||||
Instruction::Copy {
|
||||
source: single_binding(elements[0].clone(), "startSketchAt", "number", arg_number)?,
|
||||
destination: Destination::Address(start_x),
|
||||
length: 1,
|
||||
},
|
||||
Instruction::Copy {
|
||||
source: single_binding(elements[1].clone(), "startSketchAt", "number", arg_number)?,
|
||||
destination: Destination::Address(start_y),
|
||||
length: 1,
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: start_z,
|
||||
value: 0.0.into(),
|
||||
},
|
||||
]);
|
||||
Ok(start)
|
||||
}
|
@ -1,211 +0,0 @@
|
||||
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_modeling_cmds::{
|
||||
shared::{Point3d, Point4d},
|
||||
ModelingCmdEndpoint,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::helpers::{arg_point2d, no_arg_api_call, single_binding, stack_api_call};
|
||||
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)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
pub struct StartSketchAt;
|
||||
|
||||
impl Callable for StartSketchAt {
|
||||
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
let mut instructions = Vec::new();
|
||||
// First, before we send any API calls, let's validate the arguments to this function.
|
||||
let mut args_iter = args.into_iter();
|
||||
let Some(start) = args_iter.next() else {
|
||||
return Err(CompileError::NotEnoughArgs {
|
||||
fn_name: "startSketchAt".into(),
|
||||
required: 1,
|
||||
actual: 0,
|
||||
});
|
||||
};
|
||||
let start_point = arg_point2d(start, "startSketchAt", &mut instructions, next_addr, 0)?;
|
||||
let tag = match args_iter.next() {
|
||||
None => None,
|
||||
Some(b) => Some(single_binding(b, "startSketchAt", "a single string", 1)?),
|
||||
};
|
||||
|
||||
// Define some constants:
|
||||
let axes = Axes {
|
||||
x: Point3d { x: 1.0, y: 0.0, z: 0.0 },
|
||||
y: Point3d { x: 0.0, y: 1.0, z: 0.0 },
|
||||
z: Point3d { x: 0.0, y: 0.0, z: 1.0 },
|
||||
};
|
||||
let origin = Point3d::default();
|
||||
|
||||
// Now the function can start.
|
||||
// First API call: make the plane.
|
||||
let plane_id = Uuid::new_v4();
|
||||
stack_api_call(
|
||||
&mut instructions,
|
||||
ModelingCmdEndpoint::MakePlane,
|
||||
None,
|
||||
plane_id.into(),
|
||||
[
|
||||
Some(true).into_parts(), // hide
|
||||
vec![false.into()], // clobber
|
||||
vec![60.0.into()], // size
|
||||
axes.y.into_parts(),
|
||||
axes.x.into_parts(),
|
||||
origin.into_parts(),
|
||||
],
|
||||
);
|
||||
|
||||
// Next, enter sketch mode.
|
||||
stack_api_call(
|
||||
&mut instructions,
|
||||
ModelingCmdEndpoint::SketchModeEnable,
|
||||
None,
|
||||
Uuid::new_v4().into(),
|
||||
[
|
||||
Some(axes.z).into_parts(),
|
||||
vec![false.into()], // animated
|
||||
vec![false.into()], // ortho mode
|
||||
vec![plane_id.into()],
|
||||
],
|
||||
);
|
||||
|
||||
// Then start a path
|
||||
let path_id = Uuid::new_v4();
|
||||
no_arg_api_call(&mut instructions, ModelingCmdEndpoint::StartPath, path_id.into());
|
||||
|
||||
// Move the path pen to the given point.
|
||||
instructions.push(Instruction::StackPush {
|
||||
data: vec![path_id.into()],
|
||||
});
|
||||
instructions.push(Instruction::ApiRequest(ApiRequest {
|
||||
endpoint: ModelingCmdEndpoint::MovePathPen,
|
||||
store_response: None,
|
||||
arguments: vec![InMemory::StackPop, InMemory::Address(start_point)],
|
||||
cmd_id: Uuid::new_v4().into(),
|
||||
}));
|
||||
|
||||
// Starting a sketch creates a sketch group.
|
||||
// Updating the sketch will update this sketch group later.
|
||||
let sketch_group = SketchGroup {
|
||||
id: path_id,
|
||||
position: origin,
|
||||
rotation: Point4d {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
w: 1.0,
|
||||
},
|
||||
// TODO: Must copy the existing data (from the arguments to this KCL function)
|
||||
// over these values after writing to memory.
|
||||
path_first: BasePath {
|
||||
from: Default::default(),
|
||||
to: Default::default(),
|
||||
name: Default::default(),
|
||||
},
|
||||
path_rest: Vec::new(),
|
||||
on: sketch_types::SketchSurface::Plane(Plane {
|
||||
id: plane_id,
|
||||
value: sketch_types::PlaneType::XY,
|
||||
origin,
|
||||
axes,
|
||||
}),
|
||||
axes,
|
||||
entity_id: Some(plane_id),
|
||||
};
|
||||
let sketch_group_primitives = sketch_group.clone().into_parts();
|
||||
|
||||
let sketch_group_addr = next_addr.offset_by(sketch_group_primitives.len());
|
||||
instructions.push(Instruction::SetValue {
|
||||
address: sketch_group_addr,
|
||||
value_parts: sketch_group_primitives,
|
||||
});
|
||||
instructions.extend(sketch_group.set_base_path(sketch_group_addr, start_point, tag));
|
||||
|
||||
Ok(EvalPlan {
|
||||
instructions,
|
||||
binding: EpBinding::Single(sketch_group_addr),
|
||||
})
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
use std::{collections::HashMap, env};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ep::{Destination, UnaryArithmetic};
|
||||
use ept::{ListHeader, ObjectHeader};
|
||||
use kittycad_modeling_session::SessionBuilder;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
@ -1045,86 +1044,6 @@ fn store_object_with_array_property() {
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stdlib_cube_partial() {
|
||||
let program = r#"
|
||||
let cube = startSketchAt([0.0, 0.0])
|
||||
|> lineTo([4.0, 0.0], %)
|
||||
"#;
|
||||
let (_plan, _scope) = must_plan(program);
|
||||
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
|
||||
.ast()
|
||||
.unwrap();
|
||||
let client = test_client().await;
|
||||
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 {
|
||||
let kittycad_api_token = env::var("KITTYCAD_API_TOKEN").expect("You must set $KITTYCAD_API_TOKEN");
|
||||
let kittycad_api_client = kittycad::Client::new(kittycad_api_token);
|
||||
let session_builder = SessionBuilder {
|
||||
client: kittycad_api_client,
|
||||
fps: Some(10),
|
||||
unlocked_framerate: Some(false),
|
||||
video_res_height: Some(720),
|
||||
video_res_width: Some(1280),
|
||||
buffer_reqs: None,
|
||||
await_response_timeout: None,
|
||||
};
|
||||
match Session::start(session_builder).await {
|
||||
Err(e) => match e {
|
||||
kittycad::types::error::Error::InvalidRequest(s) => panic!("Request did not meet requirements {s}"),
|
||||
kittycad::types::error::Error::CommunicationError(e) => {
|
||||
panic!(" A server error either due to the data, or with the connection: {e}")
|
||||
}
|
||||
kittycad::types::error::Error::RequestError(e) => panic!("Could not build request: {e}"),
|
||||
kittycad::types::error::Error::SerdeError { error, status } => {
|
||||
panic!("Serde error (HTTP {status}): {error}")
|
||||
}
|
||||
kittycad::types::error::Error::InvalidResponsePayload { error, response } => {
|
||||
panic!("Invalid response payload. Error {error}, response {response:?}")
|
||||
}
|
||||
kittycad::types::error::Error::Server { body, status } => panic!("Server error (HTTP {status}): {body}"),
|
||||
kittycad::types::error::Error::UnexpectedResponse(resp) => {
|
||||
let status = resp.status();
|
||||
let url = resp.url().to_owned();
|
||||
match resp.text().await {
|
||||
Ok(body) => panic!(
|
||||
"Unexpected response from KittyCAD API.
|
||||
URL:{url}
|
||||
HTTP {status}
|
||||
---Body----
|
||||
{body}"
|
||||
),
|
||||
Err(e) => panic!(
|
||||
"Unexpected response from KittyCAD API.
|
||||
URL:{url}
|
||||
HTTP {status}
|
||||
---Body could not be read, the error is----
|
||||
{e}"
|
||||
),
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(x) => x,
|
||||
}
|
||||
}
|
||||
|
||||
#[ignore = "haven't done API calls or stdlib yet"]
|
||||
#[test]
|
||||
fn stdlib_api_calls() {
|
||||
|
@ -15,7 +15,7 @@ databake = "0.1.7"
|
||||
kcl-lib = { path = "../kcl" }
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2.0.52", features = ["full"] }
|
||||
syn = { version = "2.0.49", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.0"
|
||||
|
@ -14,7 +14,7 @@ keywords = ["kcl", "KittyCAD", "CAD"]
|
||||
anyhow = { version = "1.0.79", features = ["backtrace"] }
|
||||
async-recursion = "1.0.5"
|
||||
async-trait = "0.1.77"
|
||||
clap = { version = "4.5.1", features = ["cargo", "derive", "env", "unicode"], optional = true }
|
||||
clap = { version = "4.5.0", features = ["cargo", "derive", "env", "unicode"], optional = true }
|
||||
dashmap = "5.5.3"
|
||||
databake = { version = "0.1.7", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.8" }
|
||||
|
@ -96,19 +96,7 @@ impl Program {
|
||||
let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
|
||||
Some(noncodes) => noncodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, custom_white_space_or_comment)| {
|
||||
let formatted = custom_white_space_or_comment.format(&indentation);
|
||||
if i == 0 && !formatted.trim().is_empty() {
|
||||
if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
|
||||
format!("\n{}", formatted)
|
||||
} else {
|
||||
formatted
|
||||
}
|
||||
} else {
|
||||
formatted
|
||||
}
|
||||
})
|
||||
.map(|custom_white_space_or_comment| custom_white_space_or_comment.format(&indentation))
|
||||
.collect::<String>(),
|
||||
None => String::new(),
|
||||
};
|
||||
@ -171,35 +159,6 @@ impl Program {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a non code meta that includes the given character position.
|
||||
pub fn get_non_code_meta_for_position(&self, pos: usize) -> Option<&NonCodeMeta> {
|
||||
// Check if its in the body.
|
||||
if self.non_code_meta.contains(pos) {
|
||||
return Some(&self.non_code_meta);
|
||||
}
|
||||
let Some(item) = self.get_body_item_for_position(pos) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Recurse over the item.
|
||||
let value = match item {
|
||||
BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression),
|
||||
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_value_for_position(pos),
|
||||
BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument),
|
||||
};
|
||||
|
||||
// Check if the value's non code meta contains the position.
|
||||
if let Some(value) = value {
|
||||
if let Some(non_code_meta) = value.get_non_code_meta() {
|
||||
if non_code_meta.contains(pos) {
|
||||
return Some(non_code_meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns all the lsp symbols in the program.
|
||||
pub fn get_lsp_symbols(&self, code: &str) -> Vec<DocumentSymbol> {
|
||||
let mut symbols = vec![];
|
||||
@ -472,24 +431,6 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the non code meta for the value.
|
||||
pub fn get_non_code_meta(&self) -> Option<&NonCodeMeta> {
|
||||
match self {
|
||||
Value::BinaryExpression(_bin_exp) => None,
|
||||
Value::ArrayExpression(_array_exp) => None,
|
||||
Value::ObjectExpression(_obj_exp) => None,
|
||||
Value::MemberExpression(_mem_exp) => None,
|
||||
Value::Literal(_literal) => None,
|
||||
Value::FunctionExpression(_func_exp) => None,
|
||||
Value::CallExpression(_call_exp) => None,
|
||||
Value::Identifier(_ident) => None,
|
||||
Value::PipeExpression(pipe_exp) => Some(&pipe_exp.non_code_meta),
|
||||
Value::UnaryExpression(_unary_exp) => None,
|
||||
Value::PipeSubstitution(_pipe_substitution) => None,
|
||||
Value::None(_none) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_value(&mut self, source_range: SourceRange, new_value: Value) {
|
||||
if source_range == self.clone().into() {
|
||||
*self = new_value;
|
||||
@ -795,10 +736,6 @@ pub struct NonCodeNode {
|
||||
}
|
||||
|
||||
impl NonCodeNode {
|
||||
pub fn contains(&self, pos: usize) -> bool {
|
||||
self.start <= pos && pos <= self.end
|
||||
}
|
||||
|
||||
pub fn value(&self) -> String {
|
||||
match &self.value {
|
||||
NonCodeValue::InlineComment { value, style: _ } => value.clone(),
|
||||
@ -818,27 +755,18 @@ impl NonCodeNode {
|
||||
value,
|
||||
style: CommentStyle::Block,
|
||||
} => format!(" /* {} */", value),
|
||||
NonCodeValue::BlockComment { value, style } => match style {
|
||||
CommentStyle::Block => format!("{}/* {} */", indentation, value),
|
||||
CommentStyle::Line => {
|
||||
if value.trim().is_empty() {
|
||||
format!("{}//\n", indentation)
|
||||
} else {
|
||||
format!("{}// {}\n", indentation, value.trim())
|
||||
}
|
||||
NonCodeValue::BlockComment { value, style } => {
|
||||
let add_start_new_line = if self.start == 0 { "" } else { "\n" };
|
||||
match style {
|
||||
CommentStyle::Block => format!("{}{}/* {} */", add_start_new_line, indentation, value),
|
||||
CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
|
||||
}
|
||||
},
|
||||
}
|
||||
NonCodeValue::NewLineBlockComment { value, style } => {
|
||||
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
|
||||
match style {
|
||||
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
|
||||
CommentStyle::Line => {
|
||||
if value.trim().is_empty() {
|
||||
format!("{}{}//\n", add_start_new_line, indentation)
|
||||
} else {
|
||||
format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
|
||||
}
|
||||
}
|
||||
CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
|
||||
}
|
||||
}
|
||||
NonCodeValue::NewLine => "\n\n".to_string(),
|
||||
@ -935,16 +863,6 @@ impl NonCodeMeta {
|
||||
pub fn insert(&mut self, i: usize, new: NonCodeNode) {
|
||||
self.non_code_nodes.entry(i).or_default().push(new);
|
||||
}
|
||||
|
||||
pub fn contains(&self, pos: usize) -> bool {
|
||||
if self.start.iter().any(|node| node.contains(pos)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.non_code_nodes
|
||||
.iter()
|
||||
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
|
||||
@ -2615,13 +2533,7 @@ impl PipeExpression {
|
||||
let non_code_meta = self.non_code_meta.clone();
|
||||
if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
|
||||
for val in non_code_meta_value {
|
||||
let formatted = val.format(&indentation).trim_end_matches('\n').to_string();
|
||||
if let NonCodeValue::BlockComment { .. } = val.value {
|
||||
s += "\n";
|
||||
s += &formatted;
|
||||
} else {
|
||||
s += &formatted;
|
||||
}
|
||||
s += val.format(&indentation).trim_end_matches('\n')
|
||||
}
|
||||
}
|
||||
|
||||
@ -3091,7 +3003,8 @@ let baz = {a: 1, b: "thing"}
|
||||
fn ghi = (x) => {
|
||||
return x
|
||||
}
|
||||
"#;
|
||||
|
||||
show(part001)"#;
|
||||
let tokens = crate::token::lexer(code);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
@ -3193,109 +3106,6 @@ fn ghi = (x) => {
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_comment_under_variable() {
|
||||
let some_program_string = r#"const key = 'c'
|
||||
// this is also a comment
|
||||
const thing = 'foo'
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
r#"const key = 'c'
|
||||
// this is also a comment
|
||||
const thing = 'foo'
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_multiline_comment_start_file() {
|
||||
let some_program_string = r#"// hello world
|
||||
// I am a comment
|
||||
const key = 'c'
|
||||
// this is also a comment
|
||||
// hello
|
||||
const thing = 'foo'
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
r#"// hello world
|
||||
// I am a comment
|
||||
const key = 'c'
|
||||
// this is also a comment
|
||||
// hello
|
||||
const thing = 'foo'
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_empty_comment() {
|
||||
let some_program_string = r#"// hello world
|
||||
//
|
||||
// I am a comment
|
||||
const key = 'c'
|
||||
|
||||
//
|
||||
// I am a comment
|
||||
const thing = 'c'
|
||||
|
||||
const foo = 'bar' //
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
r#"// hello world
|
||||
//
|
||||
// I am a comment
|
||||
const key = 'c'
|
||||
|
||||
//
|
||||
// I am a comment
|
||||
const thing = 'c'
|
||||
|
||||
const foo = 'bar' //
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_multiline_comment_under_variable() {
|
||||
let some_program_string = r#"const key = 'c'
|
||||
// this is also a comment
|
||||
// hello
|
||||
const thing = 'foo'
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
r#"const key = 'c'
|
||||
// this is also a comment
|
||||
// hello
|
||||
const thing = 'foo'
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_comment_at_start() {
|
||||
let test_program = r#"
|
||||
@ -3371,7 +3181,9 @@ const mySk1 = startSketchOn('XY')
|
||||
offset: -1.35,
|
||||
intersectTag: 'seg01'
|
||||
}, %)
|
||||
|> line([-0.42, -1.72], %)"#;
|
||||
|> line([-0.42, -1.72], %)
|
||||
|
||||
show(part001)"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
@ -3520,7 +3332,8 @@ let baz = {a: 1, part001: "thing"}
|
||||
fn ghi = (part001) => {
|
||||
return part001
|
||||
}
|
||||
"#;
|
||||
|
||||
show(part001)"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let mut program = parser.ast().unwrap();
|
||||
@ -3542,6 +3355,8 @@ let baz = { a: 1, part001: "thing" }
|
||||
fn ghi = (part001) => {
|
||||
return part001
|
||||
}
|
||||
|
||||
show(mySuperCoolPart)
|
||||
"#
|
||||
);
|
||||
}
|
||||
@ -3566,97 +3381,6 @@ fn ghi = (part001) => {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_trailing_comma() {
|
||||
let some_program_string = r#"startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> arc({
|
||||
radius: 1,
|
||||
angle_start: 0,
|
||||
angle_end: 180,
|
||||
}, %)"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
r#"startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> arc({
|
||||
radius: 1,
|
||||
angle_start: 0,
|
||||
angle_end: 180
|
||||
}, %)
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ast_get_non_code_node() {
|
||||
let some_program_string = r#"const r = 20 / pow(pi(), 1 / 3)
|
||||
const h = 30
|
||||
|
||||
// st
|
||||
const cylinder = startSketchOn('-XZ')
|
||||
|> startProfileAt([50, 0], %)
|
||||
|> arc({
|
||||
angle_end: 360,
|
||||
angle_start: 0,
|
||||
radius: r
|
||||
}, %)
|
||||
|> extrude(h, %)
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let value = program.get_non_code_meta_for_position(50);
|
||||
|
||||
assert!(value.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ast_get_non_code_node_pipe() {
|
||||
let some_program_string = r#"const r = 20 / pow(pi(), 1 / 3)
|
||||
const h = 30
|
||||
|
||||
// st
|
||||
const cylinder = startSketchOn('-XZ')
|
||||
|> startProfileAt([50, 0], %)
|
||||
// comment
|
||||
|> arc({
|
||||
angle_end: 360,
|
||||
angle_start: 0,
|
||||
radius: r
|
||||
}, %)
|
||||
|> extrude(h, %)
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let value = program.get_non_code_meta_for_position(124);
|
||||
|
||||
assert!(value.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ast_get_non_code_node_inline_comment() {
|
||||
let some_program_string = r#"const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> xLine(5, %) // lin
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let value = program.get_non_code_meta_for_position(86);
|
||||
|
||||
assert!(value.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_negative_var() {
|
||||
let some_program_string = r#"const w = 20
|
||||
@ -3670,7 +3394,8 @@ const firstExtrude = startSketchOn('XY')
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
"#;
|
||||
|
||||
show(firstExtrude)"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
@ -3689,48 +3414,8 @@ const firstExtrude = startSketchOn('XY')
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_multiline_comment() {
|
||||
let some_program_string = r#"const w = 20
|
||||
const l = 8
|
||||
const h = 10
|
||||
|
||||
// This is my comment
|
||||
// It has multiple lines
|
||||
// And it's really long
|
||||
const firstExtrude = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(
|
||||
recasted,
|
||||
r#"const w = 20
|
||||
const l = 8
|
||||
const h = 10
|
||||
|
||||
// This is my comment
|
||||
// It has multiple lines
|
||||
// And it's really long
|
||||
const firstExtrude = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
show(firstExtrude)
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
@ -566,4 +566,17 @@ 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
|
||||
//! engine.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use dashmap::DashMap;
|
||||
@ -15,12 +15,6 @@ use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum SocketHealth {
|
||||
Active,
|
||||
Inactive,
|
||||
}
|
||||
|
||||
type WebSocketTcpWrite = futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<reqwest::Upgraded>, WsMsg>;
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)] // for the TcpReadHandle
|
||||
@ -28,7 +22,6 @@ pub struct EngineConnection {
|
||||
engine_req_tx: mpsc::Sender<ToEngineReq>,
|
||||
responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>>,
|
||||
tcp_read_handle: Arc<TcpReadHandle>,
|
||||
socket_health: Arc<Mutex<SocketHealth>>,
|
||||
}
|
||||
|
||||
pub struct TcpRead {
|
||||
@ -126,9 +119,7 @@ impl EngineConnection {
|
||||
|
||||
let responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>> = Arc::new(DashMap::new());
|
||||
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 {
|
||||
// Get Websocket messages from API server
|
||||
loop {
|
||||
@ -140,7 +131,6 @@ impl EngineConnection {
|
||||
}
|
||||
Err(e) => {
|
||||
println!("got ws error: {:?}", e);
|
||||
*socket_health_tcp_read.lock().unwrap() = SocketHealth::Inactive;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@ -153,7 +143,6 @@ impl EngineConnection {
|
||||
handle: Arc::new(tcp_read_handle),
|
||||
}),
|
||||
responses,
|
||||
socket_health,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -203,14 +192,6 @@ impl EngineManager for EngineConnection {
|
||||
// Wait for the response.
|
||||
let current_time = std::time::Instant::now();
|
||||
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.
|
||||
if let Some((_, resp)) = self.responses.remove(&id) {
|
||||
return if let Some(data) = &resp.resp {
|
||||
|
@ -1017,7 +1017,7 @@ impl ExecutorContext {
|
||||
pub async fn execute(
|
||||
program: crate::ast::types::Program,
|
||||
memory: &mut ProgramMemory,
|
||||
_options: BodyType,
|
||||
options: BodyType,
|
||||
ctx: &ExecutorContext,
|
||||
) -> Result<ProgramMemory, KclError> {
|
||||
// Before we even start executing the program, set the units.
|
||||
@ -1073,11 +1073,24 @@ pub async fn execute(
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
let _show_fn = Box::new(crate::std::Show);
|
||||
match ctx.stdlib.get_either(&call_expr.callee.name) {
|
||||
FunctionKind::Core(func) => {
|
||||
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));
|
||||
use crate::docs::StdLibFn;
|
||||
if func.name() == _show_fn.name() {
|
||||
if options != BodyType::Root {
|
||||
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) => {
|
||||
let mut newmem = memory.clone();
|
||||
@ -1339,7 +1352,8 @@ const newVar = myVar + 1"#;
|
||||
offset: {},
|
||||
tag: "yo2"
|
||||
}}, %)
|
||||
const intersect = segEndX('yo2', part001)"#,
|
||||
const intersect = segEndX('yo2', part001)
|
||||
show(part001)"#,
|
||||
offset
|
||||
)
|
||||
};
|
||||
@ -1385,7 +1399,8 @@ const part001 = startSketchOn('XY')
|
||||
|> angledLine([ghi(2), 3.04], %)
|
||||
|> angledLine([jkl(yo) + 2, 3.05], %)
|
||||
|> close(%)
|
||||
const yo2 = hmm([identifierGuy + 5])"#;
|
||||
const yo2 = hmm([identifierGuy + 5])
|
||||
show(part001)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1400,7 +1415,8 @@ const part001 = startSketchOn('XY')
|
||||
min(segLen('seg01', %), myVar),
|
||||
-legLen(segLen('seg01', %), myVar)
|
||||
], %)
|
||||
"#;
|
||||
|
||||
show(part001)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1415,7 +1431,8 @@ const part001 = startSketchOn('XY')
|
||||
min(segLen('seg01', %), myVar),
|
||||
legLen(segLen('seg01', %), myVar)
|
||||
], %)
|
||||
"#;
|
||||
|
||||
show(part001)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1437,7 +1454,8 @@ const part001 = startSketchOn('XY')
|
||||
|> xLine(3.84, %) // selection-range-7ish-before-this
|
||||
|
||||
const variableBelowShouldNotBeIncluded = 3
|
||||
"#;
|
||||
|
||||
show(part001)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1458,7 +1476,9 @@ const firstExtrude = startSketchOn('XY')
|
||||
|> line([w, 0], %)
|
||||
|> line([0, thing()], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)"#;
|
||||
|> extrude(h, %)
|
||||
|
||||
show(firstExtrude)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1479,7 +1499,9 @@ const firstExtrude = startSketchOn('XY')
|
||||
|> line([w, 0], %)
|
||||
|> line([0, thing(8)], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)"#;
|
||||
|> extrude(h, %)
|
||||
|
||||
show(firstExtrude)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1500,7 +1522,9 @@ const firstExtrude = startSketchOn('XY')
|
||||
|> line([w, 0], %)
|
||||
|> line(thing(8), %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)"#;
|
||||
|> extrude(h, %)
|
||||
|
||||
show(firstExtrude)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1525,7 +1549,9 @@ const firstExtrude = startSketchOn('XY')
|
||||
|> line([w, 0], %)
|
||||
|> line([0, thing(8)], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)"#;
|
||||
|> extrude(h, %)
|
||||
|
||||
show(firstExtrude)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1544,7 +1570,9 @@ const firstExtrude = startSketchOn('XY')
|
||||
return myBox
|
||||
}
|
||||
|
||||
const fnBox = box(3, 6, 10)"#;
|
||||
const fnBox = box(3, 6, 10)
|
||||
|
||||
show(fnBox)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1564,6 +1592,8 @@ const fnBox = box(3, 6, 10)"#;
|
||||
}
|
||||
|
||||
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
|
||||
|
||||
show(thisBox)
|
||||
"#;
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1583,6 +1613,8 @@ 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();
|
||||
}
|
||||
@ -1602,6 +1634,8 @@ 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();
|
||||
}
|
||||
@ -1623,6 +1657,7 @@ 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}] {
|
||||
const thisBox = box(var)
|
||||
show(thisBox)
|
||||
}"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
@ -1646,6 +1681,7 @@ 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]]] {
|
||||
const thisBox = box(var[0], var[1], var[2], var[3])
|
||||
show(thisBox)
|
||||
}"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
@ -1667,6 +1703,7 @@ for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
|
||||
|
||||
const thisBox = box([[0,0], 6, 10, 3])
|
||||
|
||||
show(thisBox)
|
||||
"#;
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1783,6 +1820,7 @@ const bracket = startSketchOn('XY')
|
||||
|> line([0, -1 * leg1 + thickness], %)
|
||||
|> close(%)
|
||||
|> extrude(width, %)
|
||||
show(bracket)
|
||||
"#;
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
@ -1807,6 +1845,7 @@ const bracket = startSketchOn('XY')
|
||||
|> line([0, -1 * leg1 + thickness], %)
|
||||
|> close(%)
|
||||
|> extrude(width, %)
|
||||
show(bracket)
|
||||
"#;
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|