Compare commits

...

26 Commits

Author SHA1 Message Date
866cbfc6e3 Merge branch 'main' into zoom-to-fit-on-object-pop 2024-08-07 10:17:25 +10:00
5cb5dbd689 artifactGraph snapshot stability (#3305)
* artifactgraph snapshot stability

* clean up

* tweak

* Look at this (photo)Graph *in the voice of Nickelback*

* trigger ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-07 10:15:08 +10:00
b7085d50b7 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-08-06 22:09:09 +00:00
8783151be7 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-08-06 22:07:09 +00:00
be03b2c51d zoom to fit on object pop 2024-08-07 08:02:20 +10:00
0a8881bc69 badge scale on hover (#3298) 2024-08-07 07:29:16 +10:00
be9438160e bisect docs (#3304) 2024-08-07 06:27:23 +10:00
77b565f781 Add a search bar to the projects/home page (#3301)
* Add a search bar to the projects/home page

* Better hotkey config

* Look at this (photo)Graph *in the voice of Nickelback*

* Re-run CI

* Look at this (photo)Graph *in the voice of Nickelback*

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-06 16:19:30 -04:00
c843dfad95 Add a little dropdown arrow menu to gizmo with view settings (#3300) 2024-08-06 10:01:55 -04:00
a3ff0a45eb Disable build-test-web's tests on release or nightly build (#3296) 2024-08-06 05:50:49 -04:00
4617ad0fed Cut release v0.24.9 (#3295) 2024-08-06 05:30:34 -04:00
5fa51a2f92 Revert "Cut release v0.24.9"
This reverts commit 4218777afb.
2024-08-06 17:34:11 +10:00
4218777afb Cut release v0.24.9 2024-08-06 17:32:39 +10:00
8b1b462e29 Revert "Cut release v0.24.9 (#3284)" (#3294)
This reverts commit 1c778bf373.
2024-08-06 17:32:18 +10:00
2bc99ba39b reset camera on empty scene (#3293)
* reset camera on empty scene

* tweak numbers

* number tweak again
2024-08-06 17:05:46 +10:00
ffe0da6dcd don't allow edit on sketches with no variable declaration (#3292)
don't allow edit on sketches with no variable decleration
2024-08-06 06:17:30 +00:00
d27afb8c74 Improve docs for x/yLine and x/yLineTo (#3285)
* Improve docs for xLineTo and yLineTo

* Improve docs for xLine and yLine
2024-08-06 14:26:19 +10:00
1c778bf373 Cut release v0.24.9 (#3284)
* Cut release v0.24.9

* Look at this (photo)Graph *in the voice of Nickelback*

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-06 13:13:34 +10:00
5df8a943a9 Easy KCL tests with no engine/visuals (#3282)
This new test framework, `no_visuals`, is for testing KCL programs via asserts, not via twenty-twenty visual tests. This is useful for unit-testing small fragments of KCL.

It's easy! All you need to do is:
- Write a KCL file
- Save it under `tests/executor/inputs/no_visuals/foo.kcl`
- Open `no_visuals.rs` and add `gen_test!(foo);`
2024-08-06 02:44:49 +00:00
ab729dbcdb Bump google-github-actions/upload-cloud-storage from 2.1.0 to 2.1.1 (#3272)
Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 10:13:00 -07:00
84865eaed0 Add assertEqual function to KCL stdlib (#3279)
Takes a tolerance, because floating-point imprecision.
2024-08-05 16:31:58 +00:00
543e809739 Add "report a bug" mention to user menu onboarding step (#3278)
* Mention "report a bug" in user menu onboarding step

* Add to a test so we match QAWolf a bit more

* Re-run CI
2024-08-05 08:24:19 -04:00
61b669cf4e github actions Playwright shard execution (#3199)
* add @snapshot tag to all snapshot-tests

* set workers to 1 on CI

* try sharding on google chrome only

* add retry when navigating to the app (on CI)

* reduce ubuntu-cores to 2

* revert runner back to 8 cores

* re-add retry + enable macos

* Reduce timeouts to 30 minutes

* ensure retry is triggered

* removed sharding when retrying failures

* added --pass-with-no-tests

* ensure failure occurs

* revert failure

* use smaller sized runners

* revert back to supported runner size

* revert to og version

* minimize changes

* yarn fmt

* ensure failure

* undo failure

---------

Co-authored-by: ryanrosello-og <ry@zoo.dev>
2024-08-05 21:30:16 +10:00
75f1aaa824 camera breaks on extrude zoom to fit (#3276) 2024-08-05 16:08:30 +10:00
f4848d7dea Jump to error not lint (#3271)
* dont jump to lints

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-05 02:44:33 +00:00
a0167f6ba6 Coplanar sketch should have diagnostic error. (#3269)
* artifactMapTweak

* typo
2024-08-05 02:04:53 +00:00
56 changed files with 1377 additions and 699 deletions

View File

@ -89,16 +89,20 @@ jobs:
- run: yarn build:wasm
- run: yarn simpleserver:ci
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
- name: Install Chromium Browser
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn playwright install chromium --with-deps
- name: run unit tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn test:nowatch
env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
id: git-check
run: |
git add src/lang/std/artifactMapGraphs
@ -107,7 +111,7 @@ jobs:
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: steps.git-check.outputs.modified == 'true'
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' && steps.git-check.outputs.modified == 'true' }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
@ -531,7 +535,7 @@ jobs:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: artifact
glob: '*/Zoo*'
@ -539,13 +543,13 @@ jobs:
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}

View File

@ -13,7 +13,7 @@ permissions:
contents: write
pull-requests: write
actions: read
jobs:
@ -34,8 +34,13 @@ jobs:
- 'src/wasm-lib/**'
playwright-ubuntu:
timeout-minutes: 60
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
@ -107,7 +112,7 @@ jobs:
run: yarn build:local
- name: Run ubuntu/chrome snapshots
run: |
yarn playwright test --project="Google Chrome" --retries="3" --update-snapshots e2e/playwright/snapshot-tests.spec.ts
yarn playwright test --project="Google Chrome" --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
@ -115,7 +120,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-ubuntu-snapshot-${{ github.sha }}
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
@ -149,7 +154,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true'
with:
name: playwright-report-ubuntu-${{ github.sha }}
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
# if have previous run results, use them
@ -157,7 +162,7 @@ jobs:
if: always()
continue-on-error: true
with:
name: test-results-ubuntu-${{ github.sha }}
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run ubuntu/chrome flow (with retries)
id: retry
@ -166,14 +171,14 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="Google Chrome" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
@ -181,7 +186,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="Google Chrome" --last-failed --grep-invert=@snapshot || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -194,9 +199,9 @@ jobs:
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
@ -216,21 +221,26 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-ubuntu-${{ github.sha }}
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-ubuntu-${{ github.sha }}
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
playwright-macos:
timeout-minutes: 60
runs-on: macos-14-large
timeout-minutes: 30
runs-on: macos-14
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
@ -306,7 +316,7 @@ jobs:
if: ${{ always() }}
continue-on-error: true
with:
name: test-results-macos-${{ github.sha }}
name: test-results-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run macos/safari flow (with retries)
id: retry
@ -315,14 +325,14 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="webkit" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
@ -330,7 +340,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="webkit" --last-failed --grep-invert=@snapshot || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -343,9 +353,9 @@ jobs:
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
@ -360,15 +370,14 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: test-results-macos-${{ github.sha }}
name: test-results-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: playwright-report-macos-${{ github.sha }}
name: playwright-report-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true

View File

@ -110,6 +110,17 @@ Note that these became separate apps on Macos, so make sure you open the right o
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
## Checking out commits / Bisecting
Which commands from setup are one off vs need to be run every time?
The following will need to be run when checking out a new commit and guarantees the build is not stale:
```bash
yarn install
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
yarn start # or yarn build:local && yarn serve for slower but more production-like build
```
## Before submitting a PR
Before you submit a contribution PR to this repo, please ensure that:

37
docs/kcl/assertEqual.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,7 @@ layout: manual
* [`arc`](kcl/arc)
* [`asin`](kcl/asin)
* [`assert`](kcl/assert)
* [`assertEqual`](kcl/assertEqual)
* [`assertGreaterThan`](kcl/assertGreaterThan)
* [`assertGreaterThanOrEq`](kcl/assertGreaterThanOrEq)
* [`assertLessThan`](kcl/assertLessThan)

View File

@ -54791,6 +54791,62 @@
"const myVar = true\nassert(myVar, \"should always be true\")"
]
},
{
"name": "assertEqual",
"summary": "Check that a numerical value equals another at runtime,",
"description": "otherwise raise an error.",
"tags": [],
"args": [
{
"name": "left",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
{
"name": "right",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
{
"name": "epsilon",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
{
"name": "message",
"type": "string",
"schema": {
"type": "string"
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "()",
"schema": {
"type": "null"
},
"required": true
},
"unpublished": false,
"deprecated": false,
"examples": [
"let n = 1.0285\nlet m = 1.0286\nassertEqual(n, m, 0.01, \"n is within the given tolerance for m\")"
]
},
{
"name": "assertGreaterThan",
"summary": "Check that a numerical value is greater than another at runtime,",
@ -223435,7 +223491,7 @@
},
{
"name": "xLine",
"summary": "Draw a line on the x-axis.",
"summary": "Draw a line parallel to the X-axis, with the given length.",
"description": "",
"tags": [],
"args": [
@ -230069,8 +230125,8 @@
},
{
"name": "xLineTo",
"summary": "Draw a line to a point on the x-axis.",
"description": "",
"summary": "Draw a line parallel to the X axis, that ends at the given X.",
"description": "E.g. if the previous line ended at (1, 1), then xLineTo(4) draws a line from (1, 1) to (4, 1)",
"tags": [],
"args": [
{
@ -236703,7 +236759,7 @@
},
{
"name": "yLine",
"summary": "Draw a line on the y-axis.",
"summary": "Draw a line parallel to the Y-axis, with the given length.",
"description": "",
"tags": [],
"args": [
@ -243337,8 +243393,8 @@
},
{
"name": "yLineTo",
"summary": "Draw a line to a point on the y-axis.",
"description": "",
"summary": "Draw a line parallel to the Y axis, that ends at the given Y.",
"description": "E.g. if the previous line ended at (1, 1), then yLineTo(4) draws a line from (1, 1) to (1, 4)",
"tags": [],
"args": [
{

View File

@ -1,10 +1,10 @@
---
title: "xLine"
excerpt: "Draw a line on the x-axis."
excerpt: "Draw a line parallel to the X-axis, with the given length."
layout: manual
---
Draw a line on the x-axis.
Draw a line parallel to the X-axis, with the given length.

View File

@ -1,12 +1,12 @@
---
title: "xLineTo"
excerpt: "Draw a line to a point on the x-axis."
excerpt: "Draw a line parallel to the X axis, that ends at the given X."
layout: manual
---
Draw a line to a point on the x-axis.
Draw a line parallel to the X axis, that ends at the given X.
E.g. if the previous line ended at (1, 1), then xLineTo(4) draws a line from (1, 1) to (4, 1)
```js
xLineTo(to: number, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,10 +1,10 @@
---
title: "yLine"
excerpt: "Draw a line on the y-axis."
excerpt: "Draw a line parallel to the Y-axis, with the given length."
layout: manual
---
Draw a line on the y-axis.
Draw a line parallel to the Y-axis, with the given length.

View File

@ -1,12 +1,12 @@
---
title: "yLineTo"
excerpt: "Draw a line to a point on the y-axis."
excerpt: "Draw a line parallel to the Y axis, that ends at the given Y."
layout: manual
---
Draw a line to a point on the y-axis.
Draw a line parallel to the Y axis, that ends at the given Y.
E.g. if the previous line ended at (1, 1), then yLineTo(4) draws a line from (1, 1) to (1, 4)
```js
yLineTo(to: number, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1020,7 +1020,7 @@ test.describe('Editor tests', () => {
|> line([0, -10], %, $revolveAxis)
|> close(%)
|> extrude(10, %)
const sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %)
|> line([0, -10], %)
@ -2535,18 +2535,29 @@ test.describe('Onboarding tests', () => {
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
// Test that the text in this step is correct
const avatarLocator = await page
.getByTestId('user-sidebar-toggle')
.locator('img')
const onboardingOverlayLocator = await page
const sidebar = page.getByTestId('user-sidebar-toggle')
const avatar = sidebar.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatarLocator).not.toBeVisible()
await expect(avatar).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
// Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939
// which doesn't deserver its own full test spun up
const userMenuFeatures = [
'manage your account',
'report a bug',
'request a feature',
'sign out',
]
for (const feature of userMenuFeatures) {
await expect(onboardingOverlayLocator).toContainText(feature)
}
})
})
@ -3894,6 +3905,39 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
test.describe('Regression tests', () => {
// bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({ page }) => {
// because the model has `line([0,0]..` it is valid code, but the model is invalid
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch2 = startSketchOn("XY")
const sketch001 = startSketchAt([-0, -0])
|> line([0, 0], %)
|> line([-4.84, -5.29], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
// this is a cryptic error message, fact that all the lines are co-linear from the `line([0,0])` is the issue why
// the close doesn't work
// when https://github.com/KittyCAD/modeling-app/issues/3268 is closed
// this test will need updating
const crypticErrorText = `ApiError`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
})
test('executes on load', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -7559,7 +7603,7 @@ test.describe('Testing Gizmo', () => {
})
}
test('Context menu', async ({ page }) => {
test('Context menu and popover menu', async ({ page }) => {
const testCase = {
testDescription: 'Right view',
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
@ -7654,6 +7698,16 @@ test.describe('Testing Gizmo', () => {
testCase.expectedCameraTarget.z.toString()
),
])
// Now test the popover menu.
// It has the same click handlers, so we can just
// test that it opens and contains the same content.
const gizmoPopoverButton = page.getByRole('button', {
name: 'view settings',
})
await expect(gizmoPopoverButton).toBeVisible()
await gizmoPopoverButton.click()
await expect(buttonToTest).toBeVisible()
})
})
@ -8333,6 +8387,83 @@ test.describe('Code pane and errors', () => {
await badge.click()
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
// Hover over the error to see the error message
await page.hover('.cm-lint-marker-error')
await expect(
page
.getByText(
'sketch profile must lie entirely on one side of the revolution axis'
)
.first()
).toBeVisible()
})
test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({
page,
}) => {
const u = await getUtils(page)
// Load the app with the working starter code
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(1000)
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have no errors in the gutter, since error out of view.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// click in the editor to focus it
await page.locator('.cm-content').click()
await page.waitForTimeout(500)
// go to the start of the editor and enter more text which will trigger
// a lint error.
// GO to the start of the editor.
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.type('const foo_bar = 1')
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
// ensure we have a lint error
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// Click the badge.
const badge = page.locator('#code-badge')
await expect(badge).toBeVisible()
await badge.click()
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
// Hover over the error to see the error message
await page.hover('.cm-lint-marker-error')
await expect(
page
.getByText(
'sketch profile must lie entirely on one side of the revolution axis'
)
.first()
).toBeVisible()
})
})

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -337,7 +337,24 @@ fn svg = (surface, origin, depth) => {
|> close(%)
|> extrude(depth, %)
"thing";kajsnd;akjsnd
const box = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %, $revolveAxis)
|> close(%)
|> extrude(10, %)
const sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %)
|> line([0, -10], %)
|> line([2, 0], %)
|> line([0, -10], %)
|> close(%)
|> revolve({
axis: revolveAxis,
angle: 90
}, %)
return 0
}

View File

@ -15,6 +15,23 @@ export const TEST_COLORS = {
BLUE: [0, 0, 255] as TestColor,
} as const
async function waitForPageLoadWithRetry(page: Page) {
await expect(async () => {
await page.goto('/')
const errorMessage = 'App failed to load - 🔃 Retrying ...'
await expect(page.getByTestId('loading'), errorMessage).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' }),
errorMessage
).toBeEnabled({
timeout: 20_000,
})
}).toPass({ timeout: 70_000, intervals: [1_000] })
}
async function waitForPageLoad(page: Page) {
// wait for all spinners to be gone
await expect(page.getByTestId('loading')).not.toBeAttached({
@ -218,9 +235,12 @@ async function waitForAuthAndLsp(page: Page) {
}
return false
})
await page.goto('/')
await waitForPageLoad(page)
if (process.env.CI) {
await waitForPageLoadWithRetry(page)
} else {
await page.goto('/')
await waitForPageLoad(page)
}
return waitForLspPromise
}
@ -234,6 +254,7 @@ export async function getUtils(page: Page) {
return {
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
waitForPageLoad: () => waitForPageLoad(page),
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => {

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.24.8",
"version": "0.24.9",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",

View File

@ -18,7 +18,7 @@ export default defineConfig({
/* Do not retry */
retries: process.env.CI ? 0 : 0,
/* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 4 : 4,
workers: process.env.CI ? 1 : 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
[process.env.CI ? 'dot' : 'list'],

View File

@ -80,5 +80,5 @@
}
},
"productName": "Zoo Modeling App",
"version": "0.24.8"
"version": "0.24.9"
}

View File

@ -1,6 +1,6 @@
import { SceneInfra } from 'clientSideScene/sceneInfra'
import { sceneInfra } from 'lib/singletons'
import { MutableRefObject, useEffect, useRef } from 'react'
import { MutableRefObject, useEffect, useMemo, useRef } from 'react'
import {
WebGLRenderer,
Scene,
@ -25,6 +25,8 @@ import {
ContextMenuItem,
ContextMenuItemRefresh,
} from './ContextMenu'
import { Popover } from '@headlessui/react'
import { CustomIcon } from './CustomIcon'
const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5
@ -59,6 +61,30 @@ export default function Gizmo() {
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
const cameraPassiveUpdateTimer = useRef(0)
const raycasterPassiveUpdateTimer = useRef(0)
const menuItems = useMemo(
() => [
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames)
}}
>
{axisSemantic} view
</ContextMenuItem>
)),
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition()
}}
>
Reset view
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
],
[axisNamesSemantic]
)
useEffect(() => {
if (!canvasRef.current) return
@ -115,43 +141,48 @@ export default function Gizmo() {
}, [])
return (
<>
<div className="relative">
<div
ref={wrapperRef}
aria-label="View orientation gizmo"
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
>
<canvas ref={canvasRef} />
<ContextMenu
menuTargetElement={wrapperRef}
items={[
...Object.entries(axisNamesSemantic).map(
([axisName, axisSemantic]) => (
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls.updateCameraToAxis(
axisName as AxisNames
)
}}
>
{axisSemantic} view
</ContextMenuItem>
)
),
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition()
}}
>
Reset view
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
]}
/>
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} />
</div>
</>
<GizmoDropdown items={menuItems} />
</div>
)
}
function GizmoDropdown({ items }: { items: React.ReactNode[] }) {
return (
<Popover className="absolute top-0 right-0 pointer-events-auto">
{({ close }) => (
<>
<Popover.Button className="border-none p-0 m-0 -translate-y-1/4 translate-x-1/4">
<CustomIcon
name="caretDown"
className="w-4 h-4 ui-open:rotate-180"
/>
<span className="sr-only">View settings</span>
</Popover.Button>
<Popover.Panel
className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded
shadow-lg`}
>
<ul className="relative flex flex-col items-stretch content-stretch p-0.5">
{items.map((item, index) => (
<li key={index} className="contents" onClick={() => close()}>
{item}
</li>
))}
</ul>
</Popover.Panel>
</>
)}
</Popover>
)
}

View File

@ -22,7 +22,7 @@ import {
historyKeymap,
history,
} from '@codemirror/commands'
import { lintGutter, lintKeymap } from '@codemirror/lint'
import { diagnosticCount, lintGutter, lintKeymap } from '@codemirror/lint'
import {
foldGutter,
foldKeymap,
@ -196,7 +196,10 @@ export const KclEditorPane = () => {
// On first load of this component, ensure we show the current errors
// in the editor.
kclManager.setDiagnosticsForCurrentErrors()
// Make sure we don't add them twice.
if (diagnosticCount(_editorView.state) === 0) {
kclManager.setDiagnosticsForCurrentErrors()
}
}}
/>
</div>

View File

@ -63,7 +63,7 @@ export const sidebarPanes: SidebarPane[] = [
},
onClick: (e) => {
e.preventDefault()
editorManager.scrollToFirstDiagnosticIfExists()
editorManager.scrollToFirstErrorDiagnosticIfExists()
},
},
},

View File

@ -295,7 +295,7 @@ function ModelingPaneButton({
<p
id={`${paneConfig.id}-badge`}
className={
'absolute m-0 p-0 top-1 right-0 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer'
'absolute m-0 p-0 top-1 right-0 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
}
onClick={showBadge.onClick}
title={`Click to view ${showBadge.value} notification${

View File

@ -0,0 +1,63 @@
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { CustomIcon } from './CustomIcon'
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import Fuse from 'fuse.js'
export function useProjectSearch(projects: Project[]) {
const [query, setQuery] = useState('')
const [searchResults, setSearchResults] = useState(projects)
const fuse = new Fuse(projects, {
keys: [{ name: 'name', weight: 0.7 }],
includeScore: true,
})
useEffect(() => {
const results = fuse.search(query).map((result) => result.item)
setSearchResults(query.length > 0 ? results : projects)
}, [query, projects])
return {
searchResults,
query,
setQuery,
}
}
export function ProjectSearchBar({
setQuery,
}: {
setQuery: (query: string) => void
}) {
const inputRef = useRef<HTMLInputElement>(null)
useHotkeys(
'Ctrl+.',
(event) => {
event.preventDefault()
inputRef.current?.focus()
},
{ enableOnFormTags: true }
)
return (
<div className="relative group">
<div className="flex items-center gap-2 py-0.5 pl-0.5 pr-2 rounded border-solid border border-primary/10 dark:border-chalkboard-80 focus-within:border-primary dark:focus-within:border-chalkboard-30">
<CustomIcon
name="search"
className="w-5 h-5 rounded-sm bg-primary/10 dark:bg-transparent text-primary dark:text-chalkboard-10 group-focus-within:bg-primary group-focus-within:text-chalkboard-10"
/>
<input
ref={inputRef}
onChange={(event) => setQuery(event.target.value)}
className="w-full text-sm bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
placeholder="Search projects (^.)"
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
/>
</div>
</div>
)
}

View File

@ -141,14 +141,14 @@ export default class EditorManager {
})
}
scrollToFirstDiagnosticIfExists() {
scrollToFirstErrorDiagnosticIfExists() {
if (!this._editorView) return
let firstDiagnosticPos: [number, number] | null = null
forEachDiagnostic(
this._editorView.state,
(d: Diagnostic, from: number, to: number) => {
if (!firstDiagnosticPos) {
if (!firstDiagnosticPos && d.severity === 'error') {
firstDiagnosticPos = [from, to]
}
}
@ -161,7 +161,11 @@ export default class EditorManager {
selection: EditorSelection.create([
EditorSelection.cursor(firstDiagnosticPos[0]),
]),
effects: [EditorView.scrollIntoView(firstDiagnosticPos[0])],
effects: [
EditorView.scrollIntoView(
EditorSelection.range(firstDiagnosticPos[0], firstDiagnosticPos[1])
),
],
annotations: [
updateOutsideEditorEvent,
Transaction.addToHistory.of(false),

View File

@ -791,6 +791,7 @@ export function isSingleCursorInPipe(
const pathToNode = getNodePathFromSourceRange(ast, selection.range)
const nodeTypes = pathToNode.map(([, type]) => type)
if (nodeTypes.includes('FunctionExpression')) return false
if (!nodeTypes.includes('VariableDeclaration')) return false
if (nodeTypes.includes('PipeExpression')) return true
return false
}

View File

@ -465,30 +465,45 @@ async function GraphTheGraph(
await browser.close()
const img1Path = path.resolve(`./src/lang/std/artifactMapGraphs/${imageName}`)
const img2Path = path.resolve('./e2e/playwright/temp3.png')
const originalImgPath = path.resolve(
`./src/lang/std/artifactMapGraphs/${imageName}`
)
// chop the top 30 pixels off the image
const originalImg = PNG.sync.read(fs.readFileSync(originalImgPath))
// const img1Data = new Uint8Array(img1.data)
// const img1DataChopped = img1Data.slice(30 * img1.width * 4)
// img1.data = Buffer.from(img1DataChopped)
const img1 = PNG.sync.read(fs.readFileSync(img1Path))
const img2 = PNG.sync.read(fs.readFileSync(img2Path))
const newImagePath = path.resolve('./e2e/playwright/temp3.png')
const newImage = PNG.sync.read(fs.readFileSync(newImagePath))
const newImageData = new Uint8Array(newImage.data)
const newImageDataChopped = newImageData.slice(30 * newImage.width * 4)
newImage.data = Buffer.from(newImageDataChopped)
const { width, height } = img1
const { width, height } = originalImg
const diff = new PNG({ width, height })
const numDiffPixels = pixelmatch(
img1.data,
img2.data,
diff.data,
width,
height,
{ threshold: 0.1 }
)
const imageSizeDifferent = originalImg.data.length !== newImage.data.length
let numDiffPixels = 0
if (!imageSizeDifferent) {
numDiffPixels = pixelmatch(
originalImg.data,
newImage.data,
diff.data,
width,
height,
{
threshold: 0.1,
}
)
}
if (numDiffPixels > 10) {
if (numDiffPixels > 10 || imageSizeDifferent) {
console.warn('numDiffPixels', numDiffPixels)
// write file out to final place
fs.writeFileSync(
`src/lang/std/artifactMapGraphs/${imageName}`,
PNG.sync.write(img2)
PNG.sync.write(newImage)
)
}
}

View File

@ -240,6 +240,7 @@ export function getArtifactsToUpdate({
const response = responseMap[id]
const cmd = command.cmd
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr
if (cmd.type === 'enable_sketch_mode') {
const plane = getArtifact(currentPlaneId)
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
@ -316,7 +317,7 @@ export function getArtifactsToUpdate({
artifact: { ...path, segIds: [id] },
})
if (
response.type === 'modeling' &&
response?.type === 'modeling' &&
response.data.modeling_response.type === 'close_path'
) {
returnArr.push({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 371 KiB

View File

@ -2,7 +2,7 @@ import { Program, SourceRange } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { deferExecution, uuidv4 } from 'lib/utils'
import { deferExecution, isOverlap, uuidv4 } from 'lib/utils'
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import {
@ -1899,15 +1899,10 @@ export class EngineCommandManager extends EventTarget {
range: SourceRange,
commandTypeToTarget: string
): string | undefined {
const values = Object.entries(this.artifactGraph)
for (const [id, data] of values) {
// // Our range selection seems to just select the cursor position, so either
// // of these can be right...
if (
(data.range[0] === range[0] || data.range[1] === range[1]) &&
data.type === commandTypeToTarget
)
return id
for (const [artifactId, artifact] of this.artifactGraph) {
if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) {
if (commandTypeToTarget === artifact.type) return artifactId
}
}
return undefined
}

View File

@ -4,6 +4,7 @@ import EditorManager from 'editor/manager'
import { KclManager } from 'lang/KclSingleton'
import CodeManager from 'lang/codeManager'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { uuidv4 } from './utils'
export const codeManager = new CodeManager()
@ -40,4 +41,14 @@ if (typeof window !== 'undefined') {
;(window as any).enableFillet = () => {
;(window as any)._enableFillet = true
}
;(window as any).zoomToFit = () =>
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
object_ids: [], // leave empty to zoom to all objects
padding: 0.2, // padding around the objects
},
})
}

View File

@ -358,7 +358,7 @@ export const modelingMachine = createMachine(
'Artifact graph populated': 'showPlanes',
},
entry: 'hide default planes',
entry: ['hide default planes', 'zoom to fit'],
},
showPlanes: {
@ -366,7 +366,7 @@ export const modelingMachine = createMachine(
'Artifact graph emptied': 'hidePlanes',
},
entry: 'show default planes',
entry: ['show default planes', 'reset camera position'],
},
},
@ -1063,6 +1063,27 @@ export const modelingMachine = createMachine(
sketchEnginePathId: '',
sketchPlaneId: '',
}),
'zoom to fit': () =>
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'zoom_to_fit',
padding: 0.1,
object_ids: [],
},
}),
'reset camera position': () =>
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: 0, y: -1250, z: 580 },
up: { x: 0, y: 0, z: 1 },
},
}),
'set new sketch metadata': assign((_, { data }) => ({
sketchDetails: data,
})),
@ -1101,11 +1122,13 @@ export const modelingMachine = createMachine(
store.videoElement?.pause()
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToExtrudeArg,
zoomToFit: true,
zoomOnRangeAndType: {
range: selection.codeBasedSelections[0].range,
type: 'start_path',
},
// commented out as a part of https://github.com/KittyCAD/modeling-app/issues/3270
// looking to add back in the future
// zoomToFit: true,
// zoomOnRangeAndType: {
// range: selection.codeBasedSelections[0].range,
// type: 'path',
// },
})
if (!engineCommandManager.engineConnection?.idleMode) {
store.videoElement?.play().catch((e) => {

View File

@ -39,6 +39,7 @@ import {
listProjects,
renameProjectDirectory,
} from 'lib/tauri'
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
// This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -154,6 +155,7 @@ const Home = () => {
})
const { projects } = state.context
const [searchParams, setSearchParams] = useSearchParams()
const { searchResults, query, setQuery } = useProjectSearch(projects)
const sort = searchParams.get('sort_by') ?? 'modified:desc'
const isSortByModified = sort?.includes('modified') || !sort || sort === null
@ -206,8 +208,8 @@ const Home = () => {
<AppHeader showToolbar={false} />
<div className="w-full flex flex-col overflow-hidden max-w-5xl px-4 mx-auto mt-24 lg:px-2">
<section>
<div className="flex justify-between items-baseline select-none">
<div className="flex gap-8 items-baseline">
<div className="flex justify-between items-center select-none">
<div className="flex gap-8 items-center">
<h1 className="text-3xl font-bold">Your Projects</h1>
<ActionButton
Element="button"
@ -225,6 +227,7 @@ const Home = () => {
</ActionButton>
</div>
<div className="flex gap-2 items-center">
<ProjectSearchBar setQuery={setQuery} />
<small>Sort by</small>
<ActionButton
Element="button"
@ -289,9 +292,9 @@ const Home = () => {
<Loading>Loading your Projects...</Loading>
) : (
<>
{projects.length > 0 ? (
{searchResults.length > 0 ? (
<ul className="grid w-full grid-cols-4 gap-4">
{projects.sort(getSortFunction(sort)).map((project) => (
{searchResults.sort(getSortFunction(sort)).map((project) => (
<ProjectCard
key={project.name}
project={project}
@ -302,7 +305,10 @@ const Home = () => {
</ul>
) : (
<p className="p-4 my-8 border border-dashed rounded border-chalkboard-30 dark:border-chalkboard-70">
No Projects found, ready to make your first one?
No Projects found
{projects.length === 0
? ', ready to make your first one?'
: ` with the search term "${query}"`}
</p>
)}
</>

View File

@ -43,8 +43,8 @@ export default function UserMenu() {
<h2 className="text-2xl font-bold">User Menu</h2>
<p className="my-4">
Click {buttonDescription} in the upper right to open the user menu.
You can change your user-level settings, sign out, or request a
feature.
You can change your user-level settings, sign out, report a bug,
manage your account, request a feature, and more.
</p>
<p className="my-4">
Many settings can be set either a user or per-project level. User

View File

@ -2862,7 +2862,7 @@ impl MemberExpression {
// Actually evaluate memory to compute the property.
let prop = memory.get(&name, property_src)?;
let MemoryItem::UserVal(prop) = prop else {
return Err(KclError::Syntax(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
@ -2876,17 +2876,17 @@ impl MemberExpression {
.and_then(|x| usize::try_from(x).ok())
.map(Property::Number)
.ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
"{name}'s value is not a valid property/index, you can only use a string or int (>= 0) here",
),
})
})?
}
JValue::String(ref x) => Property::String(x.to_owned()),
_ => {
return Err(KclError::Syntax(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array",
@ -2903,7 +2903,7 @@ impl MemberExpression {
if let Ok(x) = u64::try_from(x) {
Property::Number(x.try_into().unwrap())
} else {
return Err(KclError::Syntax(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
}));
@ -2911,7 +2911,7 @@ impl MemberExpression {
}
LiteralValue::String(s) => Property::String(s),
_ => {
return Err(KclError::Syntax(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: "Only strings or ints (>= 0) can be properties/indexes".to_owned(),
}));
@ -2943,7 +2943,7 @@ impl MemberExpression {
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property {property} not found in object"),
message: format!("Property '{property}' not found in object"),
source_ranges: vec![self.clone().into()],
}))
}
@ -2978,10 +2978,13 @@ impl MemberExpression {
),
source_ranges: vec![self.clone().into()],
})),
(_, _) => Err(KclError::Semantic(KclErrorDetails {
message: "Only arrays and objects can be indexed".to_owned(),
source_ranges: vec![self.clone().into()],
})),
(being_indexed, _) => {
let t = human_friendly_type(being_indexed);
Err(KclError::Semantic(KclErrorDetails {
message: format!("Only arrays and objects can be indexed, but you're trying to index a {t}"),
source_ranges: vec![self.clone().into()],
}))
}
}
}
@ -4070,6 +4073,17 @@ impl ConstraintLevels {
}
}
fn human_friendly_type(j: JValue) -> &'static str {
match j {
JValue::Null => "null",
JValue::Bool(_) => "boolean (true/false value)",
JValue::Number(_) => "number",
JValue::String(_) => "string (text)",
JValue::Array(_) => "array (list)",
JValue::Object(_) => "object",
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

View File

@ -67,6 +67,27 @@ pub async fn assert_gt(args: Args) -> Result<MemoryItem, KclError> {
args.make_null_user_val()
}
/// Check that a numerical value equals another at runtime,
/// otherwise raise an error.
///
/// ```no_run
/// let n = 1.0285
/// let m = 1.0286
/// assertEqual(n, m, 0.01, "n is within the given tolerance for m")
/// ```
#[stdlib {
name = "assertEqual",
}]
async fn inner_assert_equal(left: f64, right: f64, epsilon: f64, message: &str, args: &Args) -> Result<(), KclError> {
_assert((right - left).abs() < epsilon, message, args).await
}
pub async fn assert_equal(args: Args) -> Result<MemoryItem, KclError> {
let (left, right, epsilon, description): (f64, f64, f64, String) = args.get_data()?;
inner_assert_equal(left, right, epsilon, &description, &args).await?;
args.make_null_user_val()
}
/// Check that a numerical value is greater than another at runtime,
/// otherwise raise an error.
///

View File

@ -120,6 +120,7 @@ lazy_static! {
Box::new(crate::std::math::ToRadians),
Box::new(crate::std::polar::Polar),
Box::new(crate::std::assert::Assert),
Box::new(crate::std::assert::AssertEqual),
Box::new(crate::std::assert::AssertLessThan),
Box::new(crate::std::assert::AssertGreaterThan),
Box::new(crate::std::assert::AssertLessThanOrEq),

View File

@ -168,7 +168,9 @@ pub async fn x_line_to(args: Args) -> Result<MemoryItem, KclError> {
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line to a point on the x-axis.
/// Draw a line parallel to the X axis, that ends at the given X.
/// E.g. if the previous line ended at (1, 1),
/// then xLineTo(4) draws a line from (1, 1) to (4, 1)
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')
@ -214,7 +216,9 @@ pub async fn y_line_to(args: Args) -> Result<MemoryItem, KclError> {
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line to a point on the y-axis.
/// Draw a line parallel to the Y axis, that ends at the given Y.
/// E.g. if the previous line ended at (1, 1),
/// then yLineTo(4) draws a line from (1, 1) to (1, 4)
///
/// ```no_run
/// const exampleSketch = startSketchOn("XZ")
@ -336,7 +340,7 @@ pub async fn x_line(args: Args) -> Result<MemoryItem, KclError> {
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line on the x-axis.
/// Draw a line parallel to the X-axis, with the given length.
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')
@ -378,7 +382,7 @@ pub async fn y_line(args: Args) -> Result<MemoryItem, KclError> {
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
/// Draw a line on the y-axis.
/// Draw a line parallel to the Y-axis, with the given length.
///
/// ```no_run
/// const exampleSketch = startSketchOn('XZ')

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -4,8 +4,7 @@ const arr = [0, 0, 0, 10]
const i = 3
const ten = arr[i]
assertLessThanOrEq(ten, 10, "oops")
assertGreaterThanOrEq(ten, 10, "oops2")
assertEqual(ten, 10, 0.000001, "oops")
const p = "foo"
const obj = {
@ -14,5 +13,4 @@ const obj = {
}
const one = obj[p]
assertLessThanOrEq(one, 1, "oops")
assertGreaterThanOrEq(one, 1, "oops2")
assertEqual(one, 1, 0.0000001, "oops")

View File

@ -0,0 +1,2 @@
let arr = []
let x = arr[0]

View File

@ -0,0 +1,18 @@
// This tests indexing an array.
const array = [90, 91, 92]
// Test: literal index.
const result0 = array[1]
assertLessThanOrEq(result0, 91, "Literal property lookup")
assertGreaterThanOrEq(result0, 91, "Literal property lookup")
// Test: computed index.
const i = int(1 + 0)
const result1 = array[i]
assertLessThanOrEq(result1, 91, "Computed property lookup")
assertGreaterThanOrEq(result1, 91, "Computed property lookup")

View File

@ -0,0 +1,2 @@
let arr = [1, 2, 3]
let x = arr[1.2]

View File

@ -0,0 +1,3 @@
let arr = [1, 2, 3]
let i = -1
let x = arr[i]

View File

@ -0,0 +1,2 @@
let arr = [1, 2, 3]
let x = arr["s"]

View File

@ -0,0 +1,2 @@
let num = 999
let x = num[3]

View File

@ -0,0 +1,2 @@
let b = true
let x = b["property"]

View File

@ -0,0 +1,2 @@
let obj = {key: 123}
let num = obj[3]

View File

@ -0,0 +1,2 @@
let obj = {}
let k = obj["age"]

View File

@ -0,0 +1,40 @@
// This tests evaluating properties of objects.
const obj = {
foo: 1,
bar: 0,
}
// Test: the property is a literal.
const one_a = obj["foo"]
assertLessThanOrEq(one_a, 1, "Literal property lookup")
assertGreaterThanOrEq(one_a, 1, "Literal property lookup")
// Test: the property is a variable,
// which must be evaluated before looking it up.
const p = "foo"
const one_b = obj[p]
assertLessThanOrEq(one_b, 1, "Computed property lookup")
assertGreaterThanOrEq(one_b, 1, "Computed property lookup")
// Test: multiple literal properties.
const obj2 = {
inner: obj,
}
const one_c = obj2.inner["foo"]
assertLessThanOrEq(one_c, 1, "Literal property lookup")
assertGreaterThanOrEq(one_c, 1, "Literal property lookup")
// Test: multiple properties, mix of literal and computed.
const one_d = obj2.inner[p]
assertLessThanOrEq(one_d, 1, "Computed property lookup")
assertGreaterThanOrEq(one_d, 1, "Computed property lookup")

View File

@ -4,7 +4,7 @@ use kcl_lib::{settings::types::UnitLength, test_server::execute_and_snapshot};
/// i.e. how different the current model snapshot can be from the previous saved one.
const MIN_DIFF: f64 = 0.99;
// mod server;
mod no_visuals;
macro_rules! kcl_input {
($file:literal) => {

View File

@ -0,0 +1,87 @@
use kcl_lib::{ast::types::Program, errors::KclError, executor::ExecutorContext};
macro_rules! gen_test {
($file:ident) => {
#[tokio::test]
async fn $file() {
let code = include_str!(concat!("inputs/no_visuals/", stringify!($file), ".kcl"));
run(&code).await;
}
};
}
macro_rules! gen_test_fail {
($file:ident, $expected:literal) => {
#[tokio::test]
async fn $file() {
let code = include_str!(concat!("inputs/no_visuals/", stringify!($file), ".kcl"));
let actual = run_fail(&code).await;
assert_eq!(actual.get_message(), $expected);
}
};
}
async fn run(code: &str) {
let (ctx, program) = setup(code).await;
ctx.run(&program, None).await.unwrap();
}
async fn setup(program: &str) -> (ExecutorContext, Program) {
let tokens = kcl_lib::token::lexer(program).unwrap();
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let ctx = kcl_lib::executor::ExecutorContext {
engine: std::sync::Arc::new(Box::new(
kcl_lib::engine::conn_mock::EngineConnection::new().await.unwrap(),
)),
fs: std::sync::Arc::new(kcl_lib::fs::FileManager::new()),
stdlib: std::sync::Arc::new(kcl_lib::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
};
(ctx, program)
}
async fn run_fail(code: &str) -> KclError {
let (ctx, program) = setup(code).await;
let Err(e) = ctx.run(&program, None).await else {
panic!("Expected this KCL program to fail, but it (incorrectly) never threw an error.");
};
e
}
gen_test!(property_of_object);
gen_test!(index_of_array);
gen_test_fail!(
invalid_index_str,
"semantic: Only integers >= 0 can be used as the index of an array, but you're using a string"
);
gen_test_fail!(
invalid_index_negative,
"semantic: i's value is not a valid property/index, you can only use a string or int (>= 0) here"
);
gen_test_fail!(
invalid_index_fractional,
"semantic: Only strings or ints (>= 0) can be properties/indexes"
);
gen_test_fail!(
invalid_member_object,
"semantic: Only arrays and objects can be indexed, but you're trying to index a number"
);
gen_test_fail!(
invalid_member_object_prop,
"semantic: Only arrays and objects can be indexed, but you're trying to index a boolean (true/false value)"
);
gen_test_fail!(
non_string_key_of_object,
"semantic: Only strings can be used as the property of an object, but you're using a number"
);
gen_test_fail!(
array_index_oob,
"undefined value: The array doesn't have any item at index 0"
);
gen_test_fail!(
object_prop_not_found,
"undefined value: Property 'age' not found in object"
);