Compare commits

...

34 Commits

Author SHA1 Message Date
239ab6850e Cut release v0.26.2 (#4322)
* Cut release v0.26.2

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-26 03:06:01 -04:00
4a7dd6e650 update env vars to match other repos, make dry (#4321)
* update env vars to match other repos, make dry

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

* bump

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-10-26 03:06:32 +00:00
af2609e678 Fix NetworkMachineIndicator and machines dynamically showing in CommandBar (#4311) 2024-10-25 16:28:10 -07:00
30909dedda Bump kittycad from 0.3.23 to 0.3.25 in /src/wasm-lib (#4316)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.3.23 to 0.3.25.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.3.23...v0.3.25)

---
updated-dependencies:
- dependency-name: kittycad
  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-10-25 22:58:59 +00:00
39d76ed54f Bugfix: arc paths were stored as straight line paths (#4310)
Problem 1 of https://github.com/KittyCAD/modeling-app/issues/4297
2024-10-25 22:49:30 +00:00
4925251c29 Make application aware it saved the buffer and not something else (#4314)
Make application aware it saved the buffer and something else
2024-10-25 18:07:50 -04:00
9772869545 Add a radius length indicator to the circle sketch tool (#4304)
* Add a radius length indicator to the circle sketch tool

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 15b078f641.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-10-25 17:42:27 -04:00
a7e830cd02 Update machine-api spec (#4305)
YOYO NEW API SPEC!

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 20:53:08 +00:00
ca102116b6 Bump kittycad-modeling-cmds from 0.2.70 to 0.2.71 in /src/wasm-lib (#4302)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.70 to 0.2.71.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.70...kittycad-modeling-cmds-0.2.71)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  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>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-10-25 20:52:22 +00:00
c2fba89e77 Bump regex from 1.11.0 to 1.11.1 in /src/wasm-lib (#4301)
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.0...1.11.1)

---
updated-dependencies:
- dependency-name: regex
  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-10-25 13:50:21 -07:00
7e31678ba2 Chore: Don't let draft lines receive mouseEnter/Leave events, or create invalid overlays (#4306) 2024-10-25 15:34:53 -04:00
1140ced121 Tags should refer to full paths, not just base paths. (#4299)
See "Problem 2" in https://github.com/KittyCAD/modeling-app/issues/4297

This is a pure refactor, it should not change any behaviour at all.
It adds more information into the tag system, but nothing reads that
extra information yet. It will be used to address problem 3 of the above
issue.
2024-10-25 10:27:40 -04:00
32b7ddaa7c Update circular pattern snapshots (#4303)
Engine changed (Serena fixed a bug with circular patterns), so snapshots need to be updated.
2024-10-25 08:28:46 -04:00
2525f99515 Move more KCL tests into files (#4260) 2024-10-25 01:52:43 +00:00
4b8ce34b31 Fix release notes (#4295) 2024-10-24 17:07:12 -04:00
6617f72373 Fix docs for how to patch kcmc (#4294) 2024-10-24 20:14:38 +00:00
e9033e1754 Cut release v0.26.1 (#4289) 2024-10-24 14:08:29 -04:00
9b697e30cf Revert "Separate debug/release electron-builder to help mac job (#4270)" (#4293)
This reverts commit 19d01c563e.
2024-10-24 13:07:43 -04:00
a70facdab4 Disable rotate on start new sketch (#4287)
disable rotate on start new sketch

Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-10-24 10:18:41 -04:00
4083f9f3dd Fix release notes in last_download.json (#4247)
* Fix release notes in last_download.json
Fixes #4246

* Fix job output

* Clean up, ready for merge
2024-10-24 09:47:44 -04:00
7ead2bb875 Stop propagation of camera clicks after drags (#4257)
* Update CameraControls.ts

* fix static analyzer error

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 0b63016217.

* Don't perform sketch click operations if a camera movement interaction also matches

* Don't `stopPropogation`, make selection listener early return if `wasDragging`
+ consolidate `wasDragging` set statements, add comments

* Codespell

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-10-24 09:26:33 -04:00
19d01c563e Separate debug/release electron-builder to help mac job (#4270)
* Separate debug/release electron-builder to help mac job
Will attempt to fix #4199

* Test BUILD_RELEASE: true

* Revert "Test BUILD_RELEASE: true"

This reverts commit f2c0c24432.
2024-10-24 08:10:16 -04:00
dfe7cfc91c Bump syn from 2.0.82 to 2.0.85 in /src/wasm-lib (#4283)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.82 to 2.0.85.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.82...2.0.85)

---
updated-dependencies:
- dependency-name: syn
  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-10-24 05:52:18 +00:00
01443e445d Bump kittycad-modeling-cmds from 0.2.68 to 0.2.70 in /src/wasm-lib (#4284)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.68 to 0.2.70.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.68...kittycad-modeling-cmds-0.2.70)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  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-10-24 03:53:07 +00:00
e16eb49f51 Bump serde from 1.0.210 to 1.0.213 in /src/wasm-lib (#4285)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.210 to 1.0.213.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.210...v1.0.213)

---
updated-dependencies:
- dependency-name: serde
  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-10-24 03:50:38 +00:00
5d5138e8e6 Bump syn from 2.0.79 to 2.0.82 in /src/wasm-lib (#4252)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.79 to 2.0.82.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.79...2.0.82)

---
updated-dependencies:
- dependency-name: syn
  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>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-23 23:48:18 +00:00
e1d6e29523 Update machine-api spec (#4241)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-23 23:41:48 +00:00
49657ad2e5 Bump @types/node from 22.5.0 to 22.7.8 (#4258)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.0 to 22.7.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 15:12:40 -07:00
b40d353994 Bump thiserror from 1.0.64 to 1.0.65 in /src/wasm-lib (#4268)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.64 to 1.0.65.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.64...1.0.65)

---
updated-dependencies:
- dependency-name: thiserror
  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-10-23 21:47:08 +00:00
62ffa53add Bump proc-macro2 from 1.0.88 to 1.0.89 in /src/wasm-lib (#4266)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.88 to 1.0.89.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.88...1.0.89)

---
updated-dependencies:
- dependency-name: proc-macro2
  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-10-23 21:46:23 +00:00
64dce4d8b1 Bump anyhow from 1.0.89 to 1.0.91 in /src/wasm-lib (#4269)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.89 to 1.0.91.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.89...1.0.91)

---
updated-dependencies:
- dependency-name: anyhow
  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-10-23 21:41:00 +00:00
02588b2672 Rename Sketch.value to Sketch.paths (#4272)
'value' is not a useful name
2024-10-23 17:42:54 +00:00
3d1ac2ac0b Fix engine connection break when starting onboarding from a fresh install (#4263)
* Make electron test setting overrides not entirely replace default settings

* Add failing test

* Fix test by checking for healthy engine connection before executing demo code

* Fix one electron test that assumed all settings got wiped if you override any.

* 🤷🏻‍♂️ an engine-side camera position in one of the E2E tests changed by 0.01 randomly
2024-10-22 20:22:52 -04:00
ff5ce29fd7 Delete ..env.development.local.swp (#4259) 2024-10-22 18:33:28 -04:00
79 changed files with 28203 additions and 11483 deletions

Binary file not shown.

View File

@ -15,6 +15,7 @@ on:
env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -25,7 +26,6 @@ jobs:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
notes: ${{ steps.export_version.outputs.notes }}
steps:
- uses: actions/checkout@v4
@ -55,8 +55,6 @@ jobs:
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
- name: Generate release notes
env:
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
run: |
echo "$NOTES" > release-notes.md
cat release-notes.md
@ -84,9 +82,6 @@ jobs:
path: |
electron-builder.yml
- id: export_notes
run: echo "notes=`cat release-notes.md'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
@ -262,7 +257,6 @@ jobs:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ needs.prepare-files.outputs.notes }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}

View File

@ -62,7 +62,7 @@ jobs:
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo llvm-cov nextest --all --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
cargo llvm-cov nextest --workspace --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000

File diff suppressed because it is too large Load Diff

View File

@ -162,6 +162,28 @@ A base path.
----
A circular arc, not necessarily tangential to the current point.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Arc`| | No |
| `center` |`[number, number]`| Center of the circle that this arc is drawn on. | No |
| `radius` |`number`| Radius of the circle that this arc is drawn on. | No |
| `from` |`[number, number]`| The from point. | No |
| `to` |`[number, number]`| The to point. | No |
| `tag` |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----

View File

@ -16,8 +16,8 @@ A sketch is a collection of paths.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes. | No |
| `value` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes). | No |
| `paths` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |

View File

@ -25,8 +25,8 @@ A sketch is a collection of paths.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `sketch`| | No |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes. | No |
| `value` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `id` |`string`| The id of the sketch (this will change when the engine's reference to it changes). | No |
| `paths` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |

View File

@ -18,7 +18,7 @@ Engine information for a tag.
|----------|------|-------------|----------|
| `id` |`string`| The id of the tagged object. | No |
| `sketch` |`string`| The sketch the tag is on. | No |
| `path` |[`BasePath`](/docs/kcl/types/BasePath)| The path the tag is on. | No |
| `path` |[`Path`](/docs/kcl/types/Path)| The path the tag is on. | No |
| `surface` |[`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface)| The surface information for the tag. | No |

View File

@ -55,6 +55,53 @@ test.describe('Onboarding tests', () => {
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
})
test(
'Desktop: fresh onboarding executes and loads',
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
})
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
// Locators and constants
const newProjectButton = page.getByRole('button', { name: 'New project' })
const projectLink = page.getByTestId('project-link')
await test.step(`Create a project and open to the onboarding`, async () => {
await newProjectButton.click()
await projectLink.click()
await test.step(`Ensure the engine connection works by testing the sketch button`, async () => {
await u.waitForPageLoad()
})
})
await test.step(`Ensure we see the onboarding stuff`, async () => {
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
})
await electronApp.close()
}
)
test('Code resets after confirmation', async ({ page }) => {
const initialCode = `sketch001 = startSketchOn('XZ')`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -888,7 +888,17 @@ export async function setupElectron({
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify(
appSettings
? { settings: appSettings }
? {
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
projectDirectory: projectDirName,
...appSettings.app,
},
},
}
: {
settings: {
...TEST_SETTINGS,

View File

@ -292,7 +292,7 @@ test.describe(`Testing gizmo, fixture-based`, () => {
await test.step(`Verify the camera moved`, async () => {
await scene.expectState({
camera: {
position: [0, -23865.37, 11073.54],
position: [0, -23865.37, 11073.53],
target: [0, 0, 0],
},
})

View File

@ -430,7 +430,6 @@ test.describe('Testing settings', () => {
await test.step('Check color of logo changed when in modeling view', async () => {
await page.getByRole('button', { name: 'New project' }).click()
await page.getByTestId('project-link').first().click()
await page.getByRole('button', { name: 'Dismiss' }).click()
await changeColor('58')
await expect(logoLink).toHaveCSS('--primary-hue', '58')
})

2
interface.d.ts vendored
View File

@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
import fsSync from 'node:fs'
import path from 'path'
import { dialog, shell } from 'electron'
import { MachinesListing } from 'lib/machineManager'
import { MachinesListing } from 'components/MachineManagerProvider'
type EnvFn = (value?: string) => string

View File

@ -107,6 +107,13 @@
},
"type": "array"
},
"loaded_filament_idx": {
"description": "The currently loaded filament index.",
"format": "uint",
"minimum": 0,
"nullable": true,
"type": "integer"
},
"nozzle_diameter": {
"description": "Diameter of the extrusion nozzle, in mm.",
"format": "double",
@ -285,6 +292,21 @@
"type"
],
"type": "object"
},
{
"description": "Unknown material",
"properties": {
"type": {
"enum": [
"unknown"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
}
]
},
@ -974,7 +996,7 @@
},
"description": "",
"title": "machine-api",
"version": "0.1.0"
"version": "0.1.1"
},
"openapi": "3.0.3",
"paths": {

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.26.0",
"version": "0.26.2",
"private": true,
"productName": "Zoo Modeling App",
"author": {
@ -161,7 +161,7 @@
"@types/isomorphic-fetch": "^0.0.39",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.6",
"@types/node": "^22.5.0",
"@types/node": "^22.7.8",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react": "^18.3.4",

View File

@ -21,6 +21,7 @@ import { WasmErrBanner } from 'components/WasmErrBanner'
import { CommandBar } from 'components/CommandBar/CommandBar'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import FileMachineProvider from 'components/FileMachineProvider'
import { MachineManagerProvider } from 'components/MachineManagerProvider'
import { PATHS } from 'lib/paths'
import {
fileLoader,
@ -49,6 +50,7 @@ const router = createRouter([
{
loader: settingsLoader,
id: PATHS.INDEX,
// TODO: Re-evaluate if this is true
/* Make sure auth is the outermost provider or else we will have
* inefficient re-renders, use the react profiler to see. */
element: (
@ -57,7 +59,9 @@ const router = createRouter([
<LspProvider>
<KclContextProvider>
<AppStateProvider>
<Outlet />
<MachineManagerProvider>
<Outlet />
</MachineManagerProvider>
</AppStateProvider>
</KclContextProvider>
</LspProvider>

View File

@ -92,6 +92,7 @@ export class CameraControls {
target: Vector3
domElement: HTMLCanvasElement
isDragging: boolean
wasDragging: boolean
mouseDownPosition: Vector2
mouseNewPosition: Vector2
rotationSpeed = 0.3
@ -233,6 +234,7 @@ export class CameraControls {
this.target = new Vector3()
this.domElement = domElement
this.isDragging = false
this.wasDragging = false
this.mouseDownPosition = new Vector2()
this.mouseNewPosition = new Vector2()
@ -363,6 +365,8 @@ export class CameraControls {
onMouseDown = (event: PointerEvent) => {
this.domElement.setPointerCapture(event.pointerId)
this.isDragging = true
// Reset the wasDragging flag to false when starting a new drag
this.wasDragging = false
this.mouseDownPosition.set(event.clientX, event.clientY)
let interaction = this.getInteractionType(event)
if (interaction === 'none') return
@ -392,6 +396,10 @@ export class CameraControls {
const interaction = this.getInteractionType(event)
if (interaction === 'none') return
// If there's a valid interaction and the mouse is moving,
// our past (and current) interaction was a drag.
this.wasDragging = true
if (this.syncDirection === 'engineToClient') {
this.moveSender.send(() => {
this.doMove(interaction, [event.clientX, event.clientY])
@ -399,6 +407,7 @@ export class CameraControls {
return
}
// else "clientToEngine" (Sketch Mode) or forceUpdate
// Implement camera movement logic here based on deltaMove
// For example, for rotating the camera around the target:
if (interaction === 'rotate') {
@ -427,6 +436,9 @@ export class CameraControls {
* under the cursor. This recently moved from being handled in App.tsx.
* This might not be the right spot, but it is more consolidated.
*/
// Clear any previous drag state
this.wasDragging = false
if (this.syncDirection === 'engineToClient') {
const newCmdId = uuidv4()

View File

@ -338,6 +338,11 @@ export class SceneEntities {
sceneInfra.setCallbacks({
onClick: async (args) => {
if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
if (!intersectionPoint?.twoD || !sketchDetails?.sketchPathToNode) return
@ -407,7 +412,7 @@ export class SceneEntities {
if (err(sketch)) return Promise.reject(sketch)
if (!sketch) return Promise.reject('sketch not found')
if (!isArray(sketch?.value))
if (!isArray(sketch?.paths))
return {
truncatedAst,
programMemoryOverride,
@ -435,7 +440,7 @@ export class SceneEntities {
maybeModdedAst,
sketch.start.__geoMeta.sourceRange
)
if (sketch?.value?.[0]?.type !== 'Circle') {
if (sketch?.paths?.[0]?.type !== 'Circle') {
const _profileStart = createProfileStartHandle({
from: sketch.start.from,
id: sketch.start.__geoMeta.id,
@ -451,16 +456,16 @@ export class SceneEntities {
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
}
const callbacks: (() => SegmentOverlayPayload | null)[] = []
sketch.value.forEach((segment, index) => {
sketch.paths.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
segment.__geoMeta.sourceRange
)
if (
draftExpressionsIndices &&
(sketch.value[index - 1] || sketch.start)
(sketch.paths[index - 1] || sketch.start)
) {
const previousSegment = sketch.value[index - 1] || sketch.start
const previousSegment = sketch.paths[index - 1] || sketch.start
const previousSegmentPathToNode = getNodePathFromSourceRange(
maybeModdedAst,
previousSegment.__geoMeta.sourceRange
@ -511,7 +516,7 @@ export class SceneEntities {
to: segment.to,
}
const result = initSegment({
prevSegment: sketch.value[index - 1],
prevSegment: sketch.paths[index - 1],
callExpName,
input,
id: segment.__geoMeta.id,
@ -610,9 +615,9 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sg)) return Promise.reject(sg)
const lastSeg = sg?.value?.slice(-1)[0] || sg.start
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const index = sg.paths.length // because we've added a new segment that's not in the memory yet, no need for `-1`
const mod = addNewSketchLn({
node: _ast,
programMemory: kclManager.programMemory,
@ -645,7 +650,13 @@ export class SceneEntities {
sceneInfra.setCallbacks({
onClick: async (args) => {
if (!args) return
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args.mouseEvent.which !== 1) return
const { intersectionPoint } = args
let intersection2d = intersectionPoint?.twoD
const profileStart = args.intersects
@ -654,7 +665,7 @@ export class SceneEntities {
let modifiedAst
if (profileStart) {
const lastSegment = sketch.value.slice(-1)[0]
const lastSegment = sketch.paths.slice(-1)[0]
modifiedAst = addCallExpressionsToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -686,7 +697,7 @@ export class SceneEntities {
})
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
} else if (intersection2d) {
const lastSegment = sketch.value.slice(-1)[0]
const lastSegment = sketch.paths.slice(-1)[0]
const tmp = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -735,7 +746,6 @@ export class SceneEntities {
},
})
},
...this.mouseEnterLeaveCallbacks(),
})
}
setupDraftRectangle = async (
@ -817,7 +827,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.value
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
@ -826,6 +836,11 @@ export class SceneEntities {
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return
@ -868,7 +883,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.value
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
@ -985,7 +1000,7 @@ export class SceneEntities {
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.value
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
@ -994,6 +1009,11 @@ export class SceneEntities {
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
// Commit the rectangle to the full AST/code and return to sketch.idle
const cornerPoint = args.intersectionPoint?.twoD
if (!cornerPoint || args.mouseEvent.button !== 0) return
@ -1105,7 +1125,7 @@ export class SceneEntities {
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
if (addingNewSegmentStatus === 'nothing') {
const prevSegment = sketch.value[pipeIndex - 2]
const prevSegment = sketch.paths[pipeIndex - 2]
const mod = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
@ -1157,6 +1177,11 @@ export class SceneEntities {
},
onMove: () => {},
onClick: (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
args.mouseEvent
)
if (interaction !== 'none') return
if (args?.mouseEvent.which !== 1) return
if (!args || !args.selected) {
sceneInfra.modelingSend({
@ -1345,7 +1370,7 @@ export class SceneEntities {
}
if (!sketch) return
const sgPaths = sketch.value
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(
@ -1393,7 +1418,7 @@ export class SceneEntities {
modifiedAst,
segment.__geoMeta.sourceRange
)
const sgPaths = sketch.value
const sgPaths = sketch.paths
const originalPathToNodeStr = JSON.stringify(segPathToNode)
segPathToNode[1][0] = varDecIndex
const pathToNodeStr = JSON.stringify(segPathToNode)
@ -1701,7 +1726,7 @@ function prepareTruncatedMemoryAndAst(
variableDeclarationName
)
if (err(sg)) return sg
const lastSeg = sg?.value.slice(-1)[0]
const lastSeg = sg?.paths.slice(-1)[0]
if (draftSegment) {
// truncatedAst needs to setup with another segment at the end
let newSegment

View File

@ -213,7 +213,7 @@ export class SceneInfra {
to: Coords2d
angle?: number
}): SegmentOverlayPayload | null {
if (group.userData.pathToNode && arrowGroup) {
if (!group.userData.draft && group.userData.pathToNode && arrowGroup) {
const vector = new Vector3(0, 0, 0)
// Get the position of the object3D in world space

View File

@ -58,7 +58,7 @@ import { err } from 'lib/trap'
interface CreateSegmentArgs {
input: SegmentInputs
prevSegment: Sketch['value'][number]
prevSegment: Sketch['paths'][number]
id: string
pathToNode: PathToNode
isDraftSegment?: boolean
@ -72,7 +72,7 @@ interface CreateSegmentArgs {
interface UpdateSegmentArgs {
input: SegmentInputs
prevSegment: Sketch['value'][number]
prevSegment: Sketch['paths'][number]
group: Group
sceneInfra: SceneInfra
scale?: number
@ -147,6 +147,7 @@ class StraightSegment implements SegmentUtils {
segmentGroup.name = STRAIGHT_SEGMENT
segmentGroup.userData = {
type: STRAIGHT_SEGMENT,
draft: isDraftSegment,
id,
from,
to,
@ -347,6 +348,7 @@ class TangentialArcToSegment implements SegmentUtils {
mesh.name = meshName
group.userData = {
type: TANGENTIAL_ARC_TO_SEGMENT,
draft: isDraftSegment,
id,
from,
to,
@ -515,11 +517,18 @@ class CircleSegment implements SegmentUtils {
const meshType = isDraftSegment ? CIRCLE_SEGMENT_DASH : CIRCLE_SEGMENT_BODY
const arrowGroup = createArrowhead(scale, theme, color)
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
// A radius indicator that appears from the center to the perimeter
const radiusIndicatorGroup = createLengthIndicator({
from: center,
to: [center[0] + radius, center[1]],
scale,
})
arcMesh.userData.type = meshType
arcMesh.name = meshType
group.userData = {
type: CIRCLE_SEGMENT,
draft: isDraftSegment,
id,
from,
radius,
@ -532,7 +541,7 @@ class CircleSegment implements SegmentUtils {
}
group.name = CIRCLE_SEGMENT
group.add(arcMesh, arrowGroup, circleCenterGroup)
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
const updateOverlaysCallback = this.update({
prevSegment,
input,
@ -564,6 +573,9 @@ class CircleSegment implements SegmentUtils {
group.userData.radius = radius
group.userData.prevSegment = prevSegment
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const radiusLengthIndicator = group.getObjectByName(
SEGMENT_LENGTH_LABEL
) as Group
const circleCenterHandle = group.getObjectByName(
CIRCLE_CENTER_HANDLE
) as Group
@ -581,11 +593,14 @@ class CircleSegment implements SegmentUtils {
}
if (arrowGroup) {
arrowGroup.position.set(
center[0] + Math.cos(Math.PI / 4) * radius,
center[1] + Math.sin(Math.PI / 4) * radius,
0
)
// The arrowhead is placed at the perimeter of the circle,
// pointing up and to the right
const arrowPoint = {
x: center[0] + Math.cos(Math.PI / 4) * radius,
y: center[1] + Math.sin(Math.PI / 4) * radius,
}
arrowGroup.position.set(arrowPoint.x, arrowPoint.y, 0)
const arrowheadAngle = Math.PI / 4
arrowGroup.quaternion.setFromUnitVectors(
@ -596,6 +611,31 @@ class CircleSegment implements SegmentUtils {
arrowGroup.visible = isHandlesVisible
}
if (radiusLengthIndicator) {
// The radius indicator is placed at the midpoint of the radius,
// at a 45 degree CCW angle from the positive X-axis
const indicatorPoint = {
x: center[0] + (Math.cos(Math.PI / 4) * radius) / 2,
y: center[1] + (Math.sin(Math.PI / 4) * radius) / 2,
}
const labelWrapper = radiusLengthIndicator.getObjectByName(
SEGMENT_LENGTH_LABEL_TEXT
) as CSS2DObject
const labelWrapperElem = labelWrapper.element as HTMLDivElement
const label = labelWrapperElem.children[0] as HTMLParagraphElement
label.innerText = `${roundOff(radius)}`
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
const isPlaneBackFace = center[0] > indicatorPoint.x
label.style.setProperty(
'--degree',
`${isPlaneBackFace ? '45' : '-45'}deg`
)
label.style.setProperty('--x', `0px`)
label.style.setProperty('--y', `0px`)
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
radiusLengthIndicator.visible = isHandlesVisible
}
if (circleCenterHandle) {
circleCenterHandle.position.set(center[0], center[1], 0)
circleCenterHandle.scale.set(scale, scale, scale)

View File

@ -140,6 +140,13 @@ const FileTreeItem = ({
async (eventType, path) => {
// Don't try to read a file that was removed.
if (isCurrentFile && eventType !== 'unlink') {
// Prevents a cyclic read / write causing editor problems such as
// misplaced cursor positions.
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
return
}
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code)

View File

@ -23,6 +23,7 @@ export function LowerRightControls({
}) {
const location = useLocation()
const filePath = useAbsoluteFilePath()
const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'

View File

@ -0,0 +1,123 @@
import { createContext, useEffect, useState } from 'react'
import { engineCommandManager } from 'lib/singletons'
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
export type MachinesListing = Array<
components['schemas']['MachineInfoResponse']
>
export interface MachineManager {
machines: MachinesListing
machineApiIp: string | null
currentMachine: components['schemas']['MachineInfoResponse'] | null
noMachinesReason: () => string | undefined
setCurrentMachine: (
m: components['schemas']['MachineInfoResponse'] | null
) => void
}
export const MachineManagerContext = createContext<MachineManager>({
machines: [],
machineApiIp: null,
currentMachine: null,
setCurrentMachine: (
_: components['schemas']['MachineInfoResponse'] | null
) => {},
noMachinesReason: () => undefined,
})
export const MachineManagerProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [machines, setMachines] = useState<MachinesListing>([])
const [machineApiIp, setMachineApiIp] = useState<string | null>(null)
const [currentMachine, setCurrentMachine] = useState<
components['schemas']['MachineInfoResponse'] | null
>(null)
const commandBarActor = CommandsContext.useActorRef()
// Get the reason message for why there are no machines.
const noMachinesReason = (): string | undefined => {
if (machines.length > 0) {
return undefined
}
if (machineApiIp === null) {
return 'Machine API server was not discovered'
}
return 'Machine API server was discovered, but no machines are available'
}
useEffect(() => {
if (!isDesktop()) return
const update = async () => {
const _machineApiIp = await window.electron.getMachineApiIp()
if (_machineApiIp === null) return
setMachineApiIp(_machineApiIp)
const _machines = await window.electron.listMachines(_machineApiIp)
setMachines(_machines)
}
// Start a background job to update the machines every ten seconds.
// If MDNS is already watching, this timeout will wait until it's done to trigger the
// finding again.
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
const timeoutLoop = () => {
clearTimeout(timeoutId)
timeoutId = setTimeout(
toSync(async () => {
await update()
timeoutLoop()
}, reportRejection),
1000
)
}
timeoutLoop()
update().catch(reportRejection)
}, [])
// Update engineCommandManager's copy of this data.
useEffect(() => {
const machineManagerNext = {
machines,
machineApiIp,
currentMachine,
noMachinesReason,
setCurrentMachine,
}
engineCommandManager.machineManager = machineManagerNext
commandBarActor.send({
type: 'Set machine manager',
data: machineManagerNext,
})
}, [machines, machineApiIp, currentMachine])
return (
<MachineManagerContext.Provider
value={{
machines,
machineApiIp,
currentMachine,
setCurrentMachine,
noMachinesReason,
}}
>
{' '}
{children}{' '}
</MachineManagerContext.Provider>
)
}

View File

@ -1,5 +1,11 @@
import { useMachine } from '@xstate/react'
import React, { createContext, useEffect, useMemo, useRef } from 'react'
import React, {
createContext,
useEffect,
useMemo,
useRef,
useContext,
} from 'react'
import {
Actor,
AnyStateMachine,
@ -28,7 +34,7 @@ import {
editorManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { machineManager } from 'lib/machineManager'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { useHotkeys } from 'react-hotkeys-hook'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
@ -140,6 +146,8 @@ export const ModelingMachineProvider = ({
// >
// )
const machineManager = useContext(MachineManagerContext)
const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine.provide({
actions: {
@ -408,7 +416,7 @@ export const ModelingMachineProvider = ({
return {}
}
),
Make: ({ event }) => {
Make: ({ context, event }) => {
if (event.type !== 'Make') return
// Check if we already have an export intent.
if (engineCommandManager.exportInfo) {
@ -422,7 +430,21 @@ export const ModelingMachineProvider = ({
}
// Set the current machine.
machineManager.currentMachine = event.data.machine
// Due to our use of singeton pattern, we need to do this to reliably
// update this object across React and non-React boundary.
// We need to do this eagerly because of the exportToEngine call below.
if (engineCommandManager.machineManager === null) {
console.warn(
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
)
return
} else {
engineCommandManager.machineManager.currentMachine =
event.data.machine
}
// Update the rest of the UI that needs to know the current machine
context.machineManager.setCurrentMachine(event.data.machine)
const format: Models['OutputFormat_type'] = {
type: 'stl',
@ -644,6 +666,7 @@ export const ModelingMachineProvider = ({
input.plane
)
await kclManager.updateAst(modifiedAst, false)
sceneInfra.camControls.enableRotate = false
sceneInfra.camControls.syncDirection = 'clientToEngine'
await letEngineAnimateAndSyncCamAfter(
@ -994,6 +1017,7 @@ export const ModelingMachineProvider = ({
...modelingMachineDefaultContext.store,
...persistedContext,
},
machineManager,
},
// devTools: true,
}

View File

@ -95,7 +95,7 @@ export const processMemory = (programMemory: ProgramMemory) => {
return rest
})
} else if (!err(sg)) {
processedMemory[key] = sg.value.map(({ __geoMeta, ...rest }: Path) => {
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if ((val.type as any) === 'Function') {

View File

@ -1,6 +1,12 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import { MouseEventHandler, useCallback, useEffect, useMemo } from 'react'
import {
MouseEventHandler,
useCallback,
useEffect,
useMemo,
useContext,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip'
@ -13,7 +19,7 @@ import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { useKclContext } from 'lang/KclProvider'
import { machineManager } from 'lib/machineManager'
import { MachineManagerContext } from 'components/MachineManagerProvider'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -29,6 +35,7 @@ function getPlatformString(): 'web' | 'desktop' {
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext()
const { settings } = useSettingsAuthContext()

View File

@ -1,7 +1,9 @@
import { Popover } from '@headlessui/react'
import { useContext } from 'react'
import Tooltip from './Tooltip'
import { machineManager } from 'lib/machineManager'
import { isDesktop } from 'lib/isDesktop'
import { components } from 'lib/machine-api'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import { CustomIcon } from './CustomIcon'
export const NetworkMachineIndicator = ({
@ -9,9 +11,12 @@ export const NetworkMachineIndicator = ({
}: {
className?: string
}) => {
const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason()
const machines = machineManager.machines
const {
noMachinesReason,
machines,
machines: { length: machineCount },
} = useContext(MachineManagerContext)
const reason = noMachinesReason()
return isDesktop() ? (
<Popover className="relative">
@ -47,34 +52,36 @@ export const NetworkMachineIndicator = ({
</div>
{machineCount > 0 && (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
{machines.map((machine) => {
return (
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.id.toUpperCase()}</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{machine.make_model.model}
</p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter}
</p>
)}
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{`Status: ${machine.state.state
.charAt(0)
.toUpperCase()}${machine.state.state.slice(1)}`}
{machine.state.state === 'failed' && machine.state.message
? ` (${machine.state.message})`
: ''}
{machine.state.state === 'running' && machine.progress
? ` (${Math.round(machine.progress)}%)`
: ''}
</p>
</li>
)
})}
{machines.map(
(machine: components['schemas']['MachineInfoResponse']) => {
return (
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
<p className="">{machine.id.toUpperCase()}</p>
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{machine.make_model.model}
</p>
{machine.extra &&
machine.extra.type === 'bambu' &&
machine.extra.nozzle_diameter && (
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
Nozzle Diameter: {machine.extra.nozzle_diameter}
</p>
)}
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
{`Status: ${machine.state.state
.charAt(0)
.toUpperCase()}${machine.state.state.slice(1)}`}
{machine.state.state === 'failed' && machine.state.message
? ` (${machine.state.message})`
: ''}
{machine.state.state === 'running' && machine.progress
? ` (${Math.round(machine.progress)}%)`
: ''}
</p>
</li>
)
}
)}
</ul>
)}
</Popover.Panel>

View File

@ -4,14 +4,14 @@ import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { isDesktop } from '../lib/isDesktop'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo } from 'react'
import { Fragment, useMemo, useContext } from 'react'
import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons'
import { machineManager } from 'lib/machineManager'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
@ -96,6 +96,8 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const machineManager = useContext(MachineManagerContext)
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
@ -106,7 +108,7 @@ function ProjectMenuPopover({
(c) => c.name === obj.name && c.groupId === obj.groupId
)
)
const machineCount = machineManager.machineCount()
const machineCount = machineManager.machines.length
// We filter this memoized list so that no orphan "break" elements are rendered.
const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>(

View File

@ -255,10 +255,14 @@ export const Stream = () => {
}, [mediaStream])
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
// If we've got no stream or connection, don't do anything
if (!isNetworkOkay) return
if (!videoRef.current) return
// If we're in sketch mode, don't send a engine-side select event
if (state.matches('Sketch')) return
if (state.matches({ idle: 'showPlanes' })) return
// If we're mousing up from a camera drag, don't send a select event
if (sceneInfra.camControls.wasDragging === true) return
if (btnName(e.nativeEvent).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -32,7 +32,7 @@ const mySketch001 = startSketchOn('XY')
sourceRange: [46, 71],
},
},
value: [
paths: [
{
type: 'ToPoint',
tag: null,
@ -96,7 +96,7 @@ const mySketch001 = startSketchOn('XY')
on: expect.any(Object),
start: expect.any(Object),
type: 'Sketch',
value: [
paths: [
{
type: 'ToPoint',
from: [0, 0],
@ -202,7 +202,7 @@ const sk2 = startSketchOn('XY')
info: expect.any(Object),
},
},
value: [
paths: [
{
type: 'ToPoint',
from: [0, 0],
@ -294,7 +294,7 @@ const sk2 = startSketchOn('XY')
info: expect.any(Object),
},
},
value: [
paths: [
{
type: 'ToPoint',
from: [0, 0],

View File

@ -20,6 +20,8 @@ export default class CodeManager {
private _hotkeys: { [key: string]: () => void } = {}
private timeoutWriter: ReturnType<typeof setTimeout> | undefined = undefined
public writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
constructor() {
if (isDesktop()) {
this.code = ''
@ -120,6 +122,7 @@ export default class CodeManager {
// and file-system watchers which read, will receive empty data during
// writes.
clearTimeout(this.timeoutWriter)
this.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
this.timeoutWriter = setTimeout(() => {
// Wait one event loop to give a chance for params to be set
// Save the file to disk

View File

@ -58,7 +58,7 @@ const newVar = myVar + 1`
`
const mem = await exe(code)
// geo is three js buffer geometry and is very bloated to have in tests
const minusGeo = mem.get('mySketch')?.value?.value
const minusGeo = mem.get('mySketch')?.value?.paths
expect(minusGeo).toEqual([
{
type: 'ToPoint',
@ -175,7 +175,7 @@ const newVar = myVar + 1`
info: expect.any(Object),
},
},
value: [
paths: [
{
type: 'ToPoint',
to: [1, 1],
@ -367,7 +367,7 @@ describe('testing math operators', () => {
const mem = await exe(code)
const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
// result of `-legLen(5, min(3, 999))` should be -4
const yVal = (sketch as Sketch).value?.[0]?.to?.[1]
const yVal = (sketch as Sketch).paths?.[0]?.to?.[1]
expect(yVal).toBe(-4)
})
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
@ -385,8 +385,8 @@ describe('testing math operators', () => {
const mem = await exe(code)
const sketch = sketchFromKclValue(mem.get('part001'), 'part001')
// expect -legLen(segLen('seg01'), myVar) to equal -4 setting the y value back to 0
expect((sketch as Sketch).value?.[1]?.from).toEqual([3, 4])
expect((sketch as Sketch).value?.[1]?.to).toEqual([6, 0])
expect((sketch as Sketch).paths?.[1]?.from).toEqual([3, 4])
expect((sketch as Sketch).paths?.[1]?.to).toEqual([6, 0])
const removedUnaryExp = code.replace(
`-legLen(segLen(seg01), myVar)`,
`legLen(segLen(seg01), myVar)`
@ -398,7 +398,7 @@ describe('testing math operators', () => {
)
// without the minus sign, the y value should be 8
expect((removedUnaryExpMemSketch as Sketch).value?.[1]?.to).toEqual([6, 8])
expect((removedUnaryExpMemSketch as Sketch).paths?.[1]?.to).toEqual([6, 8])
})
it('with nested callExpression and binaryExpression', async () => {
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'

View File

@ -717,7 +717,7 @@ export function isLinesParallelAndConstrained(
constraintType === 'angle' || constraintLevel === 'full'
// get the previous segment
const prevSegment = sg.value[secondaryIndex - 1]
const prevSegment = sg.paths[secondaryIndex - 1]
const prevSourceRange = prevSegment.__geoMeta.sourceRange
const isParallelAndConstrained =

View File

@ -28,6 +28,7 @@ import {
} from 'lib/constants'
import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
import { MachineManager } from 'components/MachineManagerProvider'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000
@ -1415,6 +1416,9 @@ export class EngineCommandManager extends EventTarget {
(() => {}) as any
kclManager: null | KclManager = null
// The current "manufacturing machine" aka 3D printer, CNC, etc.
public machineManager: MachineManager | null = null
set exportInfo(info: ExportInfo | null) {
this._exportInfo = info
}
@ -1630,10 +1634,16 @@ export class EngineCommandManager extends EventTarget {
break
}
case ExportIntent.Make: {
if (!this.machineManager) {
console.warn('Some how, no manufacturing machine is selected.')
break
}
exportMake(
event.data,
this.exportInfo.name,
this.pendingExport.toastId
this.pendingExport.toastId,
this.machineManager
).then((result) => {
if (result) {
this.pendingExport?.resolve(null)

View File

@ -64,7 +64,7 @@ const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"')
export type Coords2d = [number, number]
export function getCoordsFromPaths(skGroup: Sketch, index = 0): Coords2d {
const currentPath = skGroup?.value?.[index]
const currentPath = skGroup?.paths?.[index]
if (!currentPath && skGroup?.start) {
return skGroup.start.to
} else if (!currentPath) {
@ -1704,7 +1704,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
varName
)
if (err(sketch)) return sketch
const intersectPath = sketch.value.find(
const intersectPath = sketch.paths.find(
({ tag }: Path) => tag && tag.value === intersectTagName
)
let offset = 0

View File

@ -18,7 +18,7 @@ export function getSketchSegmentFromPathToNode(
pathToNode: PathToNode
):
| {
segment: Sketch['value'][number]
segment: Sketch['paths'][number]
index: number
}
| Error {
@ -39,15 +39,15 @@ export function getSketchSegmentFromSourceRange(
[rangeStart, rangeEnd]: SourceRange
):
| {
segment: Sketch['value'][number]
segment: Sketch['paths'][number]
index: number
}
| Error {
const lineIndex = sketch.value.findIndex(
const lineIndex = sketch.paths.findIndex(
({ __geoMeta: { sourceRange } }: Path) =>
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
)
const line = sketch.value[lineIndex]
const line = sketch.paths[lineIndex]
if (line) {
return {
segment: line,

View File

@ -1732,7 +1732,7 @@ export function transformAstSketchLines({
if (err(_segment)) return _segment
referencedSegment = _segment.segment
} else {
referencedSegment = sketch.value.find(
referencedSegment = sketch.paths.find(
(path) => path.tag?.value === _referencedSegmentName
)
}

View File

@ -3,7 +3,6 @@ import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections'
import { machineManager } from 'lib/machineManager'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
type OutputFormat = Models['OutputFormat_type']
@ -187,41 +186,41 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
machine.make_model.model ||
machine.make_model.manufacturer ||
'Unknown Machine',
options: () => {
return Object.entries(machineManager.machines).map(
([_, machine]) => ({
name:
`${machine.id} (${
machine.make_model.model || machine.make_model.manufacturer
}) (${machine.state.state})` +
(machine.hardware_configuration &&
machine.hardware_configuration.type !== 'none' &&
machine.hardware_configuration.config.nozzle_diameter
? ` - Nozzle Diameter: ${machine.hardware_configuration.config.nozzle_diameter}`
: '') +
(machine.hardware_configuration &&
machine.hardware_configuration.type !== 'none' &&
machine.hardware_configuration.config.filaments &&
machine.hardware_configuration.config.filaments[0]
? ` - ${
machine.hardware_configuration.config.filaments[0].name
} #${
machine.hardware_configuration.config &&
machine.hardware_configuration.config.filaments[0].color?.slice(
0,
6
)
}`
: ''),
isCurrent: false,
disabled: machine.state.state !== 'idle',
value: machine as components['schemas']['MachineInfoResponse'],
})
)
},
defaultValue: () => {
options: (commandBarContext) => {
return Object.values(
machineManager.machines
commandBarContext.machineManager?.machines || []
).map((machine: components['schemas']['MachineInfoResponse']) => ({
name:
`${machine.id} (${
machine.make_model.model || machine.make_model.manufacturer
}) (${machine.state.state})` +
(machine.hardware_configuration &&
machine.hardware_configuration.type !== 'none' &&
machine.hardware_configuration.config.nozzle_diameter
? ` - Nozzle Diameter: ${machine.hardware_configuration.config.nozzle_diameter}`
: '') +
(machine.hardware_configuration &&
machine.hardware_configuration.type !== 'none' &&
machine.hardware_configuration.config.filaments &&
machine.hardware_configuration.config.filaments[0]
? ` - ${
machine.hardware_configuration.config.filaments[0].name
} #${
machine.hardware_configuration.config &&
machine.hardware_configuration.config.filaments[0].color?.slice(
0,
6
)
}`
: ''),
isCurrent: false,
disabled: machine.state.state !== 'idle',
value: machine,
}))
},
defaultValue: (commandBarContext) => {
return Object.values(
commandBarContext.machineManager.machines || []
)[0] as components['schemas']['MachineInfoResponse']
},
},

View File

@ -5,6 +5,7 @@ import { Selection } from './selections'
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
import { commandBarMachine } from 'machines/commandBarMachine'
import { ReactNode } from 'react'
import { MachineManager } from 'components/MachineManagerProvider'
type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const
@ -127,6 +128,7 @@ export type CommandArgumentConfig<
| ((
commandBarContext: {
argumentsToSubmit: Record<string, unknown>
machineManager?: MachineManager
}, // Should be the commandbarMachine's context, but it creates a circular dependency
machineContext?: C
) => CommandArgumentOption<OutputType>[])

View File

@ -1,5 +1,5 @@
import { deserialize_files } from 'wasm-lib/pkg/wasm_lib'
import { machineManager } from './machineManager'
import { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast'
import { components } from './machine-api'
import ModelingAppFile from './modelingAppFile'
@ -9,7 +9,8 @@ import { MAKE_TOAST_MESSAGES } from './constants'
export async function exportMake(
data: ArrayBuffer,
name: string,
toastId: string
toastId: string,
machineManager: MachineManager
): Promise<Response | null> {
if (name === '') {
console.error(MAKE_TOAST_MESSAGES.NO_NAME)
@ -17,7 +18,7 @@ export async function exportMake(
return null
}
if (machineManager.machineCount() === 0) {
if (machineManager.machines.length === 0) {
console.error(MAKE_TOAST_MESSAGES.NO_MACHINES)
toast.error(MAKE_TOAST_MESSAGES.NO_MACHINES, { id: toastId })
return null

View File

@ -138,6 +138,11 @@ export interface components {
FdmHardwareConfiguration: {
/** @description The filaments the printer has access to. */
filaments: components['schemas']['Filament'][]
/**
* Format: uint
* @description The currently loaded filament index.
*/
loaded_filament_idx?: number | null
/**
* Format: double
* @description Diameter of the extrusion nozzle, in mm.
@ -191,6 +196,10 @@ export interface components {
/** @enum {string} */
type: 'composite'
}
| {
/** @enum {string} */
type: 'unknown'
}
/** @description The hardware configuration of a machine. */
HardwareConfiguration:
| {

View File

@ -1,105 +0,0 @@
import { isDesktop } from './isDesktop'
import { components } from './machine-api'
import { reportRejection } from './trap'
import { toSync } from './utils'
export type MachinesListing = Array<
components['schemas']['MachineInfoResponse']
>
export class MachineManager {
private _isDesktop: boolean = isDesktop()
private _machines: MachinesListing = []
private _machineApiIp: string | null = null
private _currentMachine: components['schemas']['MachineInfoResponse'] | null =
null
constructor() {
if (!this._isDesktop) {
return
}
this.updateMachines().catch(reportRejection)
}
start() {
if (!this._isDesktop) {
return
}
// Start a background job to update the machines every ten seconds.
// If MDNS is already watching, this timeout will wait until it's done to trigger the
// finding again.
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
const timeoutLoop = () => {
clearTimeout(timeoutId)
timeoutId = setTimeout(
toSync(async () => {
await this.updateMachineApiIp()
await this.updateMachines()
timeoutLoop()
}, reportRejection),
10000
)
}
timeoutLoop()
}
get machines(): MachinesListing {
return this._machines
}
machineCount(): number {
return this._machines.length
}
get machineApiIp(): string | null {
return this._machineApiIp
}
// Get the reason message for why there are no machines.
noMachinesReason(): string | undefined {
if (this.machineCount() > 0) {
return undefined
}
if (this.machineApiIp === null) {
return 'Machine API server was not discovered'
}
return 'Machine API server was discovered, but no machines are available'
}
get currentMachine(): components['schemas']['MachineInfoResponse'] | null {
return this._currentMachine
}
set currentMachine(
machine: components['schemas']['MachineInfoResponse'] | null
) {
this._currentMachine = machine
}
private async updateMachines(): Promise<void> {
if (!this._isDesktop) {
return
}
if (this._machineApiIp === null) {
return
}
this._machines = await window.electron.listMachines(this._machineApiIp)
}
private async updateMachineApiIp(): Promise<void> {
if (!this._isDesktop) {
return
}
this._machineApiIp = await window.electron.getMachineApiIp()
}
}
export const machineManager = new MachineManager()
machineManager.start()

View File

@ -7,6 +7,7 @@ import {
} from 'lib/commandTypes'
import { Selections } from 'lib/selections'
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
import { MachineManager } from 'components/MachineManagerProvider'
export type CommandBarContext = {
commands: Command[]
@ -14,6 +15,7 @@ export type CommandBarContext = {
currentArgument?: CommandArgument<unknown> & { name: string }
selectionRanges: Selections
argumentsToSubmit: { [x: string]: unknown }
machineManager: MachineManager
}
export type CommandBarMachineEvent =
@ -71,6 +73,7 @@ export type CommandBarMachineEvent =
type: 'Change current argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
}
| { type: 'Set machine manager'; data: MachineManager }
export const commandBarMachine = setup({
types: {
@ -90,6 +93,12 @@ export const commandBarMachine = setup({
}
},
}),
'Set machine manager': assign({
machineManager: ({ event, context }) => {
if (event.type !== 'Set machine manager') return context.machineManager
return event.data
},
}),
'Execute command': ({ context, event }) => {
const { selectedCommand } = context
if (!selectedCommand) return
@ -339,6 +348,13 @@ export const commandBarMachine = setup({
codeBasedSelections: [],
},
argumentsToSubmit: {},
machineManager: {
machines: [],
machineApiIp: null,
currentMachine: null,
setCurrentMachine: () => {},
noMachinesReason: () => undefined,
},
},
id: 'Command Bar',
initial: 'Closed',
@ -520,6 +536,11 @@ export const commandBarMachine = setup({
},
},
on: {
'Set machine manager': {
reenter: false,
actions: 'Set machine manager',
},
Close: {
target: '.Closed',
},

View File

@ -64,6 +64,7 @@ import toast from 'react-hot-toast'
import { ToolbarModeName } from 'lib/toolbar'
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
import { Vector3 } from 'three'
import { MachineManager } from 'components/MachineManagerProvider'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -301,6 +302,7 @@ export const getPersistedContext = (): Partial<PersistedModelingContext> => {
export interface ModelingMachineContext {
currentMode: ToolbarModeName
currentTool: SketchTool
machineManager: MachineManager
selection: string[]
selectionRanges: Selections
sketchDetails: SketchDetails | null
@ -315,6 +317,13 @@ export interface ModelingMachineContext {
export const modelingMachineDefaultContext: ModelingMachineContext = {
currentMode: 'modeling',
currentTool: 'none',
machineManager: {
machines: [],
machineApiIp: null,
currentMachine: null,
setCurrentMachine: () => {},
noMachinesReason: () => undefined,
},
selection: [],
selectionRanges: {
otherSelections: [],

View File

@ -4,7 +4,7 @@ import fs from 'node:fs/promises'
import os from 'node:os'
import fsSync from 'node:fs'
import packageJson from '../package.json'
import { MachinesListing } from 'lib/machineManager'
import { MachinesListing } from 'components/MachineManagerProvider'
import chokidar from 'chokidar'
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)

View File

@ -23,6 +23,9 @@ import { codeManager, editorManager, kclManager } from 'lib/singletons'
import { bracket } from 'lib/exampleKcl'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
export const kbdClasses =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
@ -80,8 +83,20 @@ export const onboardingRoutes = [
]
export function useDemoCode() {
const { overallState, immediateState } = useNetworkContext()
useEffect(() => {
if (!editorManager.editorView || codeManager.code === bracket) return
// Don't run if the editor isn't loaded or the code is already the bracket
if (!editorManager.editorView || codeManager.code === bracket) {
return
}
// Don't run if the network isn't healthy or the connection isn't established
if (
overallState !== NetworkHealthState.Ok ||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
) {
return
}
setTimeout(
toSync(async () => {
codeManager.updateCodeStateEditor(bracket)
@ -89,7 +104,7 @@ export function useDemoCode() {
await codeManager.writeToFile()
}, reportRejection)
)
}, [editorManager.editorView])
}, [editorManager.editorView, immediateState, overallState])
}
export function useNextClick(newStatus: string) {

128
src/wasm-lib/Cargo.lock generated
View File

@ -121,9 +121,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
dependencies = [
"backtrace",
]
@ -176,7 +176,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -187,7 +187,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -204,7 +204,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -465,7 +465,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -656,7 +656,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -667,7 +667,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -722,7 +722,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"synstructure",
]
@ -751,7 +751,7 @@ dependencies = [
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -762,7 +762,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -789,7 +789,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -827,7 +827,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -988,7 +988,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1084,7 +1084,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1542,7 +1542,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.22"
version = "0.2.23"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1612,12 +1612,12 @@ dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
name = "kcl-test-server"
version = "0.1.14"
version = "0.1.15"
dependencies = [
"anyhow",
"hyper 0.14.30",
@ -1644,9 +1644,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.3.23"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71b6f0c34165939697548dd0c94221200dbb8b5d1c84b5d8e803e70f9f720ea7"
checksum = "f6359cc0a1bbccbcf78775eea17a033cf2aa89d3fe6a9784f8ce94e5f882c185"
dependencies = [
"anyhow",
"async-trait",
@ -1684,9 +1684,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.68"
version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3aedfcc1d8ea9995ec3eb78a6743c585c9380475c48701797f107489b696aa"
checksum = "c6d2160dcb0e5373b1242a760dbf17fb9c12de523c410c11b145381c852b377b"
dependencies = [
"anyhow",
"chrono",
@ -1716,7 +1716,7 @@ dependencies = [
"kittycad-modeling-cmds-macros-impl",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -1727,7 +1727,7 @@ checksum = "6607507a8a0e4273b943179f0a3ef8e90712308d1d3095246040c29cfdbf985b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2112,7 +2112,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.5",
"structmeta",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2126,7 +2126,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.5",
"structmeta",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2166,7 +2166,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2224,7 +2224,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2337,9 +2337,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.88"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
@ -2391,7 +2391,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2404,7 +2404,7 @@ dependencies = [
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2586,9 +2586,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@ -2925,7 +2925,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -2965,9 +2965,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
dependencies = [
"serde_derive",
]
@ -2983,13 +2983,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3000,14 +3000,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"indexmap 2.6.0",
"itoa",
@ -3024,7 +3024,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3045,7 +3045,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3182,7 +3182,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3193,7 +3193,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3215,7 +3215,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3237,9 +3237,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.79"
version = "2.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
dependencies = [
"proc-macro2",
"quote",
@ -3263,7 +3263,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3326,22 +3326,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3437,7 +3437,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3579,7 +3579,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3607,7 +3607,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3689,7 +3689,7 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"termcolor",
]
@ -3865,7 +3865,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@ -3927,7 +3927,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"wasm-bindgen-shared",
]
@ -3962,7 +3962,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -4328,7 +4328,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]

View File

@ -71,8 +71,8 @@ members = [
[workspace.dependencies]
http = "1"
kittycad = { version = "0.3.23", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.68", features = ["websocket"] }
kittycad = { version = "0.3.25", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.71", features = ["websocket"] }
[[test]]
name = "executor"
@ -83,6 +83,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"]
#[patch.crates-io]
#kittycad-modeling-cmds = { path = "../../../modeling-api/modeling-cmds" }
#kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }

View File

@ -17,13 +17,13 @@ convert_case = "0.6.0"
once_cell = "1.20.2"
proc-macro2 = "1"
quote = "1"
regex = "1.10"
serde = { version = "1.0.210", features = ["derive"] }
regex = "1.11"
serde = { version = "1.0.213", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.79", features = ["full"] }
syn = { version = "2.0.85", features = ["full"] }
[dev-dependencies]
anyhow = "1.0.89"
anyhow = "1.0.91"
expectorate = "1.1.0"
pretty_assertions = "1.4.1"
rustfmt-wrapper = "0.2.1"

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.79", features = ["full"] }
syn = { version = "2.0.85", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@ -1,15 +1,15 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.14"
version = "0.1.15"
edition = "2021"
license = "MIT"
[dependencies]
anyhow = "1.0.89"
anyhow = "1.0.91"
hyper = { version = "0.14.29", features = ["http1", "server", "tcp"] }
kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.210", features = ["derive"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.128"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }

View File

@ -31,7 +31,7 @@ pub struct ServerArgs {
/// Where to find the engine.
/// If none, uses the prod engine.
/// This is useful for testing a local engine instance.
/// Overridden by the $LOCAL_ENGINE_ADDR environment variable.
/// Overridden by the $ZOO_HOST environment variable.
pub engine_address: Option<String>,
}
@ -44,8 +44,8 @@ impl ServerArgs {
num_engine_conns: pargs.opt_value_from_str("--num-engine-conns")?.unwrap_or(1),
engine_address: pargs.opt_value_from_str("--engine-address")?,
};
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
println!("Overriding engine address via $LOCAL_ENGINE_ADDR");
if let Ok(addr) = std::env::var("ZOO_HOST") {
println!("Overriding engine address via $ZOO_HOST");
args.engine_address = Some(addr);
}
println!("Config is {args:?}");

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.22"
version = "0.2.23"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -11,7 +11,7 @@ keywords = ["kcl", "KittyCAD", "CAD"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { version = "1.0.89", features = ["backtrace"] }
anyhow = { version = "1.0.91", features = ["backtrace"] }
async-recursion = "1.1.1"
async-trait = "0.1.83"
base64 = "0.22.1"
@ -38,11 +38,11 @@ pyo3 = { version = "0.22.5", optional = true }
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1", "preserve_order"] }
serde = { version = "1.0.210", features = ["derive"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.128"
sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true }
thiserror = "1.0.64"
thiserror = "1.0.65"
toml = "0.8.19"
ts-rs = { version = "10.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }

View File

@ -1127,7 +1127,7 @@ pub struct TagEngineInfo {
/// The sketch the tag is on.
pub sketch: uuid::Uuid,
/// The path the tag is on.
pub path: Option<BasePath>,
pub path: Option<Path>,
/// The surface information for the tag.
pub surface: Option<ExtrudeSurface>,
}
@ -1137,10 +1137,10 @@ pub struct TagEngineInfo {
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct Sketch {
/// The id of the sketch (this will change when the engine's reference to it changes.
/// The id of the sketch (this will change when the engine's reference to it changes).
pub id: uuid::Uuid,
/// The paths in the sketch.
pub value: Vec<Path>,
pub paths: Vec<Path>,
/// What the sketch is on (can be a plane or a face).
pub on: SketchSurface,
/// The starting path.
@ -1206,7 +1206,7 @@ impl Sketch {
tag_identifier.info = Some(TagEngineInfo {
id: base.geo_meta.id,
sketch: self.id,
path: Some(base.clone()),
path: Some(current_path.clone()),
surface: None,
});
@ -1215,7 +1215,7 @@ impl Sketch {
/// Get the path most recently sketched.
pub(crate) fn latest_path(&self) -> Option<&Path> {
self.value.last()
self.paths.last()
}
/// The "pen" is an imaginary pen drawing the path.
@ -1656,6 +1656,43 @@ pub enum Path {
#[serde(flatten)]
base: BasePath,
},
/// A circular arc, not necessarily tangential to the current point.
Arc {
#[serde(flatten)]
base: BasePath,
/// Center of the circle that this arc is drawn on.
center: [f64; 2],
/// Radius of the circle that this arc is drawn on.
radius: f64,
},
}
/// What kind of path is this?
#[derive(Display)]
enum PathType {
ToPoint,
Base,
TangentialArc,
TangentialArcTo,
Circle,
Horizontal,
AngledLineTo,
Arc,
}
impl From<&Path> for PathType {
fn from(value: &Path) -> Self {
match value {
Path::ToPoint { .. } => Self::ToPoint,
Path::TangentialArcTo { .. } => Self::TangentialArcTo,
Path::TangentialArc { .. } => Self::TangentialArc,
Path::Circle { .. } => Self::Circle,
Path::Horizontal { .. } => Self::Horizontal,
Path::AngledLineTo { .. } => Self::AngledLineTo,
Path::Base { .. } => Self::Base,
Path::Arc { .. } => Self::Arc,
}
}
}
impl Path {
@ -1668,6 +1705,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
Path::TangentialArc { base, .. } => base.geo_meta.id,
Path::Circle { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id,
}
}
@ -1680,6 +1718,7 @@ impl Path {
Path::TangentialArcTo { base, .. } => base.tag.clone(),
Path::TangentialArc { base, .. } => base.tag.clone(),
Path::Circle { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(),
}
}
@ -1692,6 +1731,47 @@ impl Path {
Path::TangentialArcTo { base, .. } => base,
Path::TangentialArc { base, .. } => base,
Path::Circle { base, .. } => base,
Path::Arc { base, .. } => base,
}
}
/// Where does this path segment start?
pub fn get_from(&self) -> &[f64; 2] {
&self.get_base().from
}
/// Where does this path segment end?
pub fn get_to(&self) -> &[f64; 2] {
&self.get_base().to
}
/// Length of this path segment, in cartesian plane.
pub fn length(&self) -> f64 {
match self {
Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
linear_distance(self.get_from(), self.get_to())
}
Self::TangentialArc {
base: _,
center,
ccw: _,
}
| Self::TangentialArcTo {
base: _,
center,
ccw: _,
} => {
// The radius can be calculated as the linear distance between `to` and `center`,
// or between `from` and `center`. They should be the same.
let radius = linear_distance(self.get_from(), center);
debug_assert_eq!(radius, linear_distance(self.get_to(), center));
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
}
Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
Self::Arc { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
}
}
}
@ -1704,10 +1784,22 @@ impl Path {
Path::TangentialArcTo { base, .. } => Some(base),
Path::TangentialArc { base, .. } => Some(base),
Path::Circle { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base),
}
}
}
/// Compute the straight-line distance between a pair of (2D) points.
#[rustfmt::skip]
fn linear_distance(
[x0, y0]: &[f64; 2],
[x1, y1]: &[f64; 2]
) -> f64 {
let y_sq = (y1 - y0).powi(2);
let x_sq = (x1 - x0).powi(2);
(y_sq + x_sq).sqrt()
}
/// An extrude surface.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -1888,9 +1980,73 @@ impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
}
}
/// Create a new zoo api client.
#[cfg(not(target_arch = "wasm32"))]
pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> Result<kittycad::Client> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.connection_verbose(true)
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let zoo_token_env = std::env::var("ZOO_API_TOKEN");
let token = if let Some(token) = token {
token
} else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
if let Ok(zoo_token) = zoo_token_env {
if zoo_token != token {
return Err(anyhow::anyhow!(
"Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
token,
zoo_token
));
}
}
token
} else if let Ok(token) = zoo_token_env {
token
} else {
return Err(anyhow::anyhow!(
"No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN"
));
};
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// Set an engine address if it's set.
let kittycad_host_env = std::env::var("KITTYCAD_HOST");
if let Some(addr) = engine_addr {
client.set_base_url(addr);
} else if let Ok(addr) = std::env::var("ZOO_HOST") {
if let Ok(kittycad_host) = kittycad_host_env {
if kittycad_host != addr {
return Err(anyhow::anyhow!(
"Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
kittycad_host,
addr
));
}
}
client.set_base_url(addr);
} else if let Ok(addr) = kittycad_host_env {
client.set_base_url(addr);
}
Ok(client)
}
impl ExecutorContext {
/// Create a new default executor context.
/// Also returns the response HTTP headers from the server.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
let (ws, _headers) = client
@ -1935,6 +2091,35 @@ impl ExecutorContext {
})
}
/// Create a new default executor context.
/// With a kittycad client.
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
/// variables.
/// But also allows for passing in a token and engine address directly.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_with_client(
settings: ExecutorSettings,
token: Option<String>,
engine_addr: Option<String>,
) -> Result<Self> {
// Create the client.
let client = new_zoo_client(token, engine_addr)?;
let ctx = Self::new(&client, settings).await?;
Ok(ctx)
}
/// Create a new default executor context.
/// With the default kittycad client.
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
/// variables.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_with_default_client(settings: ExecutorSettings) -> Result<Self> {
// Create the client.
let ctx = Self::new_with_client(settings, None, None).await?;
Ok(ctx)
}
pub fn is_mock(&self) -> bool {
self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
}
@ -1942,35 +2127,7 @@ impl ExecutorContext {
/// For executing unit tests.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_for_unit_test(units: UnitLength, engine_addr: Option<String>) -> Result<Self> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"));
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.connection_verbose(true)
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// Set a local engine address if it's set.
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
client.set_base_url(addr);
}
if let Some(addr) = engine_addr {
client.set_base_url(addr);
}
let ctx = ExecutorContext::new(
&client,
let ctx = ExecutorContext::new_with_client(
ExecutorSettings {
units,
highlight_edges: true,
@ -1978,6 +2135,8 @@ impl ExecutorContext {
show_grid: false,
replay: None,
},
None,
engine_addr,
)
.await?;
Ok(ctx)

View File

@ -3,41 +3,13 @@ use std::sync::{Arc, RwLock};
use anyhow::Result;
use tower_lsp::LanguageServer;
fn new_zoo_client() -> kittycad::Client {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.connection_verbose(true)
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// Set a local engine address if it's set.
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
client.set_base_url(addr);
}
client
}
// Create a fake kcl lsp server for testing.
pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
let stdlib = crate::std::StdLib::new();
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib)?;
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib)?;
let zoo_client = new_zoo_client();
let zoo_client = crate::executor::new_zoo_client(None, None)?;
let executor_ctx = if execute {
Some(crate::executor::ExecutorContext::new(&zoo_client, Default::default()).await?)

View File

@ -141,15 +141,15 @@ pub(crate) async fn do_post_extrude(
)
.await?;
if sketch.value.is_empty() {
if sketch.paths.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "Expected a non-empty sketch".to_string(),
source_ranges: vec![args.source_range],
}));
}
let edge_id = sketch.value.iter().find_map(|segment| match segment {
Path::ToPoint { base } | Path::Circle { base, .. } => Some(base.geo_meta.id),
let edge_id = sketch.paths.iter().find_map(|segment| match segment {
Path::ToPoint { base } | Path::Circle { base, .. } | Path::Arc { base, .. } => Some(base.geo_meta.id),
_ => None,
});
@ -229,12 +229,15 @@ pub(crate) async fn do_post_extrude(
} = analyze_faces(exec_state, &args, face_infos);
// Iterate over the sketch.value array and add face_id to GeoMeta
let new_value = sketch
.value
.paths
.iter()
.flat_map(|path| {
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
match path {
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } | Path::Circle { .. } => {
Path::Arc { .. }
| Path::TangentialArc { .. }
| Path::TangentialArcTo { .. }
| Path::Circle { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),

View File

@ -42,7 +42,7 @@ fn inner_segment_end_x(tag: &TagIdentifier, exec_state: &mut ExecState, args: Ar
})
})?;
Ok(path.to[0])
Ok(path.get_base().to[0])
}
/// Returns the segment end of y.
@ -79,7 +79,7 @@ fn inner_segment_end_y(tag: &TagIdentifier, exec_state: &mut ExecState, args: Ar
})
})?;
Ok(path.to[1])
Ok(path.get_to()[1])
}
/// Returns the last segment of x.
@ -109,7 +109,7 @@ pub async fn last_segment_x(_exec_state: &mut ExecState, args: Args) -> Result<K
}]
fn inner_last_segment_x(sketch: Sketch, args: Args) -> Result<f64, KclError> {
let last_line = sketch
.value
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
@ -149,7 +149,7 @@ pub async fn last_segment_y(_exec_state: &mut ExecState, args: Args) -> Result<K
}]
fn inner_last_segment_y(sketch: Sketch, args: Args) -> Result<f64, KclError> {
let last_line = sketch
.value
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
@ -202,7 +202,7 @@ fn inner_segment_length(tag: &TagIdentifier, exec_state: &mut ExecState, args: A
})
})?;
let result = ((path.from[1] - path.to[1]).powi(2) + (path.from[0] - path.to[0]).powi(2)).sqrt();
let result = path.length();
Ok(result)
}
@ -242,7 +242,7 @@ fn inner_segment_angle(tag: &TagIdentifier, exec_state: &mut ExecState, args: Ar
})
})?;
let result = between(path.from.into(), path.to.into());
let result = between(path.get_from().into(), path.get_to().into());
Ok(result.to_degrees())
}
@ -286,10 +286,10 @@ fn inner_angle_to_match_length_x(
})
})?;
let length = ((path.from[1] - path.to[1]).powi(2) + (path.from[0] - path.to[0]).powi(2)).sqrt();
let length = path.length();
let last_line = sketch
.value
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
@ -350,10 +350,10 @@ fn inner_angle_to_match_length_y(
})
})?;
let length = ((path.from[1] - path.to[1]).powi(2) + (path.from[0] - path.to[0]).powi(2)).sqrt();
let length = path.length();
let last_line = sketch
.value
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {

View File

@ -134,7 +134,7 @@ async fn inner_circle(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
.await?;

View File

@ -154,7 +154,7 @@ async fn inner_line_to(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
@ -323,7 +323,7 @@ async fn inner_line(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
@ -506,7 +506,7 @@ async fn inner_angled_line(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
@ -813,7 +813,7 @@ async fn inner_angled_line_that_intersects(
let from = sketch.current_pen_position()?;
let to = intersection_with_parallel_line(
&[path.from.into(), path.to.into()],
&[path.get_from().into(), path.get_to().into()],
data.offset.unwrap_or_default(),
data.angle,
from,
@ -1237,14 +1237,16 @@ pub(crate) async fn inner_start_profile_at(
id: path_id,
original_id: path_id,
on: sketch_surface.clone(),
value: vec![],
paths: vec![],
meta: vec![args.source_range.into()],
tags: if let Some(tag) = &tag {
let mut tag_identifier: TagIdentifier = tag.into();
tag_identifier.info = Some(TagEngineInfo {
id: current_path.geo_meta.id,
sketch: path_id,
path: Some(current_path.clone()),
path: Some(Path::Base {
base: current_path.clone(),
}),
surface: None,
});
HashMap::from([(tag.name.to_string(), tag_identifier)])
@ -1411,7 +1413,7 @@ pub(crate) async fn inner_close(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
@ -1528,7 +1530,7 @@ pub(crate) async fn inner_arc(
)
.await?;
let current_path = Path::ToPoint {
let current_path = Path::Arc {
base: BasePath {
from: from.into(),
to: end.into(),
@ -1538,6 +1540,8 @@ pub(crate) async fn inner_arc(
metadata: args.source_range.into(),
},
},
center: center.into(),
radius,
};
let mut new_sketch = sketch.clone();
@ -1545,7 +1549,7 @@ pub(crate) async fn inner_arc(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
@ -1677,7 +1681,7 @@ async fn inner_tangential_arc(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
@ -1773,7 +1777,7 @@ async fn inner_tangential_arc_to(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
@ -1858,7 +1862,7 @@ async fn inner_tangential_arc_to_relative(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}
@ -1951,7 +1955,7 @@ async fn inner_bezier_curve(
new_sketch.add_tag(tag, &current_path);
}
new_sketch.value.push(current_path);
new_sketch.paths.push(current_path);
Ok(new_sketch)
}

View File

@ -21,7 +21,7 @@ pub fn normalize(angle: Angle) -> Angle {
Angle::from_degrees(if result > 180.0 { result - 360.0 } else { result })
}
/// Gives the ▲-angle between from and to angles (shortest path), use radians.
/// Gives the ▲-angle between from and to angles (shortest path)
///
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
/// # Examples

View File

@ -43,38 +43,7 @@ async fn do_execute_and_snapshot(ctx: &ExecutorContext, code: &str) -> anyhow::R
}
async fn new_context(units: UnitLength, with_auth: bool) -> anyhow::Result<ExecutorContext> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.connection_verbose(true)
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = if with_auth {
std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set")
} else {
"bad_token".to_string()
};
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// Set a local engine address if it's set.
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
if with_auth {
client.set_base_url(addr);
}
}
let ctx = ExecutorContext::new(
&client,
let ctx = ExecutorContext::new_with_client(
ExecutorSettings {
units,
highlight_edges: true,
@ -82,6 +51,8 @@ async fn new_context(units: UnitLength, with_auth: bool) -> anyhow::Result<Execu
show_grid: false,
replay: None,
},
if with_auth { None } else { Some("bad_token".to_string()) },
None,
)
.await?;
Ok(ctx)

View File

@ -0,0 +1,15 @@
exampleSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([0, 2], %)
|> line([3, 1], %)
|> line([0, -4], %)
|> close(%)
|> extrude(1, %)
pattn1 = patternLinear3d({
axis: [1, 0, 0],
instances: 7,
distance: 6
}, exampleSketch)
pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], instances: 41, arcDegrees: 360, rotateDuplicates: false}, pattn1)

View File

@ -0,0 +1,19 @@
exampleSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([0, 2], %)
|> line([3, 1], %)
|> line([0, -4], %)
|> close(%)
|> extrude(1, %)
pattn1 = patternLinear3d({
axis: [1, 0, 0],
instances: 7,
distance: 6
}, exampleSketch)
pattn2 = patternLinear3d({
axis: [0, 0, 1],
distance: 1,
instances: 7
}, pattn1)

View File

@ -1,5 +1,5 @@
fn cube = (pos, scale) => {
const sg = startSketchOn('XY')
fn square = (pos, scale) => {
sg = startSketchOn('XY')
|> startProfileAt(pos, %)
|> line([0, scale], %)
|> line([scale, 0], %)
@ -9,9 +9,9 @@ fn cube = (pos, scale) => {
return sg
}
const b1 = cube([0,0], 10)
const b2 = cube([3,3], 4)
sq = square([0,0], 10)
cb = square([3,3], 4)
|> extrude(10, %)
const pt1 = b1.value[0]
const pt2 = b2.value[0]
pt1 = sq.paths[0]
pt2 = cb.value[0]

View File

@ -0,0 +1,6 @@
part001 = startSketchOn('-XZ')
|> startProfileAt([0, 0], %)
|> lineTo([100, 100], %)
|> lineTo([100, 0], %)
|> close(%)
|> extrude(5 + 7, %)

View File

@ -0,0 +1,55 @@
// Shelf Bracket
// This is a shelf bracket made out of 6061-T6 aluminum sheet metal. The required thickness is calculated based on a point load of 300 lbs applied to the end of the shelf. There are two brackets holding up the shelf, so the moment experienced is divided by 2. The shelf is 1 foot long from the wall.
// Define our bracket feet lengths
shelfMountL = 8 // The length of the bracket holding up the shelf is 6 inches
wallMountL = 6 // the length of the bracket
// Define constants required to calculate the thickness needed to support 300 lbs
sigmaAllow = 35000 // psi
width = 6 // inch
p = 300 // Force on shelf - lbs
L = 12 // inches
M = L * p / 2 // Moment experienced at fixed end of bracket
FOS = 2 // Factor of safety of 2 to be conservative
// Calculate the thickness off the bending stress and factor of safety
thickness = sqrt(6 * M * FOS / (width * sigmaAllow))
// 0.25 inch fillet radius
filletR = 0.25
// Sketch the bracket and extrude with fillets
bracket = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, wallMountL], %, $outerEdge)
|> line([-shelfMountL, 0], %, $seg01)
|> line([0, -thickness], %)
|> line([shelfMountL - thickness, 0], %, $innerEdge)
|> line([0, -wallMountL + thickness], %)
|> close(%)
|> extrude(width, %)
|> fillet({
radius: filletR,
tags: [
getNextAdjacentEdge(innerEdge)
]
}, %)
|> fillet({
radius: filletR + thickness,
tags: [
getNextAdjacentEdge(outerEdge)
]
}, %)
sketch001 = startSketchOn(bracket, seg01)
|> startProfileAt([4.28, 3.83], %)
|> line([2.17, -0.03], %)
|> line([-0.07, -1.8], %)
|> line([-2.07, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> extrude(10, %)

View File

@ -0,0 +1,6 @@
part001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> lineTo([100, 100], %)
|> lineTo([100, 0], %)
|> close(%)
|> extrude(5 + 7, %)

View File

@ -1389,84 +1389,6 @@ extrusion = startSketchOn('XY')
);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_xz_plane() {
let code = r#"part001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> lineTo([100, 100], %)
|> lineTo([100, 0], %)
|> close(%)
|> extrude(5 + 7, %)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
assert_out("xz_plane", &result);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_neg_xz_plane() {
let code = r#"part001 = startSketchOn('-XZ')
|> startProfileAt([0, 0], %)
|> lineTo([100, 100], %)
|> lineTo([100, 0], %)
|> close(%)
|> extrude(5 + 7, %)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
assert_out("neg_xz_plane", &result);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_linear_pattern3d_a_pattern() {
let code = r#"exampleSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([0, 2], %)
|> line([3, 1], %)
|> line([0, -4], %)
|> close(%)
|> extrude(1, %)
pattn1 = patternLinear3d({
axis: [1, 0, 0],
instances: 7,
distance: 6
}, exampleSketch)
pattn2 = patternLinear3d({
axis: [0, 0, 1],
distance: 1,
instances: 7
}, pattn1)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
assert_out("linear_pattern3d_a_pattern", &result);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_circular_pattern3d_a_pattern() {
let code = r#"exampleSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([0, 2], %)
|> line([3, 1], %)
|> line([0, -4], %)
|> close(%)
|> extrude(1, %)
pattn1 = patternLinear3d({
axis: [1, 0, 0],
instances: 7,
distance: 6
}, exampleSketch)
pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], instances: 41, arcDegrees: 360, rotateDuplicates: false}, pattn1)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
assert_out("circular_pattern3d_a_pattern", &result);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_array_of_sketches() {
let code = r#"plane001 = startSketchOn('XZ')
@ -1496,69 +1418,6 @@ extrude(10, sketch001)
assert_out("array_of_sketches", &result);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_sketch_on_face_after_fillets_referencing_face() {
let code = r#"// Shelf Bracket
// This is a shelf bracket made out of 6061-T6 aluminum sheet metal. The required thickness is calculated based on a point load of 300 lbs applied to the end of the shelf. There are two brackets holding up the shelf, so the moment experienced is divided by 2. The shelf is 1 foot long from the wall.
// Define our bracket feet lengths
shelfMountL = 8 // The length of the bracket holding up the shelf is 6 inches
wallMountL = 6 // the length of the bracket
// Define constants required to calculate the thickness needed to support 300 lbs
sigmaAllow = 35000 // psi
width = 6 // inch
p = 300 // Force on shelf - lbs
L = 12 // inches
M = L * p / 2 // Moment experienced at fixed end of bracket
FOS = 2 // Factor of safety of 2 to be conservative
// Calculate the thickness off the bending stress and factor of safety
thickness = sqrt(6 * M * FOS / (width * sigmaAllow))
// 0.25 inch fillet radius
filletR = 0.25
// Sketch the bracket and extrude with fillets
bracket = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, wallMountL], %, $outerEdge)
|> line([-shelfMountL, 0], %, $seg01)
|> line([0, -thickness], %)
|> line([shelfMountL - thickness, 0], %, $innerEdge)
|> line([0, -wallMountL + thickness], %)
|> close(%)
|> extrude(width, %)
|> fillet({
radius: filletR,
tags: [
getNextAdjacentEdge(innerEdge)
]
}, %)
|> fillet({
radius: filletR + thickness,
tags: [
getNextAdjacentEdge(outerEdge)
]
}, %)
sketch001 = startSketchOn(bracket, seg01)
|> startProfileAt([4.28, 3.83], %)
|> line([2.17, -0.03], %)
|> line([-0.07, -1.8], %)
|> line([-2.07, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
|> extrude(10, %)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
assert_out("sketch_on_face_after_fillets_referencing_face", &result);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_circular_pattern3d_array_of_extrudes() {
let code = r#"plane001 = startSketchOn('XZ')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -19,6 +19,15 @@ macro_rules! kcl_test {
}
kcl_test!("sketch_on_face", kcl_test_sketch_on_face);
kcl_test!("neg_xz_plane", kcl_test_neg_xz_plane);
kcl_test!("xz_plane", kcl_test_xz_plane);
kcl_test!(
"sketch_on_face_after_fillets_referencing_face",
kcl_test_sketch_on_face_after_fillets_referencing_face
);
kcl_test!("circular_pattern3d_a_pattern", kcl_test_circular_pattern3d_a_pattern);
kcl_test!("linear_pattern3d_a_pattern", kcl_test_linear_pattern3d_a_pattern);
kcl_test!("tangential_arc", kcl_test_tangential_arc);
kcl_test!(
"big_number_angle_to_match_length_x",

View File

@ -8,33 +8,10 @@ use pretty_assertions::assert_eq;
/// Setup the engine and parse code for an ast.
async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid::Uuid)> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60));
let ws_client = reqwest::Client::builder()
.user_agent(user_agent)
// For file conversions we need this to be long.
.timeout(std::time::Duration::from_secs(600))
.connect_timeout(std::time::Duration::from_secs(60))
.tcp_keepalive(std::time::Duration::from_secs(600))
.http1_only();
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
// Create the client.
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
// Set a local engine address if it's set.
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
client.set_base_url(addr);
}
let tokens = kcl_lib::token::lexer(code)?;
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let ctx = kcl_lib::executor::ExecutorContext::new(&client, Default::default()).await?;
let ctx = kcl_lib::executor::ExecutorContext::new_with_default_client(Default::default()).await?;
let exec_state = ctx.run(&program, None, IdGenerator::default(), None).await?;
// We need to get the sketch ID.

View File

@ -2597,10 +2597,10 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
"@types/node@*", "@types/node@^22.5.0":
version "22.5.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.0.tgz#10f01fe9465166b4cab72e75f60d8b99d019f958"
integrity sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==
"@types/node@*", "@types/node@^22.7.8":
version "22.7.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.8.tgz#a1dbf0dc5f71bdd2642fc89caef65d58747ce825"
integrity sha512-a922jJy31vqR5sk+kAdIENJjHblqcZ4RmERviFsER4WJcEONqxKcjNOlk0q7OUfrF5sddT+vng070cdfMlrPLg==
dependencies:
undici-types "~6.19.2"
@ -8785,7 +8785,16 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -8879,7 +8888,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -9753,7 +9769,16 @@ word-wrap@^1.2.3, word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==