Compare commits

...

10 Commits

Author SHA1 Message Date
dc9000f6c7 inprogress conversion, lots of type error still, but think this is a dead end because artifact ids are not stable across executions 2024-09-30 10:43:09 +10:00
70694d9dd3 update types ready for new selections 2024-09-25 15:32:11 +10:00
6595fca000 fix dumb logic bug in indexing auto complete snippets (#3972)
* fix dumb logic bug in indexing auto complete snippets

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

* add more tests

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

* new images

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

* regenerate docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-09-25 02:35:23 +00:00
8b0b5a0215 In-app toasts for electron-updater notifications (#3902)
* Add custom updater model back after electron migration
Fixes #3872

* Enable release builds (temp)

* Lint & clean up

* Change approach to no user input, heads up with toast

* Re-enable prod builds

* Working toasts

* Only one toast

* Add missing type

* Clean up before review

* New toast design test

* Clean up

* Use theme colors, add link to changelog

---------

Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-09-24 13:55:42 -04:00
2263958fd0 UI bug fix: padding-inline logic regression (#3967)
* Add failing component regression tests for padding-inline logic

* Fix tests by fixing swapped logic, add comments
2024-09-24 13:49:15 -04:00
66e60f2ddb Bump clap from 4.5.17 to 4.5.18 in /src/wasm-lib (#3949)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.17 to 4.5.18.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.17...clap_complete-v4.5.18)

---
updated-dependencies:
- dependency-name: clap
  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-09-23 12:42:27 -07:00
5f51a0f569 Bump thiserror from 1.0.63 to 1.0.64 in /src/wasm-lib (#3948)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.63 to 1.0.64.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.63...1.0.64)

---
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-09-23 12:42:17 -07:00
aee1d66e56 Add ability to open KCL samples in-app (#3912)
* Add a shell script to get the list of KCL samples into the app

* Add support for overwriting current file with sample

* Move these KCL commands down into FileMachineProvider

* Add support for creating a new file on desktop

* Make it so these files aren't set to "renaming mode" right away

* Add support for initializing default values that are functions

* Add E2E tests

* Add a code menu item to load a sample

* Fix tsc issues

* Remove `yarn fetch:samples` from `yarn postinstall`

* Remove change to arg initialization logic, I was holding it wrong

* Switch to use new manifest file from kcl-samples repo

* Update tests now that we use proper sample titles

* Remove double-load from units menu test

* @jtran feedback

* Don't encode `https://` that's silly

* fmt

* Update e2e/playwright/testing-samples-loading.spec.ts

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Test feedback

* Add a test step to actually check the file contents were written to (@Irev-Dev feedback)

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-09-23 14:35:38 -04:00
1d1bb8cee0 Cut release v0.25.4 (#3952) 2024-09-23 13:10:12 -04:00
c7dd89e720 bump kcl-lib 2024-09-23 11:37:11 -04:00
66 changed files with 1638 additions and 465 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,195 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
import { bracket } from 'lib/exampleKcl'
import * as fsp from 'fs/promises'
import { join } from 'path'
import { FILE_EXT } from 'lib/constants'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Testing in-app sample loading', () => {
/**
* Note this test implicitly depends on the KCL sample "flange-with-patterns.kcl"
* and its title. https://github.com/KittyCAD/kcl-samples/blob/main/flange-with-patterns/flange-with-patterns.kcl
*/
test('Web: should overwrite current code, cannot create new file', async ({
page,
}) => {
const u = await getUtils(page)
await test.step(`Test setup`, async () => {
await page.addInitScript((code) => {
window.localStorage.setItem('persistCode', code)
}, bracket)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
})
// Locators and constants
const newSample = {
file: 'flange-with-patterns' + FILE_EXT,
title: 'Flange',
}
const commandBarButton = page.getByRole('button', { name: 'Commands' })
const samplesCommandOption = page.getByRole('option', {
name: 'Open Sample',
})
const commandSampleOption = page.getByRole('option', {
name: newSample.title,
exact: true,
})
const commandMethodArgButton = page.getByRole('button', {
name: 'Method',
})
const commandMethodOption = (name: 'Overwrite' | 'Create new file') =>
page.getByRole('option', {
name,
})
const warningText = page.getByText('Overwrite current file?')
const confirmButton = page.getByRole('button', { name: 'Submit command' })
const codeLocator = page.locator('.cm-content')
await test.step(`Precondition: check the initial code`, async () => {
await u.openKclCodePanel()
await expect(codeLocator).toContainText(bracket.split('\n')[0])
})
await test.step(`Load a KCL sample with the command palette`, async () => {
await commandBarButton.click()
await samplesCommandOption.click()
await commandSampleOption.click()
await commandMethodArgButton.click()
await expect(commandMethodOption('Create new file')).not.toBeVisible()
await commandMethodOption('Overwrite').click()
await expect(warningText).toBeVisible()
await confirmButton.click()
await expect(codeLocator).toContainText('// ' + newSample.title)
})
})
/**
* Note this test implicitly depends on the KCL samples:
* "flange-with-patterns.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/flange-with-patterns/flange-with-patterns.kcl
* "gear-rack.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/gear-rack.kcl
*/
test(
'Desktop: should create new file by default, optionally overwrite',
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
const { electronApp, page, dir } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.writeFile(join(bracketDir, 'main.kcl'), bracket, {
encoding: 'utf-8',
})
},
})
const u = await getUtils(page)
// Locators and constants
const sampleOne = {
file: 'flange-with-patterns' + FILE_EXT,
title: 'Flange',
}
const sampleTwo = {
file: 'gear-rack' + FILE_EXT,
title: '100mm Gear Rack',
}
const projectCard = page.getByRole('link', { name: 'bracket' })
const commandBarButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'Open Sample' })
const commandSampleOption = (name: string) =>
page.getByRole('option', {
name,
exact: true,
})
const commandMethodArgButton = page.getByRole('button', {
name: 'Method',
})
const commandMethodOption = page.getByRole('option', {
name: 'Overwrite',
})
const newFileWarning = page.getByText(
'Create a new file with the example code?'
)
const overwriteWarning = page.getByText('Overwrite current file?')
const confirmButton = page.getByRole('button', { name: 'Submit command' })
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const newlyCreatedFile = (name: string) =>
page.getByRole('listitem').filter({
has: page.getByRole('button', { name }),
})
const codeLocator = page.locator('.cm-content')
await test.step(`Test setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 })
await projectCard.click()
await u.waitForPageLoad()
})
await test.step(`Precondition: check the initial code`, async () => {
await u.openKclCodePanel()
await expect(codeLocator).toContainText(bracket.split('\n')[0])
await u.openFilePanel()
await expect(projectMenuButton).toContainText('main.kcl')
await expect(newlyCreatedFile(sampleOne.file)).not.toBeVisible()
})
await test.step(`Load a KCL sample with the command palette`, async () => {
await commandBarButton.click()
await commandOption.click()
await commandSampleOption(sampleOne.title).click()
await expect(overwriteWarning).not.toBeVisible()
await expect(newFileWarning).toBeVisible()
await confirmButton.click()
})
await test.step(`Ensure we made and opened a new file`, async () => {
await expect(codeLocator).toContainText('// ' + sampleOne.title)
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file)
})
await test.step(`Now overwrite the current file`, async () => {
await commandBarButton.click()
await commandOption.click()
await commandSampleOption(sampleTwo.title).click()
await commandMethodArgButton.click()
await commandMethodOption.click()
await expect(commandMethodArgButton).toContainText('overwrite')
await expect(newFileWarning).not.toBeVisible()
await expect(overwriteWarning).toBeVisible()
await confirmButton.click()
})
await test.step(`Ensure we overwrote the current file without navigating`, async () => {
await expect(codeLocator).toContainText('// ' + sampleTwo.title)
await test.step(`Check actual file contents`, async () => {
await expect
.poll(async () => {
return await fsp.readFile(
join(dir, 'bracket', sampleOne.file),
'utf-8'
)
})
.toContain('// ' + sampleTwo.title)
})
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file)
})
await electronApp.close()
}
)
})

View File

@ -13,9 +13,6 @@ test.afterEach(async ({ page }, testInfo) => {
test('Units menu', async ({ page }) => { test('Units menu', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
const unitsMenuButton = page.getByRole('button', { const unitsMenuButton = page.getByRole('button', {

4
interface.d.ts vendored
View File

@ -63,6 +63,10 @@ export interface IElectronAPI {
kittycad: (access: string, args: any) => any kittycad: (access: string, args: any) => any
listMachines: () => Promise<MachinesListing> listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null> getMachineApiIp: () => Promise<string | null>
onUpdateDownloaded: (
callback: (value: string) => void
) => Electron.IpcRenderer
appRestart: () => void
} }
declare global { declare global {

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.25.3", "version": "0.25.4",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {
@ -83,6 +83,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)", "isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
@ -90,7 +91,7 @@
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings", "wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src e2e packages/codemirror-lsp-client", "lint": "eslint --fix src e2e packages/codemirror-lsp-client",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild", "postinstall": "yarn fetch:samples && yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev", "make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",

View File

@ -0,0 +1,152 @@
[
{
"file": "80-20-rail.kcl",
"title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
},
{
"file": "a-parametric-bearing-pillow-block.kcl",
"title": "A Parametric Bearing Pillow Block",
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
},
{
"file": "ball-bearing.kcl",
"title": "Ball Bearing",
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
},
{
"file": "bracket.kcl",
"title": "Shelf Bracket",
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
},
{
"file": "brake-caliper.kcl",
"title": "Brake Caliper",
"description": "Brake calipers are used to squeeze the brake pads against the rotor, causing larger and larger amounts of friction depending on how hard the brakes are pressed."
},
{
"file": "car-wheel.kcl",
"title": "Car Wheel",
"description": "A sports car wheel with a circular lug pattern and spokes."
},
{
"file": "car-wheel-assembly.kcl",
"title": "Car Wheel Assembly",
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
},
{
"file": "enclosure.kcl",
"title": "Enclosure",
"description": "An enclosure body and sealing lid for storing items"
},
{
"file": "flange-with-patterns.kcl",
"title": "Flange",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "flange-xy.kcl",
"title": "Flange with XY coordinates",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "focusrite-scarlett-mounting-bracket.kcl",
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
},
{
"file": "french-press.kcl",
"title": "French Press",
"description": "A french press immersion coffee maker"
},
{
"file": "gear.kcl",
"title": "Gear",
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
},
{
"file": "gear-rack.kcl",
"title": "100mm Gear Rack",
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
},
{
"file": "hex-nut.kcl",
"title": "Hex nut",
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
},
{
"file": "kitt.kcl",
"title": "Kitt",
"description": "The beloved KittyCAD mascot in a voxelized style."
},
{
"file": "lego.kcl",
"title": "Lego Brick",
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
},
{
"file": "lug-nut.kcl",
"title": "Lug Nut",
"description": "lug Nuts are essential components used to create secure connections, whether for electrical purposes, like terminating wires or grounding, or for mechanical purposes, such as providing mounting points or reinforcing structural joints."
},
{
"file": "mounting-plate.kcl",
"title": "Mounting Plate",
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
},
{
"file": "multi-axis-robot.kcl",
"title": "Robot Arm",
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
},
{
"file": "pipe.kcl",
"title": "Pipe",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "pipe-flange-assembly.kcl",
"title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
},
{
"file": "poopy-shoe.kcl",
"title": "Poopy Shoe",
"description": "poop shute for bambu labs printer - optimized for printing."
},
{
"file": "router-template-cross-bar.kcl",
"title": "Router template for a cross bar",
"description": "A guide for routing a notch into a cross bar."
},
{
"file": "router-template-slate.kcl",
"title": "Router template for a slate",
"description": "A guide for routing a slate for a cross bar."
},
{
"file": "sheet-metal-bracket.kcl",
"title": "Sheet Metal Bracket",
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
},
{
"file": "socket-head-cap-screw.kcl",
"title": "Socket Head Cap Screw",
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
},
{
"file": "tire.kcl",
"title": "Tire",
"description": "A tire is a critical component of a vehicle that provides the necessary traction and grip between the car and the road. It supports the vehicle's weight and absorbs shocks from road irregularities."
},
{
"file": "washer.kcl",
"title": "Washer",
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
},
{
"file": "wheel-rotor.kcl",
"title": "Wheel rotor",
"description": "A component of a disc brake system. It provides a surface for brake pads to press against, generating the friction needed to slow or stop the vehicle."
}
]

View File

@ -22,6 +22,7 @@ import {
} from 'lib/toolbar' } from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { convertSelectionsToOld } from 'lib/selections'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -38,12 +39,17 @@ export function Toolbar({
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary' '!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
const sketchPathId = useMemo(() => { const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) { if (
!isSingleCursorInPipe(
convertSelectionsToOld(context.selectionRanges),
kclManager.ast
)
) {
return false return false
} }
return isCursorInSketchCommandRange( return isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,
context.selectionRanges convertSelectionsToOld(context.selectionRanges)
) )
}, [engineCommandManager.artifactGraph, context.selectionRanges]) }, [engineCommandManager.artifactGraph, context.selectionRanges])

View File

@ -77,7 +77,7 @@ import {
createPipeSubstitution, createPipeSubstitution,
findUniqueName, findUniqueName,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { Selections, getEventForSegmentSelection } from 'lib/selections' import { Selections__old, getEventForSegmentSelection } from 'lib/selections'
import { createGridHelper, orthoScale, perspScale } from './helpers' import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
@ -374,7 +374,7 @@ export class SceneEntities {
forward: [number, number, number] forward: [number, number, number]
up: [number, number, number] up: [number, number, number]
position?: [number, number, number] position?: [number, number, number]
selectionRanges?: Selections selectionRanges?: Selections__old
}): Promise<{ }): Promise<{
truncatedAst: Program truncatedAst: Program
programMemoryOverride: ProgramMemory programMemoryOverride: ProgramMemory
@ -1171,6 +1171,7 @@ export class SceneEntities {
}, },
onMove: () => {}, onMove: () => {},
onClick: (args) => { onClick: (args) => {
console.log('onClick', args)
if (args?.mouseEvent.which !== 1) return if (args?.mouseEvent.which !== 1) return
if (!args || !args.selected) { if (!args || !args.selected) {
sceneInfra.modelingSend({ sceneInfra.modelingSend({
@ -1183,6 +1184,7 @@ export class SceneEntities {
} }
const { selected } = args const { selected } = args
const event = getEventForSegmentSelection(selected) const event = getEventForSegmentSelection(selected)
console.log('event', event)
if (!event) return if (!event) return
sceneInfra.modelingSend(event) sceneInfra.modelingSend(event)
}, },

View File

@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { ActionButton } from './ActionButton'
describe('ActionButton tests', () => {
it('ActionButton with no iconStart or iconEnd should have even left and right padding', () => {
render(<ActionButton Element="button">No icons</ActionButton>)
expect(screen.getByRole('button')).toHaveClass('px-2')
})
it('ActionButton with iconStart should have no padding on the left', () => {
render(
<ActionButton Element="button" iconStart={{ icon: 'trash' }}>
Start icon only
</ActionButton>
)
expect(screen.getByRole('button')).toHaveClass('pr-2')
})
it('ActionButton with iconEnd should have no padding on the right', () => {
render(
<ActionButton Element="button" iconEnd={{ icon: 'trash' }}>
End icon only
</ActionButton>
)
expect(screen.getByRole('button')).toHaveClass('pl-2')
})
it('ActionButton with both icons should have no padding on either side', () => {
render(
<ActionButton
Element="button"
iconStart={{ icon: 'trash' }}
iconEnd={{ icon: 'trash' }}
>
Both icons
</ActionButton>
)
expect(screen.getByRole('button')).not.toHaveClass('px-2')
expect(screen.getByRole('button')).toHaveClass('px-0')
})
})

View File

@ -44,11 +44,11 @@ export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
const classNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${ const classNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${
props.iconStart props.iconStart
? props.iconEnd ? props.iconEnd
? 'px-0' ? 'px-0' // No padding if both icons are present
: 'pr-2' : 'pr-2' // Padding on the right if only the start icon is present
: props.iconEnd : props.iconEnd
? 'px-2' ? 'pl-2' // Padding on the left if only the end icon is present
: 'pl-2' : 'px-2' // Padding on both sides if no icons are present
} ${props.className ? props.className : ''}` } ${props.className ? props.className : ''}`
switch (props.Element) { switch (props.Element) {

View File

@ -12,6 +12,7 @@ import { useKclContext } from 'lang/KclProvider'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'lang/langHelpers'
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
import { convertSelectionsToOld } from 'lib/selections'
export const AvailableVars = ({ export const AvailableVars = ({
onVarClick, onVarClick,
@ -96,7 +97,8 @@ export function useCalc({
} { } {
const { programMemory } = useKclContext() const { programMemory } = useKclContext()
const { context } = useModelingContext() const { context } = useModelingContext()
const selectionRange = context.selectionRanges.codeBasedSelections[0].range const selectionRange = convertSelectionsToOld(context.selectionRanges)
.codeBasedSelections[0].range
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const [availableVarInfo, setAvailableVarInfo] = useState< const [availableVarInfo, setAvailableVarInfo] = useState<
ReturnType<typeof findAllPreviousVariables> ReturnType<typeof findAllPreviousVariables>

View File

@ -6,8 +6,8 @@ import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { AnyStateMachine, StateFrom } from 'xstate' import { AnyStateMachine, StateFrom } from 'xstate'
const contextSelector = (snapshot: StateFrom<AnyStateMachine>) => const contextSelector = (snapshot: StateFrom<AnyStateMachine> | undefined) =>
snapshot.context snapshot?.context
function CommandArgOptionInput({ function CommandArgOptionInput({
arg, arg,

View File

@ -2,7 +2,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from '../CustomIcon' import { CustomIcon } from '../CustomIcon'
import React, { useState } from 'react' import React, { useState } from 'react'
import { ActionButton } from '../ActionButton' import { ActionButton } from '../ActionButton'
import { Selections, getSelectionTypeDisplayText } from 'lib/selections' import { Selections__old, getSelectionTypeDisplayText } from 'lib/selections'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes' import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
@ -125,7 +125,9 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
<span data-testid="header-arg-value"> <span data-testid="header-arg-value">
{argValue ? ( {argValue ? (
arg.inputType === 'selection' ? ( arg.inputType === 'selection' ? (
getSelectionTypeDisplayText(argValue as Selections) getSelectionTypeDisplayText(
argValue as Selections__old
)
) : arg.inputType === 'kcl' ? ( ) : arg.inputType === 'kcl' ? (
roundOff( roundOff(
Number( Number(
@ -140,7 +142,11 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
JSON.stringify(argValue) JSON.stringify(argValue)
) )
) : ( ) : (
<em>{argValue}</em> <em>
{arg.valueSummary
? arg.valueSummary(argValue)
: argValue}
</em>
) )
) : null} ) : null}
</span> </span>

View File

@ -58,7 +58,17 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
return ( return (
<CommandBarHeader> <CommandBarHeader>
<p className="px-4">Confirm {selectedCommand?.name}</p> <p className="px-4">
{selectedCommand?.reviewMessage ? (
selectedCommand.reviewMessage instanceof Function ? (
selectedCommand.reviewMessage(commandBarState.context)
) : (
selectedCommand.reviewMessage
)
) : (
<>Confirm {selectedCommand?.name}</>
)}
</p>
<form <form
id="review-form" id="review-form"
className="absolute opacity-0 inset-0 pointer-events-none" className="absolute opacity-0 inset-0 pointer-events-none"

View File

@ -3,8 +3,10 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { CommandArgument } from 'lib/commandTypes' import { CommandArgument } from 'lib/commandTypes'
import { import {
Selection, Selection__old,
canSubmitSelectionArg, canSubmitSelectionArg,
convertSelectionToOld,
convertSelectionsToOld,
getSelectionType, getSelectionType,
getSelectionTypeDisplayText, getSelectionTypeDisplayText,
} from 'lib/selections' } from 'lib/selections'
@ -12,13 +14,15 @@ import { modelingMachine } from 'machines/modelingMachine'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { StateFrom } from 'xstate' import { StateFrom } from 'xstate'
const semanticEntityNames: { [key: string]: Array<Selection['type']> } = { const semanticEntityNames: { [key: string]: Array<Selection__old['type']> } = {
face: ['extrude-wall', 'start-cap', 'end-cap'], face: ['extrude-wall', 'start-cap', 'end-cap'],
edge: ['edge', 'line', 'arc'], edge: ['edge', 'line', 'arc'],
point: ['point', 'line-end', 'line-mid'], point: ['point', 'line-end', 'line-mid'],
} }
function getSemanticSelectionType(selectionType: Array<Selection['type']>) { function getSemanticSelectionType(
selectionType: Array<Selection__old['type']>
) {
const semanticSelectionType = new Set() const semanticSelectionType = new Set()
selectionType.forEach((type) => { selectionType.forEach((type) => {
Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => { Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => {
@ -31,8 +35,8 @@ function getSemanticSelectionType(selectionType: Array<Selection['type']>) {
return Array.from(semanticSelectionType) return Array.from(semanticSelectionType)
} }
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) => const selectionSelector = (snapshot?: StateFrom<typeof modelingMachine>) =>
snapshot.context.selectionRanges snapshot?.context.selectionRanges
function CommandBarSelectionInput({ function CommandBarSelectionInput({
arg, arg,
@ -49,10 +53,14 @@ function CommandBarSelectionInput({
const [hasSubmitted, setHasSubmitted] = useState(false) const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector) const selection = useSelector(arg.machineActor, selectionSelector)
const selectionsByType = useMemo(() => { const selectionsByType = useMemo(() => {
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1] const selectionRangeEnd = !selection
return !selectionRangeEnd || selectionRangeEnd === code.length ? null
: convertSelectionsToOld(selection)?.codeBasedSelections[0]?.range[1]
return !selectionRangeEnd || selectionRangeEnd === code.length || !selection
? 'none' ? 'none'
: getSelectionType(selection) : !selection
? 'none'
: getSelectionType(convertSelectionsToOld(selection))
}, [selection, code]) }, [selection, code])
const canSubmitSelection = useMemo<boolean>( const canSubmitSelection = useMemo<boolean>(
() => canSubmitSelectionArg(selectionsByType, arg), () => canSubmitSelectionArg(selectionsByType, arg),
@ -87,6 +95,8 @@ function CommandBarSelectionInput({
onSubmit(selection) onSubmit(selection)
} }
const selectionOld = selection && convertSelectionsToOld(selection)
return ( return (
<form id="arg-form" onSubmit={handleSubmit}> <form id="arg-form" onSubmit={handleSubmit}>
<label <label
@ -96,7 +106,7 @@ function CommandBarSelectionInput({
} }
> >
{canSubmitSelection {canSubmitSelection
? getSelectionTypeDisplayText(selection) + ' selected' ? getSelectionTypeDisplayText(selectionOld) + ' selected'
: `Please select ${ : `Please select ${
arg.multiple ? 'one or more ' : 'one ' arg.multiple ? 'one or more ' : 'one '
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`} }${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}

View File

@ -0,0 +1,16 @@
interface CommandBarOverwriteWarningProps {
heading?: string
message?: string
}
export function CommandBarOverwriteWarning({
heading = 'Overwrite current file?',
message = 'This will permanently replace the current code in the editor.',
}: CommandBarOverwriteWarningProps) {
return (
<>
<p className="font-bold text-destroy-60">{heading}</p>
<p>{message}</p>
</>
)
}

View File

@ -21,6 +21,7 @@ export const EngineCommands = () => {
const [engineCommands, clearEngineCommands] = useEngineCommands() const [engineCommands, clearEngineCommands] = useEngineCommands()
const [containsFilter, setContainsFilter] = useState('') const [containsFilter, setContainsFilter] = useState('')
const [customCmd, setCustomCmd] = useState('') const [customCmd, setCustomCmd] = useState('')
console.log(JSON.stringify(engineCommands, null, 2))
return ( return (
<div> <div>
<input <input
@ -64,7 +65,10 @@ export const EngineCommands = () => {
) )
})} })}
</div> </div>
<button data-testid="clear-commands" onClick={clearEngineCommands}> <button
data-testid="clear-commands"
onClick={() => clearEngineCommands()}
>
Clear Clear
</button> </button>
<br /> <br />

View File

@ -2,7 +2,7 @@ import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import React, { createContext } from 'react' import React, { createContext, useEffect, useMemo } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {
Actor, Actor,
@ -22,6 +22,12 @@ import {
} from 'lib/constants' } from 'lib/constants'
import { getProjectInfo } from 'lib/desktop' import { getProjectInfo } from 'lib/desktop'
import { getNextDirName, getNextFileName } from 'lib/desktopFS' import { getNextDirName, getNextFileName } from 'lib/desktopFS'
import { kclCommands } from 'lib/kclCommands'
import { codeManager, kclManager } from 'lib/singletons'
import {
getKclSamplesManifest,
KclSamplesManifestItem,
} from 'lib/getKclSamplesManifest'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -41,6 +47,16 @@ export const FileMachineProvider = ({
const navigate = useNavigate() const navigate = useNavigate()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
[]
)
useEffect(() => {
async function fetchKclSamples() {
setKclSamples(await getKclSamplesManifest())
}
fetchKclSamples().catch(reportError)
}, [])
const [state, send] = useMachine( const [state, send] = useMachine(
fileMachine.provide({ fileMachine.provide({
@ -121,6 +137,7 @@ export const FileMachineProvider = ({
return { return {
message: `Successfully created "${createdName}"`, message: `Successfully created "${createdName}"`,
path: createdPath, path: createdPath,
shouldSetToRename: input.shouldSetToRename,
} }
}), }),
createFile: fromPromise(async ({ input }) => { createFile: fromPromise(async ({ input }) => {
@ -271,6 +288,46 @@ export const FileMachineProvider = ({
} }
) )
const kclCommandMemo = useMemo(
() =>
kclCommands(
async (data) => {
if (data.method === 'overwrite') {
codeManager.updateCodeStateEditor(data.code)
await kclManager.executeCode(true)
await codeManager.writeToFile()
} else if (data.method === 'newFile' && isDesktop()) {
send({
type: 'Create file',
data: {
name: data.sampleName,
content: data.code,
makeDir: false,
},
})
}
},
kclSamples.map((sample) => ({
value: sample.file,
name: sample.title,
}))
).filter(
(command) => kclSamples.length || command.name !== 'open-kcl-example'
),
[codeManager, kclManager, send, kclSamples]
)
useEffect(() => {
commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
return () => {
commandBarSend({
type: 'Remove commands',
data: { commands: kclCommandMemo },
})
}
}, [commandBarSend, kclCommandMemo])
return ( return (
<FileContext.Provider <FileContext.Provider
value={{ value={{

View File

@ -393,14 +393,14 @@ export const FileTreeMenu = () => {
function createFile() { function createFile() {
send({ send({
type: 'Create file', type: 'Create file',
data: { name: '', makeDir: false }, data: { name: '', makeDir: false, shouldSetToRename: true },
}) })
} }
function createFolder() { function createFolder() {
send({ send({
type: 'Create file', type: 'Create file',
data: { name: '', makeDir: true }, data: { name: '', makeDir: true, shouldSetToRename: true },
}) })
} }

View File

@ -37,13 +37,17 @@ import {
} from './Toolbar/SetAngleBetween' } from './Toolbar/SetAngleBetween'
import { applyConstraintAngleLength } from './Toolbar/setAngleLength' import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { import {
Selections, Selections__old,
canSweepSelection, canSweepSelection,
handleSelectionBatch, handleSelectionBatch,
isSelectionLastLine, isSelectionLastLine,
isRangeBetweenCharacters, isRangeBetweenCharacters,
isSketchPipe, isSketchPipe,
updateSelections, updateSelections,
convertSelectionsToOld,
convertSelectionToOld,
Selections,
updateSelections2,
} from 'lib/selections' } from 'lib/selections'
import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
@ -249,6 +253,7 @@ export const ModelingMachineProvider = ({
'Set selection': assign( 'Set selection': assign(
({ context: { selectionRanges, sketchDetails }, event }) => { ({ context: { selectionRanges, sketchDetails }, event }) => {
// this was needed for ts after adding 'Set selection' action to on done modal events // this was needed for ts after adding 'Set selection' action to on done modal events
// const oldSelections = convertSelectionsToOld(selectionRanges)
const setSelections = const setSelections =
('data' in event && ('data' in event &&
event.data && event.data &&
@ -275,8 +280,12 @@ export const ModelingMachineProvider = ({
}) })
}) })
} }
// let selections: Selections__old = {
// codeBasedSelections: [],
// otherSelections: [],
// }
let selections: Selections = { let selections: Selections = {
codeBasedSelections: [], graphSelections: [],
otherSelections: [], otherSelections: [],
} }
if (setSelections.selectionType === 'singleCodeCursor') { if (setSelections.selectionType === 'singleCodeCursor') {
@ -286,21 +295,28 @@ export const ModelingMachineProvider = ({
!editorManager.isShiftDown !editorManager.isShiftDown
) { ) {
selections = { selections = {
codeBasedSelections: [], graphSelections: [],
otherSelections: [], otherSelections: [],
} }
} else if ( } else if (
setSelections.selection && setSelections.selection &&
!editorManager.isShiftDown !editorManager.isShiftDown
) { ) {
// const oldSelection = convertSelectionToOld(setSelections.selection)
// if (oldSelection) {
// }
selections = { selections = {
codeBasedSelections: [setSelections.selection], graphSelections: [setSelections.selection],
otherSelections: [], otherSelections: [],
} }
} else if (setSelections.selection && editorManager.isShiftDown) { } else if (setSelections.selection && editorManager.isShiftDown) {
// const oldSelection = convertSelectionToOld(setSelections.selection)
// if (oldSelection) {
// }
selections = { selections = {
codeBasedSelections: [ graphSelections: [
...selectionRanges.codeBasedSelections, ...selectionRanges.graphSelections,
setSelections.selection, setSelections.selection,
], ],
otherSelections: selectionRanges.otherSelections, otherSelections: selectionRanges.otherSelections,
@ -312,7 +328,7 @@ export const ModelingMachineProvider = ({
codeMirrorSelection, codeMirrorSelection,
updateSceneObjectColors, updateSceneObjectColors,
} = handleSelectionBatch({ } = handleSelectionBatch({
selections, selections: convertSelectionsToOld(selections),
}) })
codeMirrorSelection && dispatchSelection(codeMirrorSelection) codeMirrorSelection && dispatchSelection(codeMirrorSelection)
engineEvents && engineEvents &&
@ -336,18 +352,18 @@ export const ModelingMachineProvider = ({
if (setSelections.selectionType === 'otherSelection') { if (setSelections.selectionType === 'otherSelection') {
if (editorManager.isShiftDown) { if (editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: selectionRanges.codeBasedSelections, graphSelections: selectionRanges.graphSelections,
otherSelections: [setSelections.selection], otherSelections: [setSelections.selection],
} }
} else { } else {
selections = { selections = {
codeBasedSelections: [], graphSelections: [],
otherSelections: [setSelections.selection], otherSelections: [setSelections.selection],
} }
} }
const { engineEvents, updateSceneObjectColors } = const { engineEvents, updateSceneObjectColors } =
handleSelectionBatch({ handleSelectionBatch({
selections, selections: convertSelectionsToOld(selections),
}) })
engineEvents && engineEvents &&
engineEvents.forEach((event) => { engineEvents.forEach((event) => {
@ -360,7 +376,9 @@ export const ModelingMachineProvider = ({
} }
} }
if (setSelections.selectionType === 'completeSelection') { if (setSelections.selectionType === 'completeSelection') {
editorManager.selectRange(setSelections.selection) editorManager.selectRange(
convertSelectionsToOld(setSelections.selection)
)
if (!sketchDetails) if (!sketchDetails)
return { return {
selectionRanges: setSelections.selection, selectionRanges: setSelections.selection,
@ -494,10 +512,11 @@ export const ModelingMachineProvider = ({
'has valid sweep selection': ({ context: { selectionRanges } }) => { 'has valid sweep selection': ({ context: { selectionRanges } }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected // A user can begin extruding if they either have 1+ faces selected or nothing selected
// TODO: I believe this guard only allows for extruding a single face at a time // TODO: I believe this guard only allows for extruding a single face at a time
const _selections = convertSelectionsToOld(selectionRanges)
const hasNoSelection = const hasNoSelection =
selectionRanges.codeBasedSelections.length === 0 || _selections.codeBasedSelections.length === 0 ||
isRangeBetweenCharacters(selectionRanges) || isRangeBetweenCharacters(_selections) ||
isSelectionLastLine(selectionRanges, codeManager.code) isSelectionLastLine(_selections, codeManager.code)
if (hasNoSelection) { if (hasNoSelection) {
// they have no selection, we should enable the button // they have no selection, we should enable the button
@ -505,31 +524,34 @@ export const ModelingMachineProvider = ({
// BUT only if there's extrudable geometry // BUT only if there's extrudable geometry
return doesSceneHaveSweepableSketch(kclManager.ast) return doesSceneHaveSweepableSketch(kclManager.ast)
} }
if (!isSketchPipe(selectionRanges)) return false if (!isSketchPipe(_selections)) return false
return canSweepSelection(selectionRanges) return canSweepSelection(_selections)
}, },
'has valid selection for deletion': ({ 'has valid selection for deletion': ({
context: { selectionRanges }, context: { selectionRanges },
}) => { }) => {
const _selections = convertSelectionsToOld(selectionRanges)
if (!commandBarState.matches('Closed')) return false if (!commandBarState.matches('Closed')) return false
if (selectionRanges.codeBasedSelections.length <= 0) return false if (_selections.codeBasedSelections.length <= 0) return false
return true return true
}, },
'has valid fillet selection': ({ context: { selectionRanges } }) => 'has valid fillet selection': ({ context: { selectionRanges } }) => {
hasValidFilletSelection({ const _selections = convertSelectionsToOld(selectionRanges)
selectionRanges, return hasValidFilletSelection({
selectionRanges: _selections,
ast: kclManager.ast, ast: kclManager.ast,
code: codeManager.code, code: codeManager.code,
}), })
},
'Selection is on face': ({ context: { selectionRanges }, event }) => { 'Selection is on face': ({ context: { selectionRanges }, event }) => {
if (event.type !== 'Enter sketch') return false if (event.type !== 'Enter sketch') return false
if (event.data?.forceNewSketch) return false if (event.data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) const _selections = convertSelectionsToOld(selectionRanges)
return false if (!isSingleCursorInPipe(_selections, kclManager.ast)) return false
return !!isCursorInSketchCommandRange( return !!isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,
selectionRanges _selections
) )
}, },
'Has exportable geometry': () => { 'Has exportable geometry': () => {
@ -619,7 +641,8 @@ export const ModelingMachineProvider = ({
}), }),
'animate-to-sketch': fromPromise( 'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => { async ({ input: { selectionRanges } }) => {
const sourceRange = selectionRanges.codeBasedSelections[0].range const _selections = convertSelectionsToOld(selectionRanges)
const sourceRange = _selections.codeBasedSelections[0].range
const sketchPathToNode = getNodePathFromSourceRange( const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,
sourceRange sourceRange
@ -643,10 +666,11 @@ export const ModelingMachineProvider = ({
), ),
'Get horizontal info': fromPromise( 'Get horizontal info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const _selections = convertSelectionsToOld(selectionRanges)
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
selectionRanges, selectionRanges: _selections,
}) })
const _modifiedAst = parse(recast(modifiedAst)) const _modifiedAst = parse(recast(modifiedAst))
if (!sketchDetails) if (!sketchDetails)
@ -664,7 +688,8 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
const selection = updateSelections( // const selection = updateSelections(
const selection = updateSelections2(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
updatedAst.newAst updatedAst.newAst
@ -682,7 +707,7 @@ export const ModelingMachineProvider = ({
const { modifiedAst, pathToNodeMap } = const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({ await applyConstraintHorzVertDistance({
constraint: 'setVertDistance', constraint: 'setVertDistance',
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
}) })
const _modifiedAst = parse(recast(modifiedAst)) const _modifiedAst = parse(recast(modifiedAst))
if (!sketchDetails) if (!sketchDetails)
@ -702,7 +727,7 @@ export const ModelingMachineProvider = ({
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
) )
if (err(selection)) return Promise.reject(selection) if (err(selection)) return Promise.reject(selection)
@ -716,15 +741,15 @@ export const ModelingMachineProvider = ({
'Get angle info': fromPromise( 'Get angle info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => { async ({ input: { selectionRanges, sketchDetails } }) => {
const info = angleBetweenInfo({ const info = angleBetweenInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
}) })
if (err(info)) return Promise.reject(info) if (err(info)) return Promise.reject(info)
const { modifiedAst, pathToNodeMap } = await (info.enabled const { modifiedAst, pathToNodeMap } = await (info.enabled
? applyConstraintAngleBetween({ ? applyConstraintAngleBetween({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
}) })
: applyConstraintAngleLength({ : applyConstraintAngleLength({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
angleOrLength: 'setAngle', angleOrLength: 'setAngle',
})) }))
const _modifiedAst = parse(recast(modifiedAst)) const _modifiedAst = parse(recast(modifiedAst))
@ -747,7 +772,7 @@ export const ModelingMachineProvider = ({
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
) )
if (err(selection)) return Promise.reject(selection) if (err(selection)) return Promise.reject(selection)

View File

@ -9,10 +9,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
export const KclEditorMenu = ({ children }: PropsWithChildren) => { export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } = const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable() useConvertToVariable()
const { commandBarSend } = useCommandsContext()
return ( return (
<Menu> <Menu>
@ -77,6 +79,22 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
</small> </small>
</a> </a>
</Menu.Item> </Menu.Item>
<Menu.Item>
<button
onClick={() => {
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'code',
name: 'open-kcl-example',
},
})
}}
className={styles.button}
>
<span>Load a sample model</span>
</button>
</Menu.Item>
<Menu.Item> <Menu.Item>
<a <a
className={styles.button} className={styles.button}
@ -85,7 +103,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()} onClick={openExternalBrowserIfDesktop()}
> >
<span>KCL samples</span> <span>View all samples</span>
<small> <small>
zoo.dev zoo.dev
<FontAwesomeIcon <FontAwesomeIcon

View File

@ -0,0 +1,64 @@
import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
export function ToastUpdate({
version,
onRestart,
}: {
version: string
onRestart: () => void
}) {
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<div className="my-4 flex items-baseline">
<span
className="px-3 py-1 text-xl rounded-full bg-primary text-chalkboard-10"
data-testid="update-version"
>
v{version}
</span>
<span className="ml-4 text-md text-bold">
A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '}
<a
onClick={openExternalBrowserIfDesktop(
`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`}
target="_blank"
rel="noreferrer"
>
here on GitHub.
</a>
</span>
</div>
<div className="flex justify-between gap-8">
<ActionButton
Element="button"
iconStart={{
icon: 'arrowRotateRight',
}}
name="Restart app now"
onClick={onRestart}
>
Restart app now
</ActionButton>
<ActionButton
Element="button"
iconStart={{
icon: 'checkmark',
}}
name="Got it"
onClick={() => {
toast.dismiss()
}}
>
Got it
</ActionButton>
</div>
</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { Program, Expr, VariableDeclarator } from '../../lang/wasm' import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -18,7 +18,7 @@ import { TransformInfo } from 'lang/std/stdTypes'
export function equalAngleInfo({ export function equalAngleInfo({
selectionRanges, selectionRanges,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
}): }):
| { | {
transforms: TransformInfo[] transforms: TransformInfo[]
@ -82,7 +82,7 @@ export function equalAngleInfo({
export function applyConstraintEqualAngle({ export function applyConstraintEqualAngle({
selectionRanges, selectionRanges,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
}): }):
| { | {
modifiedAst: Program modifiedAst: Program

View File

@ -1,5 +1,5 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { Program, Expr, VariableDeclarator } from '../../lang/wasm' import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -18,7 +18,7 @@ import { err } from 'lib/trap'
export function setEqualLengthInfo({ export function setEqualLengthInfo({
selectionRanges, selectionRanges,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
}): }):
| { | {
transforms: TransformInfo[] transforms: TransformInfo[]
@ -83,7 +83,7 @@ export function setEqualLengthInfo({
export function applyConstraintEqualLength({ export function applyConstraintEqualLength({
selectionRanges, selectionRanges,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
}): }):
| { | {
modifiedAst: Program modifiedAst: Program

View File

@ -1,5 +1,5 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { Program, ProgramMemory, Expr } from '../../lang/wasm' import { Program, ProgramMemory, Expr } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -15,7 +15,7 @@ import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap' import { err } from 'lib/trap'
export function horzVertInfo( export function horzVertInfo(
selectionRanges: Selections, selectionRanges: Selections__old,
horOrVert: 'vertical' | 'horizontal' horOrVert: 'vertical' | 'horizontal'
): ):
| { | {
@ -53,7 +53,7 @@ export function horzVertInfo(
} }
export function applyConstraintHorzVert( export function applyConstraintHorzVert(
selectionRanges: Selections, selectionRanges: Selections__old,
horOrVert: 'vertical' | 'horizontal', horOrVert: 'vertical' | 'horizontal',
ast: Program, ast: Program,
programMemory: ProgramMemory programMemory: ProgramMemory

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Program, Expr, VariableDeclarator } from '../../lang/wasm' import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
import { Selections__old } from 'lib/selections'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
@ -25,12 +25,12 @@ const getModalInfo = createInfoModal(GetInfoModal)
export function intersectInfo({ export function intersectInfo({
selectionRanges, selectionRanges,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
}): }):
| { | {
transforms: TransformInfo[] transforms: TransformInfo[]
enabled: boolean enabled: boolean
forcedSelectionRanges: Selections forcedSelectionRanges: Selections__old
} }
| Error { | Error {
if (selectionRanges.codeBasedSelections.length < 2) { if (selectionRanges.codeBasedSelections.length < 2) {
@ -134,7 +134,7 @@ export function intersectInfo({
export async function applyConstraintIntersect({ export async function applyConstraintIntersect({
selectionRanges, selectionRanges,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program
pathToNodeMap: PathToNodeMap pathToNodeMap: PathToNodeMap

View File

@ -1,5 +1,5 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections' import { Selection__old, Selections__old } from 'lib/selections'
import { PathToNode, Program, Expr } from '../../lang/wasm' import { PathToNode, Program, Expr } from '../../lang/wasm'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
@ -18,13 +18,13 @@ export function removeConstrainingValuesInfo({
selectionRanges, selectionRanges,
pathToNodes, pathToNodes,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
pathToNodes?: Array<PathToNode> pathToNodes?: Array<PathToNode>
}): }):
| { | {
transforms: TransformInfo[] transforms: TransformInfo[]
enabled: boolean enabled: boolean
updatedSelectionRanges: Selections updatedSelectionRanges: Selections__old
} }
| Error { | Error {
const paths = const paths =
@ -45,7 +45,7 @@ export function removeConstrainingValuesInfo({
? { ? {
otherSelections: [], otherSelections: [],
codeBasedSelections: nodes.map( codeBasedSelections: nodes.map(
(node): Selection => ({ (node): Selection__old => ({
range: [node.start, node.end], range: [node.start, node.end],
type: 'default', type: 'default',
}) })
@ -73,7 +73,7 @@ export function applyRemoveConstrainingValues({
selectionRanges, selectionRanges,
pathToNodes, pathToNodes,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
pathToNodes?: Array<PathToNode> pathToNodes?: Array<PathToNode>
}): }):
| { | {

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Program, Expr } from '../../lang/wasm' import { Program, Expr } from '../../lang/wasm'
import { Selections__old } from 'lib/selections'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
@ -32,7 +32,7 @@ export function absDistanceInfo({
selectionRanges, selectionRanges,
constraint, constraint,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
constraint: Constraint constraint: Constraint
}): }):
| { | {
@ -93,7 +93,7 @@ export async function applyConstraintAbsDistance({
selectionRanges, selectionRanges,
constraint, constraint,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
constraint: 'xAbs' | 'yAbs' constraint: 'xAbs' | 'yAbs'
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program
@ -157,7 +157,7 @@ export function applyConstraintAxisAlign({
selectionRanges, selectionRanges,
constraint, constraint,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
constraint: 'snapToYAxis' | 'snapToXAxis' constraint: 'snapToYAxis' | 'snapToXAxis'
}): }):
| { | {

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Program, Expr, VariableDeclarator } from '../../lang/wasm' import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
import { Selections__old } from 'lib/selections'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
@ -24,7 +24,7 @@ const getModalInfo = createInfoModal(GetInfoModal)
export function angleBetweenInfo({ export function angleBetweenInfo({
selectionRanges, selectionRanges,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
}): }):
| { | {
transforms: TransformInfo[] transforms: TransformInfo[]
@ -90,7 +90,7 @@ export async function applyConstraintAngleBetween({
selectionRanges, selectionRanges,
}: // constraint, }: // constraint,
{ {
selectionRanges: Selections selectionRanges: Selections__old
// constraint: 'setHorzDistance' | 'setVertDistance' // constraint: 'setHorzDistance' | 'setVertDistance'
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program

View File

@ -16,7 +16,7 @@ import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst' import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { cleanErrs, err } from 'lib/trap' import { cleanErrs, err } from 'lib/trap'
const getModalInfo = createInfoModal(GetInfoModal) const getModalInfo = createInfoModal(GetInfoModal)
@ -25,7 +25,7 @@ export function horzVertDistanceInfo({
selectionRanges, selectionRanges,
constraint, constraint,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
constraint: 'setHorzDistance' | 'setVertDistance' constraint: 'setHorzDistance' | 'setVertDistance'
}): }):
| { | {
@ -95,7 +95,7 @@ export async function applyConstraintHorzVertDistance({
// TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it // TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it
isAlign = false, isAlign = false,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
constraint: 'setHorzDistance' | 'setVertDistance' constraint: 'setHorzDistance' | 'setVertDistance'
isAlign?: false isAlign?: false
}): Promise<{ }): Promise<{
@ -181,7 +181,7 @@ export function applyConstraintHorzVertAlign({
selectionRanges, selectionRanges,
constraint, constraint,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
constraint: 'setHorzDistance' | 'setVertDistance' constraint: 'setHorzDistance' | 'setVertDistance'
}): }):
| { | {

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers' import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Program, Expr } from '../../lang/wasm' import { Program, Expr } from '../../lang/wasm'
import { Selections__old } from 'lib/selections'
import { import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
getNodeFromPath, getNodeFromPath,
@ -32,7 +32,7 @@ export function angleLengthInfo({
selectionRanges, selectionRanges,
angleOrLength = 'setLength', angleOrLength = 'setLength',
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
angleOrLength?: 'setLength' | 'setAngle' angleOrLength?: 'setLength' | 'setAngle'
}): }):
| { | {
@ -74,7 +74,7 @@ export async function applyConstraintAngleLength({
selectionRanges, selectionRanges,
angleOrLength = 'setLength', angleOrLength = 'setLength',
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
angleOrLength?: 'setLength' | 'setAngle' angleOrLength?: 'setLength' | 'setAngle'
}): Promise<{ }): Promise<{
modifiedAst: Program modifiedAst: Program

View File

@ -2,7 +2,11 @@ import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, Annotation, Transaction } from '@codemirror/state' import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine' import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections' import {
Selections__old,
Selection__old,
processCodeMirrorRanges,
} from 'lib/selections'
import { undo, redo } from '@codemirror/commands' import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight, addLineHighlightEvent } from './highlightextension' import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
@ -31,7 +35,7 @@ export default class EditorManager {
private _copilotEnabled: boolean = true private _copilotEnabled: boolean = true
private _isShiftDown: boolean = false private _isShiftDown: boolean = false
private _selectionRanges: Selections = { private _selectionRanges: Selections__old = {
otherSelections: [], otherSelections: [],
codeBasedSelections: [], codeBasedSelections: [],
} }
@ -73,7 +77,7 @@ export default class EditorManager {
this._isShiftDown = isShiftDown this._isShiftDown = isShiftDown
} }
set selectionRanges(selectionRanges: Selections) { set selectionRanges(selectionRanges: Selections__old) {
this._selectionRanges = selectionRanges this._selectionRanges = selectionRanges
} }
@ -97,7 +101,7 @@ export default class EditorManager {
return this._highlightRange return this._highlightRange
} }
setHighlightRange(selections: Array<Selection['range']>): void { setHighlightRange(selections: Array<Selection__old['range']>): void {
this._highlightRange = selections this._highlightRange = selections
const selectionsWithSafeEnds = selections.map((s): [number, number] => { const selectionsWithSafeEnds = selections.map((s): [number, number] => {
@ -203,7 +207,7 @@ export default class EditorManager {
return false return false
} }
selectRange(selections: Selections) { selectRange(selections: Selections__old) {
if (selections.codeBasedSelections.length === 0) { if (selections.codeBasedSelections.length === 0) {
return return
} }

View File

@ -9,10 +9,9 @@ import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections' import { getEventForSelectWithPoint } from 'lib/selections'
import { import {
getCapCodeRef, getCapCodeRef,
getSweepEdgeCodeRef,
getSweepFromSuspectedSweepSurface, getSweepFromSuspectedSweepSurface,
getSolid2dCodeRef,
getWallCodeRef, getWallCodeRef,
getCodeRefsByArtifactId,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { err, reportRejection } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities' import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
@ -29,52 +28,14 @@ export function useEngineConnectionSubscriptions() {
event: 'highlight_set_entity', event: 'highlight_set_entity',
callback: ({ data }) => { callback: ({ data }) => {
if (data?.entity_id) { if (data?.entity_id) {
const artifact = engineCommandManager.artifactGraph.get( const codeRefs = getCodeRefsByArtifactId(
data.entity_id
)
if (artifact?.type === 'solid2D') {
const codeRef = getSolid2dCodeRef(
artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return
editorManager.setHighlightRange([codeRef.range])
} else if (artifact?.type === 'cap') {
const codeRef = getCapCodeRef(
artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return
editorManager.setHighlightRange([codeRef.range])
} else if (artifact?.type === 'wall') {
const extrusion = getSweepFromSuspectedSweepSurface(
data.entity_id, data.entity_id,
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
const codeRef = getWallCodeRef( if (codeRefs) {
artifact, editorManager.setHighlightRange(codeRefs.map(({ range }) => range))
engineCommandManager.artifactGraph
)
if (err(codeRef)) return
editorManager.setHighlightRange(
err(extrusion)
? [codeRef.range]
: [codeRef.range, extrusion.codeRef.range]
)
} else if (artifact?.type === 'sweepEdge') {
const codeRef = getSweepEdgeCodeRef(
artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return
editorManager.setHighlightRange([codeRef.range])
} else if (artifact?.type === 'segment') {
editorManager.setHighlightRange([
artifact?.codeRef?.range || [0, 0],
])
} else {
editorManager.setHighlightRange([[0, 0]])
} }
editorManager.setHighlightRange([[0, 0]])
} else if ( } else if (
!editorManager.highlightRange || !editorManager.highlightRange ||
(editorManager.highlightRange[0][0] !== 0 && (editorManager.highlightRange[0][0] !== 0 &&

View File

@ -11,6 +11,7 @@ import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange } from 'lang/wasm' import { PathToNode, SourceRange } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { convertSelectionsToOld } from 'lib/selections'
export const getVarNameModal = createSetVarNameModal(SetVarNameModal) export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
@ -28,14 +29,19 @@ export function useConvertToVariable(range?: SourceRange) {
const meta = isNodeSafeToReplace( const meta = isNodeSafeToReplace(
parsed, parsed,
range || context.selectionRanges.codeBasedSelections?.[0]?.range || [] range ||
convertSelectionsToOld(context.selectionRanges).codeBasedSelections?.[0]
?.range ||
[]
) )
if (trap(meta)) return if (trap(meta)) return
const { isSafe, value } = meta const { isSafe, value } = meta
const canReplace = isSafe && value.type !== 'Identifier' const canReplace = isSafe && value.type !== 'Identifier'
const isOnlyOneSelection = const isOnlyOneSelection =
!!range || context.selectionRanges.codeBasedSelections.length === 1 !!range ||
convertSelectionsToOld(context.selectionRanges).codeBasedSelections
.length === 1
setEnabled(canReplace && isOnlyOneSelection) setEnabled(canReplace && isOnlyOneSelection)
}, [context.selectionRanges]) }, [context.selectionRanges])
@ -52,7 +58,9 @@ export function useConvertToVariable(range?: SourceRange) {
moveValueIntoNewVariable( moveValueIntoNewVariable(
ast, ast,
kclManager.programMemory, kclManager.programMemory,
range || context.selectionRanges.codeBasedSelections[0].range, range ||
convertSelectionsToOld(context.selectionRanges)
.codeBasedSelections[0].range,
variableName variableName
) )

View File

@ -1,12 +1,13 @@
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import './index.css' import './index.css'
import reportWebVitals from './reportWebVitals' import reportWebVitals from './reportWebVitals'
import { Toaster } from 'react-hot-toast' import toast, { Toaster } from 'react-hot-toast'
import { Router } from './Router' import { Router } from './Router'
import { HotkeysProvider } from 'react-hotkeys-hook' import { HotkeysProvider } from 'react-hotkeys-hook'
import ModalContainer from 'react-modal-promise' import ModalContainer from 'react-modal-promise'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { AppStreamProvider } from 'AppState' import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
@ -52,4 +53,17 @@ root.render(
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals() reportWebVitals()
isDesktop() isDesktop() &&
window.electron.onUpdateDownloaded((version: string) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message)
toast.custom(
ToastUpdate({
version,
onRestart: () => {
window.electron.appRestart()
},
}),
{ duration: 30000 }
)
})

View File

@ -3,8 +3,6 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
const KclContext = createContext({ const KclContext = createContext({
code: codeManager?.code || '', code: codeManager?.code || '',
@ -37,7 +35,6 @@ export function KclContextProvider({
const [errors, setErrors] = useState<KCLError[]>([]) const [errors, setErrors] = useState<KCLError[]>([])
const [logs, setLogs] = useState<string[]>([]) const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false) const [wasmInitFailed, setWasmInitFailed] = useState(false)
const { commandBarSend } = useCommandsContext()
useEffect(() => { useEffect(() => {
codeManager.registerCallBacks({ codeManager.registerCallBacks({
@ -53,28 +50,6 @@ export function KclContextProvider({
}) })
}, []) }, [])
// Add format code to command palette.
useEffect(() => {
const commands: Command[] = [
{
name: 'format-code',
displayName: 'Format Code',
description: 'Nicely formats the KCL code in the editor.',
needsReview: false,
groupId: 'code',
icon: 'code',
onSubmit: (data) => {
kclManager.format()
},
},
]
commandBarSend({ type: 'Add commands', data: { commands } })
return () => {
commandBarSend({ type: 'Remove commands', data: { commands } })
}
}, [kclManager, commandBarSend])
return ( return (
<KclContext.Provider <KclContext.Provider
value={{ value={{

View File

@ -1,5 +1,5 @@
import { executeAst, lintAst } from 'lang/langHelpers' import { executeAst, lintAst } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { KCLError, kclErrorsToDiagnostics } from './errors' import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
@ -425,14 +425,14 @@ export class KclManager {
} }
): Promise<{ ): Promise<{
newAst: Program newAst: Program
selections?: Selections selections?: Selections__old
}> { }> {
const newCode = recast(ast) const newCode = recast(ast)
if (err(newCode)) return Promise.reject(newCode) if (err(newCode)) return Promise.reject(newCode)
const astWithUpdatedSource = this.safeParse(newCode) const astWithUpdatedSource = this.safeParse(newCode)
if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast')) if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast'))
let returnVal: Selections | undefined = undefined let returnVal: Selections__old | undefined = undefined
if (optionalParams?.focusPath) { if (optionalParams?.focusPath) {
returnVal = { returnVal = {

View File

@ -1,5 +1,5 @@
import { Selection } from 'lib/selections'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { Selection__old } from 'lib/selections'
import { import {
Program, Program,
CallExpression, CallExpression,
@ -762,7 +762,7 @@ export function createBinaryExpressionWithUnary([left, right]: [
export function giveSketchFnCallTag( export function giveSketchFnCallTag(
ast: Program, ast: Program,
range: Selection['range'], range: Selection__old['range'],
tag?: string tag?: string
): ):
| { | {
@ -836,7 +836,7 @@ export function moveValueIntoNewVariablePath(
export function moveValueIntoNewVariable( export function moveValueIntoNewVariable(
ast: Program, ast: Program,
programMemory: ProgramMemory, programMemory: ProgramMemory,
sourceRange: Selection['range'], sourceRange: Selection__old['range'],
variableName: string variableName: string
): { ): {
modifiedAst: Program modifiedAst: Program
@ -955,7 +955,7 @@ export function removeSingleConstraintInfo(
export async function deleteFromSelection( export async function deleteFromSelection(
ast: Program, ast: Program,
selection: Selection, selection: Selection__old,
programMemory: ProgramMemory, programMemory: ProgramMemory,
getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () => getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () =>
({} as any) ({} as any)

View File

@ -19,7 +19,7 @@ import {
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst' import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_DEV_TOKEN } from 'env'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
@ -106,7 +106,7 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
code.indexOf(selectedSegmentSnippet), code.indexOf(selectedSegmentSnippet),
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length, code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
] ]
const selection: Selections = { const selection: Selections__old = {
codeBasedSelections: [ codeBasedSelections: [
{ {
range: segmentRange, range: segmentRange,
@ -469,7 +469,7 @@ const runModifyAstWithFilletAndTagTest = async (
code.indexOf(selectionSnippet) + selectionSnippet.length, code.indexOf(selectionSnippet) + selectionSnippet.length,
] ]
) )
const selection: Selections = { const selection: Selections__old = {
codeBasedSelections: segmentRanges.map((segmentRange) => ({ codeBasedSelections: segmentRanges.map((segmentRange) => ({
range: segmentRange, range: segmentRange,
type: 'default', type: 'default',
@ -730,7 +730,7 @@ describe('Testing button states', () => {
] ]
: [ast.end, ast.end] // empty line in the end of the code : [ast.end, ast.end] // empty line in the end of the code
const selectionRanges: Selections = { const selectionRanges: Selections__old = {
codeBasedSelections: [ codeBasedSelections: [
{ {
range, range,

View File

@ -31,7 +31,7 @@ import {
sketchLineHelperMap, sketchLineHelperMap,
} from '../std/sketch' } from '../std/sketch'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections' import { Selections__old, canFilletSelection } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { import {
ArtifactGraph, ArtifactGraph,
@ -45,7 +45,7 @@ import { kclManager, engineCommandManager, editorManager } from 'lib/singletons'
export function applyFilletToSelection( export function applyFilletToSelection(
ast: Program, ast: Program,
selection: Selections, selection: Selections__old,
radius: KclCommandValue radius: KclCommandValue
): void | Error { ): void | Error {
// 1. clone ast // 1. clone ast
@ -63,7 +63,7 @@ export function applyFilletToSelection(
export function modifyAstWithFilletAndTag( export function modifyAstWithFilletAndTag(
ast: Program, ast: Program,
selection: Selections, selection: Selections__old,
radius: KclCommandValue radius: KclCommandValue
): { modifiedAst: Program; pathToFilletNode: Array<PathToNode> } | Error { ): { modifiedAst: Program; pathToFilletNode: Array<PathToNode> } | Error {
const astResult = insertRadiusIntoAst(ast, radius) const astResult = insertRadiusIntoAst(ast, radius)
@ -130,7 +130,7 @@ function insertRadiusIntoAst(
export function getPathToExtrudeForSegmentSelection( export function getPathToExtrudeForSegmentSelection(
ast: Program, ast: Program,
selection: Selections, selection: Selections__old,
programMemory: ProgramMemory, programMemory: ProgramMemory,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error { ): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
@ -447,7 +447,7 @@ export const hasValidFilletSelection = ({
ast, ast,
code, code,
}: { }: {
selectionRanges: Selections selectionRanges: Selections__old
ast: Program ast: Program
code: string code: string
}) => { }) => {

View File

@ -1,5 +1,5 @@
import { ToolTip } from 'lang/langHelpers' import { ToolTip } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections' import { Selection__old, Selections__old } from 'lib/selections'
import { import {
ArrayExpression, ArrayExpression,
BinaryExpression, BinaryExpression,
@ -120,7 +120,7 @@ export function getNodeFromPathCurry(
function moreNodePathFromSourceRange( function moreNodePathFromSourceRange(
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement, node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
sourceRange: Selection['range'], sourceRange: Selection__old['range'],
previousPath: PathToNode = [['body', '']] previousPath: PathToNode = [['body', '']]
): PathToNode { ): PathToNode {
const [start, end] = sourceRange const [start, end] = sourceRange
@ -315,7 +315,7 @@ function moreNodePathFromSourceRange(
export function getNodePathFromSourceRange( export function getNodePathFromSourceRange(
node: Program, node: Program,
sourceRange: Selection['range'], sourceRange: Selection__old['range'],
previousPath: PathToNode = [['body', '']] previousPath: PathToNode = [['body', '']]
): PathToNode { ): PathToNode {
const [start, end] = sourceRange || [] const [start, end] = sourceRange || []
@ -493,7 +493,7 @@ export function findAllPreviousVariablesPath(
export function findAllPreviousVariables( export function findAllPreviousVariables(
ast: Program, ast: Program,
programMemory: ProgramMemory, programMemory: ProgramMemory,
sourceRange: Selection['range'], sourceRange: Selection__old['range'],
type: 'number' | 'string' = 'number' type: 'number' | 'string' = 'number'
): { ): {
variables: PrevVariable<typeof type extends 'number' ? number : string>[] variables: PrevVariable<typeof type extends 'number' ? number : string>[]
@ -639,8 +639,8 @@ export function isValueZero(val?: Expr): boolean {
export function isLinesParallelAndConstrained( export function isLinesParallelAndConstrained(
ast: Program, ast: Program,
programMemory: ProgramMemory, programMemory: ProgramMemory,
primaryLine: Selection, primaryLine: Selection__old,
secondaryLine: Selection secondaryLine: Selection__old
): ):
| { | {
isParallelAndConstrained: boolean isParallelAndConstrained: boolean
@ -735,7 +735,7 @@ export function doesPipeHaveCallExp({
}: { }: {
calleeName: string calleeName: string
ast: Program ast: Program
selection: Selection selection: Selection__old
}): boolean { }): boolean {
const pathToNode = getNodePathFromSourceRange(ast, selection.range) const pathToNode = getNodePathFromSourceRange(ast, selection.range)
const pipeExpressionMeta = getNodeFromPath<PipeExpression>( const pipeExpressionMeta = getNodeFromPath<PipeExpression>(
@ -762,7 +762,7 @@ export function hasExtrudeSketchGroup({
programMemory, programMemory,
}: { }: {
ast: Program ast: Program
selection: Selection selection: Selection__old
programMemory: ProgramMemory programMemory: ProgramMemory
}): boolean { }): boolean {
const pathToNode = getNodePathFromSourceRange(ast, selection.range) const pathToNode = getNodePathFromSourceRange(ast, selection.range)
@ -786,7 +786,7 @@ export function hasExtrudeSketchGroup({
} }
export function isSingleCursorInPipe( export function isSingleCursorInPipe(
selectionRanges: Selections, selectionRanges: Selections__old,
ast: Program ast: Program
) { ) {
if (selectionRanges.codeBasedSelections.length !== 1) return false if (selectionRanges.codeBasedSelections.length !== 1) return false
@ -860,7 +860,10 @@ export function findUsesOfTagInPipe(
return dependentRanges return dependentRanges
} }
export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) { export function hasSketchPipeBeenExtruded(
selection: Selection__old,
ast: Program
) {
const path = getNodePathFromSourceRange(ast, selection.range) const path = getNodePathFromSourceRange(ast, selection.range)
const _node = getNodeFromPath<PipeExpression>(ast, path, 'PipeExpression') const _node = getNodeFromPath<PipeExpression>(ast, path, 'PipeExpression')
if (err(_node)) return false if (err(_node)) return false

View File

@ -5,86 +5,90 @@ import { err } from 'lib/trap'
export type ArtifactId = string export type ArtifactId = string
interface CommonCommandProperties { interface BaseArtifact {
id: ArtifactId
}
export interface CodeRef {
range: SourceRange range: SourceRange
pathToNode: PathToNode pathToNode: PathToNode
} }
export interface PlaneArtifact { export interface PlaneArtifact extends BaseArtifact {
type: 'plane' type: 'plane'
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
codeRef: CommonCommandProperties codeRef: CodeRef
} }
export interface PlaneArtifactRich { export interface PlaneArtifactRich extends BaseArtifact {
type: 'plane' type: 'plane'
paths: Array<PathArtifact> paths: Array<PathArtifact>
codeRef: CommonCommandProperties codeRef: CodeRef
} }
export interface PathArtifact { export interface PathArtifact extends BaseArtifact {
type: 'path' type: 'path'
planeId: ArtifactId planeId: ArtifactId
segIds: Array<ArtifactId> segIds: Array<ArtifactId>
sweepId: ArtifactId sweepId: ArtifactId
solid2dId?: ArtifactId solid2dId?: ArtifactId
codeRef: CommonCommandProperties codeRef: CodeRef
} }
interface solid2D { interface solid2D extends BaseArtifact {
type: 'solid2D' type: 'solid2D'
pathId: ArtifactId pathId: ArtifactId
} }
export interface PathArtifactRich { export interface PathArtifactRich extends BaseArtifact {
type: 'path' type: 'path'
plane: PlaneArtifact | WallArtifact plane: PlaneArtifact | WallArtifact
segments: Array<SegmentArtifact> segments: Array<SegmentArtifact>
sweep: SweepArtifact sweep: SweepArtifact
codeRef: CommonCommandProperties codeRef: CodeRef
} }
interface SegmentArtifact { interface SegmentArtifact extends BaseArtifact {
type: 'segment' type: 'segment'
pathId: ArtifactId pathId: ArtifactId
surfaceId: ArtifactId surfaceId: ArtifactId
edgeIds: Array<ArtifactId> edgeIds: Array<ArtifactId>
edgeCutId?: ArtifactId edgeCutId?: ArtifactId
codeRef: CommonCommandProperties codeRef: CodeRef
} }
interface SegmentArtifactRich { interface SegmentArtifactRich extends BaseArtifact {
type: 'segment' type: 'segment'
path: PathArtifact path: PathArtifact
surf: WallArtifact surf: WallArtifact
edges: Array<SweepEdge> edges: Array<SweepEdge>
edgeCut?: EdgeCut edgeCut?: EdgeCut
codeRef: CommonCommandProperties codeRef: CodeRef
} }
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/ /** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
interface SweepArtifact { interface SweepArtifact extends BaseArtifact {
type: 'sweep' type: 'sweep'
subType: 'extrusion' | 'revolve' subType: 'extrusion' | 'revolve'
pathId: string pathId: string
surfaceIds: Array<string> surfaceIds: Array<string>
edgeIds: Array<string> edgeIds: Array<string>
codeRef: CommonCommandProperties codeRef: CodeRef
} }
interface SweepArtifactRich { interface SweepArtifactRich extends BaseArtifact {
type: 'sweep' type: 'sweep'
subType: 'extrusion' | 'revolve' subType: 'extrusion' | 'revolve'
path: PathArtifact path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact> surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge> edges: Array<SweepEdge>
codeRef: CommonCommandProperties codeRef: CodeRef
} }
interface WallArtifact { interface WallArtifact extends BaseArtifact {
type: 'wall' type: 'wall'
segId: ArtifactId segId: ArtifactId
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId sweepId: ArtifactId
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
} }
interface CapArtifact { interface CapArtifact extends BaseArtifact {
type: 'cap' type: 'cap'
subType: 'start' | 'end' subType: 'start' | 'end'
edgeCutEdgeIds: Array<ArtifactId> edgeCutEdgeIds: Array<ArtifactId>
@ -92,7 +96,7 @@ interface CapArtifact {
pathIds: Array<ArtifactId> pathIds: Array<ArtifactId>
} }
interface SweepEdge { interface SweepEdge extends BaseArtifact {
type: 'sweepEdge' type: 'sweepEdge'
segId: ArtifactId segId: ArtifactId
sweepId: ArtifactId sweepId: ArtifactId
@ -100,16 +104,16 @@ interface SweepEdge {
} }
/** A edgeCut is a more generic term for both fillet or chamfer */ /** A edgeCut is a more generic term for both fillet or chamfer */
interface EdgeCut { interface EdgeCut extends BaseArtifact {
type: 'edgeCut' type: 'edgeCut'
subType: 'fillet' | 'chamfer' subType: 'fillet' | 'chamfer'
consumedEdgeId: ArtifactId consumedEdgeId: ArtifactId
edgeIds: Array<ArtifactId> edgeIds: Array<ArtifactId>
surfaceId: ArtifactId surfaceId: ArtifactId
codeRef: CommonCommandProperties codeRef: CodeRef
} }
interface EdgeCutEdge { interface EdgeCutEdge extends BaseArtifact {
type: 'edgeCutEdge' type: 'edgeCutEdge'
edgeCutId: ArtifactId edgeCutId: ArtifactId
surfaceId: ArtifactId surfaceId: ArtifactId
@ -258,6 +262,7 @@ export function getArtifactsToUpdate({
id: currentPlaneId, id: currentPlaneId,
artifact: { artifact: {
type: 'wall', type: 'wall',
id,
segId: existingPlane.segId, segId: existingPlane.segId,
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds, edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId, sweepId: existingPlane.sweepId,
@ -267,7 +272,10 @@ export function getArtifactsToUpdate({
] ]
} else { } else {
return [ return [
{ id: currentPlaneId, artifact: { type: 'plane', pathIds, codeRef } }, {
id: currentPlaneId,
artifact: { type: 'plane', id: currentPlaneId, pathIds, codeRef },
},
] ]
} }
} else if (cmd.type === 'start_path') { } else if (cmd.type === 'start_path') {
@ -275,6 +283,7 @@ export function getArtifactsToUpdate({
id, id,
artifact: { artifact: {
type: 'path', type: 'path',
id,
segIds: [], segIds: [],
planeId: currentPlaneId, planeId: currentPlaneId,
sweepId: '', sweepId: '',
@ -287,7 +296,7 @@ export function getArtifactsToUpdate({
if (plane?.type === 'plane') { if (plane?.type === 'plane') {
returnArr.push({ returnArr.push({
id: currentPlaneId, id: currentPlaneId,
artifact: { type: 'plane', pathIds: [id], codeRef }, artifact: { type: 'plane', id: currentPlaneId, pathIds: [id], codeRef },
}) })
} }
if (plane?.type === 'wall') { if (plane?.type === 'wall') {
@ -295,6 +304,7 @@ export function getArtifactsToUpdate({
id: currentPlaneId, id: currentPlaneId,
artifact: { artifact: {
type: 'wall', type: 'wall',
id,
segId: plane.segId, segId: plane.segId,
edgeCutEdgeIds: plane.edgeCutEdgeIds, edgeCutEdgeIds: plane.edgeCutEdgeIds,
sweepId: plane.sweepId, sweepId: plane.sweepId,
@ -309,6 +319,7 @@ export function getArtifactsToUpdate({
id, id,
artifact: { artifact: {
type: 'segment', type: 'segment',
id,
pathId, pathId,
surfaceId: '', surfaceId: '',
edgeIds: [], edgeIds: [],
@ -327,7 +338,11 @@ export function getArtifactsToUpdate({
) { ) {
returnArr.push({ returnArr.push({
id: response.data.modeling_response.data.face_id, id: response.data.modeling_response.data.face_id,
artifact: { type: 'solid2D', pathId }, artifact: {
type: 'solid2D',
id: response.data.modeling_response.data.face_id,
pathId,
},
}) })
const path = getArtifact(pathId) const path = getArtifact(pathId)
if (path?.type === 'path') if (path?.type === 'path')
@ -347,6 +362,7 @@ export function getArtifactsToUpdate({
artifact: { artifact: {
type: 'sweep', type: 'sweep',
subType: subType, subType: subType,
id,
pathId: cmd.target, pathId: cmd.target,
surfaceIds: [], surfaceIds: [],
edgeIds: [], edgeIds: [],
@ -378,6 +394,7 @@ export function getArtifactsToUpdate({
id: face_id, id: face_id,
artifact: { artifact: {
type: 'wall', type: 'wall',
id: face_id,
segId: curve_id, segId: curve_id,
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
sweepId: path.sweepId, sweepId: path.sweepId,
@ -410,6 +427,7 @@ export function getArtifactsToUpdate({
id: face_id, id: face_id,
artifact: { artifact: {
type: 'cap', type: 'cap',
id: face_id,
subType: cap === 'bottom' ? 'start' : 'end', subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [], edgeCutEdgeIds: [],
sweepId: path.sweepId, sweepId: path.sweepId,
@ -456,6 +474,7 @@ export function getArtifactsToUpdate({
id: response.data.modeling_response.data.edge, id: response.data.modeling_response.data.edge,
artifact: { artifact: {
type: 'sweepEdge', type: 'sweepEdge',
id: response.data.modeling_response.data.edge,
subType: subType:
cmd.type === 'solid3d_get_prev_adjacent_edge' cmd.type === 'solid3d_get_prev_adjacent_edge'
? 'adjacent' ? 'adjacent'
@ -484,6 +503,7 @@ export function getArtifactsToUpdate({
id, id,
artifact: { artifact: {
type: 'edgeCut', type: 'edgeCut',
id,
subType: cmd.cut_type, subType: cmd.cut_type,
consumedEdgeId: cmd.edge_id, consumedEdgeId: cmd.edge_id,
edgeIds: [], edgeIds: [],
@ -574,6 +594,7 @@ export function expandPlane(
) )
return { return {
type: 'plane', type: 'plane',
id: plane.id,
paths: Array.from(paths.values()), paths: Array.from(paths.values()),
codeRef: plane.codeRef, codeRef: plane.codeRef,
} }
@ -602,6 +623,7 @@ export function expandPath(
if (err(plane)) return plane if (err(plane)) return plane
return { return {
type: 'path', type: 'path',
id: path.id,
segments: Array.from(segs.values()), segments: Array.from(segs.values()),
sweep, sweep,
plane, plane,
@ -629,6 +651,7 @@ export function expandSweep(
return { return {
type: 'sweep', type: 'sweep',
subType: 'extrusion', subType: 'extrusion',
id: sweep.id,
surfaces: Array.from(surfs.values()), surfaces: Array.from(surfs.values()),
edges: Array.from(edges.values()), edges: Array.from(edges.values()),
path, path,
@ -664,6 +687,7 @@ export function expandSegment(
return { return {
type: 'segment', type: 'segment',
id: segment.id,
path, path,
surf, surf,
edges: Array.from(edges.values()), edges: Array.from(edges.values()),
@ -675,7 +699,7 @@ export function expandSegment(
export function getCapCodeRef( export function getCapCodeRef(
cap: CapArtifact, cap: CapArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CommonCommandProperties | Error { ): CodeRef | Error {
const sweep = getArtifactOfTypes( const sweep = getArtifactOfTypes(
{ key: cap.sweepId, types: ['sweep'] }, { key: cap.sweepId, types: ['sweep'] },
artifactGraph artifactGraph
@ -692,7 +716,7 @@ export function getCapCodeRef(
export function getSolid2dCodeRef( export function getSolid2dCodeRef(
solid2D: solid2D, solid2D: solid2D,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CommonCommandProperties | Error { ): CodeRef | Error {
const path = getArtifactOfTypes( const path = getArtifactOfTypes(
{ key: solid2D.pathId, types: ['path'] }, { key: solid2D.pathId, types: ['path'] },
artifactGraph artifactGraph
@ -704,7 +728,7 @@ export function getSolid2dCodeRef(
export function getWallCodeRef( export function getWallCodeRef(
wall: WallArtifact, wall: WallArtifact,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CommonCommandProperties | Error { ): CodeRef | Error {
const seg = getArtifactOfTypes( const seg = getArtifactOfTypes(
{ key: wall.segId, types: ['segment'] }, { key: wall.segId, types: ['segment'] },
artifactGraph artifactGraph
@ -716,7 +740,7 @@ export function getWallCodeRef(
export function getSweepEdgeCodeRef( export function getSweepEdgeCodeRef(
edge: SweepEdge, edge: SweepEdge,
artifactGraph: ArtifactGraph artifactGraph: ArtifactGraph
): CommonCommandProperties | Error { ): CodeRef | Error {
const seg = getArtifactOfTypes( const seg = getArtifactOfTypes(
{ key: edge.segId, types: ['segment'] }, { key: edge.segId, types: ['segment'] },
artifactGraph artifactGraph
@ -751,3 +775,33 @@ export function getSweepFromSuspectedPath(
artifactGraph artifactGraph
) )
} }
export function getCodeRefsByArtifactId(
id: string,
artifactGraph: ArtifactGraph
): Array<CodeRef> | null {
const artifact = artifactGraph.get(id)
if (artifact?.type === 'solid2D') {
const codeRef = getSolid2dCodeRef(artifact, artifactGraph)
if (err(codeRef)) return null
return [codeRef]
// editorManager.setHighlightRange([codeRef.range])
} else if (artifact?.type === 'cap') {
const codeRef = getCapCodeRef(artifact, artifactGraph)
if (err(codeRef)) return null
return [codeRef]
} else if (artifact?.type === 'wall') {
const extrusion = getSweepFromSuspectedSweepSurface(id, artifactGraph)
const codeRef = getWallCodeRef(artifact, artifactGraph)
if (err(codeRef)) return null
return err(extrusion) ? [codeRef] : [codeRef, extrusion.codeRef]
} else if (artifact?.type === 'sweepEdge') {
const codeRef = getSweepEdgeCodeRef(artifact, artifactGraph)
if (err(codeRef)) return null
return [codeRef]
} else if (artifact?.type === 'segment') {
return [artifact.codeRef]
} else {
return null
}
}

View File

@ -1912,11 +1912,13 @@ export function getConstraintInfo(
): ConstrainInfo[] { ): ConstrainInfo[] {
const fnName = callExpression?.callee?.name || '' const fnName = callExpression?.callee?.name || ''
if (!(fnName in sketchLineHelperMap)) return [] if (!(fnName in sketchLineHelperMap)) return []
return sketchLineHelperMap[fnName].getConstraintInfo( const result = sketchLineHelperMap[fnName].getConstraintInfo(
callExpression, callExpression,
code, code,
pathToNode pathToNode
) )
// console.log('result path', result[0].pathToNode)
return result
} }
export function compareVec2Epsilon( export function compareVec2Epsilon(

View File

@ -11,7 +11,7 @@ import {
transformAstSketchLines, transformAstSketchLines,
} from './sketchcombos' } from './sketchcombos'
import { getSketchSegmentFromSourceRange } from './sketchConstraints' import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import { Selection } from 'lib/selections' import { Selection__old } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
import { err } from 'lib/trap' import { err } from 'lib/trap'
@ -33,7 +33,7 @@ async function testingSwapSketchFnCall({
originalRange: [number, number] originalRange: [number, number]
}> { }> {
const startIndex = inputCode.indexOf(callToSwap) const startIndex = inputCode.indexOf(callToSwap)
const range: Selection = { const range: Selection__old = {
type: 'default', type: 'default',
range: [startIndex, startIndex + callToSwap.length], range: [startIndex, startIndex + callToSwap.length],
} }

View File

@ -9,7 +9,7 @@ import {
getConstraintLevelFromSourceRange, getConstraintLevelFromSourceRange,
} from './sketchcombos' } from './sketchcombos'
import { ToolTip } from 'lang/langHelpers' import { ToolTip } from 'lang/langHelpers'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
@ -87,8 +87,8 @@ function getConstraintTypeFromSourceHelper2(
} }
function makeSelections( function makeSelections(
codeBaseSelections: Selections['codeBasedSelections'] codeBaseSelections: Selections__old['codeBasedSelections']
): Selections { ): Selections__old {
return { return {
codeBasedSelections: codeBaseSelections, codeBasedSelections: codeBaseSelections,
otherSelections: [], otherSelections: [],
@ -208,7 +208,7 @@ const part001 = startSketchOn('XY')
const ast = parse(inputScript) const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections__old['codeBasedSelections'] = inputScript
.split('\n') .split('\n')
.filter((ln) => ln.includes('//')) .filter((ln) => ln.includes('//'))
.map((ln) => { .map((ln) => {
@ -299,7 +299,7 @@ const part001 = startSketchOn('XY')
const ast = parse(inputScript) const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections__old['codeBasedSelections'] = inputScript
.split('\n') .split('\n')
.filter((ln) => ln.includes('// select for horizontal constraint')) .filter((ln) => ln.includes('// select for horizontal constraint'))
.map((ln) => { .map((ln) => {
@ -361,7 +361,7 @@ const part001 = startSketchOn('XY')
const ast = parse(inputScript) const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections__old['codeBasedSelections'] = inputScript
.split('\n') .split('\n')
.filter((ln) => ln.includes('// select for vertical constraint')) .filter((ln) => ln.includes('// select for vertical constraint'))
.map((ln) => { .map((ln) => {
@ -434,7 +434,7 @@ const part001 = startSketchOn('XY')
segEndY(seg01) + 2.93 segEndY(seg01) + 2.93
], %) // xRelative`) ], %) // xRelative`)
}) })
it('testing for yRelative to horizontal distance', async () => { it.only('testing for yRelative to horizontal distance', async () => {
const expectedCode = await helperThing( const expectedCode = await helperThing(
inputScript, inputScript,
['// base selection', '// yRelative'], ['// base selection', '// yRelative'],
@ -456,7 +456,7 @@ async function helperThing(
const ast = parse(inputScript) const ast = parse(inputScript)
if (err(ast)) return Promise.reject(ast) if (err(ast)) return Promise.reject(ast)
const selectionRanges: Selections['codeBasedSelections'] = inputScript const selectionRanges: Selections__old['codeBasedSelections'] = inputScript
.split('\n') .split('\n')
.filter((ln) => .filter((ln) =>
linesOfInterest.some((lineOfInterest) => ln.includes(lineOfInterest)) linesOfInterest.some((lineOfInterest) => ln.includes(lineOfInterest))

View File

@ -7,7 +7,7 @@ import {
TransformInfo, TransformInfo,
} from './stdTypes' } from './stdTypes'
import { ToolTip, toolTips } from 'lang/langHelpers' import { ToolTip, toolTips } from 'lang/langHelpers'
import { Selections, Selection } from 'lib/selections' import { Selections__old, Selection__old } from 'lib/selections'
import { cleanErrs, err } from 'lib/trap' import { cleanErrs, err } from 'lib/trap'
import { import {
CallExpression, CallExpression,
@ -1470,7 +1470,7 @@ export function getConstraintType(
} }
export function getTransformInfos( export function getTransformInfos(
selectionRanges: Selections, selectionRanges: Selections__old,
ast: Program, ast: Program,
constraintType: ConstraintType constraintType: ConstraintType
): TransformInfo[] { ): TransformInfo[] {
@ -1502,7 +1502,7 @@ export function getTransformInfos(
} }
export function getRemoveConstraintsTransforms( export function getRemoveConstraintsTransforms(
selectionRanges: Selections, selectionRanges: Selections__old,
ast: Program, ast: Program,
constraintType: ConstraintType constraintType: ConstraintType
): TransformInfo[] | Error { ): TransformInfo[] | Error {
@ -1542,7 +1542,7 @@ export function transformSecondarySketchLinesTagFirst({
forceValueUsedInTransform, forceValueUsedInTransform,
}: { }: {
ast: Program ast: Program
selectionRanges: Selections selectionRanges: Selections__old
transformInfos: TransformInfo[] transformInfos: TransformInfo[]
programMemory: ProgramMemory programMemory: ProgramMemory
forceSegName?: string forceSegName?: string
@ -1613,12 +1613,12 @@ export function transformAstSketchLines({
referencedSegmentRange, referencedSegmentRange,
}: { }: {
ast: Program ast: Program
selectionRanges: Selections | PathToNode[] selectionRanges: Selections__old | PathToNode[]
transformInfos: TransformInfo[] transformInfos: TransformInfo[]
programMemory: ProgramMemory programMemory: ProgramMemory
referenceSegName: string referenceSegName: string
referencedSegmentRange?: Selection__old['range']
forceValueUsedInTransform?: BinaryPart forceValueUsedInTransform?: BinaryPart
referencedSegmentRange?: Selection['range']
}): }):
| { | {
modifiedAst: Program modifiedAst: Program
@ -1658,6 +1658,7 @@ export function transformAstSketchLines({
'' ''
const inputs: InputArgs = [] const inputs: InputArgs = []
console.log('getConstraintInfo', callExp.node, _pathToNode)
getConstraintInfo(callExp.node, '', _pathToNode).forEach((a) => { getConstraintInfo(callExp.node, '', _pathToNode).forEach((a) => {
if ( if (
a.type === 'tangentialWithPrevious' || a.type === 'tangentialWithPrevious' ||
@ -1822,7 +1823,7 @@ function getArgLiteralVal(arg: Literal): number | Error {
export type ConstraintLevel = 'free' | 'partial' | 'full' export type ConstraintLevel = 'free' | 'partial' | 'full'
export function getConstraintLevelFromSourceRange( export function getConstraintLevelFromSourceRange(
cursorRange: Selection['range'], cursorRange: Selection__old['range'],
ast: Program | Error ast: Program | Error
): Error | { range: [number, number]; level: ConstraintLevel } { ): Error | { range: [number, number]; level: ConstraintLevel } {
if (err(ast)) return ast if (err(ast)) return ast

View File

@ -1,4 +1,4 @@
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { Program, PathToNode } from './wasm' import { Program, PathToNode } from './wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph' import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
@ -7,10 +7,10 @@ import { err } from 'lib/trap'
export function pathMapToSelections( export function pathMapToSelections(
ast: Program, ast: Program,
prevSelections: Selections, prevSelections: Selections__old,
pathToNodeMap: { [key: number]: PathToNode } pathToNodeMap: { [key: number]: PathToNode }
): Selections { ): Selections__old {
const newSelections: Selections = { const newSelections: Selections__old = {
...prevSelections, ...prevSelections,
codeBasedSelections: [], codeBasedSelections: [],
} }
@ -47,7 +47,7 @@ export function updatePathToNodeFromMap(
export function isCursorInSketchCommandRange( export function isCursorInSketchCommandRange(
artifactGraph: ArtifactGraph, artifactGraph: ArtifactGraph,
selectionRanges: Selections selectionRanges: Selections__old
): string | false { ): string | false {
const overlappingEntries = filterArtifacts( const overlappingEntries = filterArtifacts(
{ {

View File

@ -2,7 +2,7 @@ import { Models } from '@kittycad/lib'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants' import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api' import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { machineManager } from 'lib/machineManager' import { machineManager } from 'lib/machineManager'
import { modelingMachine, SketchTool } from 'machines/modelingMachine' import { modelingMachine, SketchTool } from 'machines/modelingMachine'
@ -28,17 +28,17 @@ export type ModelingCommandSchema = {
machine: components['schemas']['MachineInfoResponse'] machine: components['schemas']['MachineInfoResponse']
} }
Extrude: { Extrude: {
selection: Selections // & { type: 'face' } would be cool to lock that down selection: Selections__old // & { type: 'face' } would be cool to lock that down
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: KclCommandValue
} }
Revolve: { Revolve: {
selection: Selections selection: Selections__old
angle: KclCommandValue angle: KclCommandValue
} }
Fillet: { Fillet: {
// todo // todo
selection: Selections selection: Selections__old
radius: KclCommandValue radius: KclCommandValue
} }
'change tool': { 'change tool': {

View File

@ -1,9 +1,10 @@
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { AllMachines } from 'hooks/useStateMachineCommands' import { AllMachines } from 'hooks/useStateMachineCommands'
import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate' import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
import { Selection } from './selections' import { Selection__old } from './selections'
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm' import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { ReactNode } from 'react'
type Icon = CustomIconName type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const const PLATFORMS = ['both', 'web', 'desktop'] as const
@ -67,6 +68,12 @@ export type Command<
name: CommandName name: CommandName
groupId: T['id'] groupId: T['id']
needsReview: boolean needsReview: boolean
reviewMessage?:
| string
| ReactNode
| ((
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
) => string | ReactNode)
onSubmit: (data?: CommandSchema) => void onSubmit: (data?: CommandSchema) => void
onCancel?: () => void onCancel?: () => void
args?: { args?: {
@ -133,7 +140,7 @@ export type CommandArgumentConfig<
} }
| { | {
inputType: 'selection' inputType: 'selection'
selectionTypes: Selection['type'][] selectionTypes: Selection__old['type'][]
multiple: boolean multiple: boolean
} }
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values | { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
@ -181,7 +188,7 @@ export type CommandArgument<
machineContext?: ContextFrom<T> machineContext?: ContextFrom<T>
) => boolean) ) => boolean)
skip?: boolean skip?: boolean
machineActor: Actor<T> machineActor?: Actor<T>
/** For showing a summary display of the current value, such as in /** For showing a summary display of the current value, such as in
* the command bar's header * the command bar's header
*/ */
@ -206,7 +213,7 @@ export type CommandArgument<
} }
| { | {
inputType: 'selection' inputType: 'selection'
selectionTypes: Selection['type'][] selectionTypes: Selection__old['type'][]
multiple: boolean multiple: boolean
} }
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value | { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value

View File

@ -95,3 +95,10 @@ export const MAKE_TOAST_MESSAGES = {
ERROR_STARTING_PRINT: 'Error while starting print', ERROR_STARTING_PRINT: 'Error while starting print',
SUCCESS: 'Started print successfully', SUCCESS: 'Started print successfully',
} }
/** The URL for the KCL samples manifest files */
export const KCL_SAMPLES_MANIFEST_URLS = {
remote:
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifst.json',
localFallback: '/kcl-samples-manifest-fallback.json',
} as const

View File

@ -0,0 +1,31 @@
import { KCL_SAMPLES_MANIFEST_URLS } from './constants'
import { isDesktop } from './isDesktop'
export type KclSamplesManifestItem = {
file: string
title: string
description: string
}
export async function getKclSamplesManifest() {
let response = await fetch(KCL_SAMPLES_MANIFEST_URLS.remote)
if (!response.ok) {
console.warn(
'Failed to fetch latest remote KCL samples manifest, falling back to local:',
response.statusText
)
response = await fetch(
(isDesktop() ? '.' : '') + KCL_SAMPLES_MANIFEST_URLS.localFallback
)
if (!response.ok) {
console.error(
'Failed to fetch fallback KCL samples manifest:',
response.statusText
)
return []
}
}
return response.json().then((manifest) => {
return manifest as KclSamplesManifestItem[]
})
}

110
src/lib/kclCommands.ts Normal file
View File

@ -0,0 +1,110 @@
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { Command, CommandArgumentOption } from './commandTypes'
import { kclManager } from './singletons'
import { isDesktop } from './isDesktop'
import { FILE_EXT } from './constants'
interface OnSubmitProps {
sampleName: string
code: string
method: 'overwrite' | 'newFile'
}
export function kclCommands(
onSubmit: (p: OnSubmitProps) => Promise<void>,
providedOptions: CommandArgumentOption<string>[]
): Command[] {
return [
{
name: 'format-code',
displayName: 'Format Code',
description: 'Nicely formats the KCL code in the editor.',
needsReview: false,
groupId: 'code',
icon: 'code',
onSubmit: () => {
kclManager.format()
},
},
{
name: 'open-kcl-example',
displayName: 'Open sample',
description: 'Imports an example KCL program into the editor.',
needsReview: true,
icon: 'code',
reviewMessage: ({ argumentsToSubmit }) =>
argumentsToSubmit.method === 'newFile'
? 'Create a new file with the example code?'
: CommandBarOverwriteWarning({}),
groupId: 'code',
onSubmit(data) {
if (!data?.sample) {
return
}
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
data.sample.replace(FILE_EXT, '')
)}/${encodeURIComponent(data.sample)}`
fetch(sampleCodeUrl)
.then(async (response) => {
if (!response.ok) {
console.error('Failed to fetch sample code:', response.statusText)
return
}
const code = await response.text()
return {
sampleName: data.sample,
code,
method: data.method,
}
})
.then((props) => {
if (props?.code) {
onSubmit(props).catch(reportError)
}
})
.catch(reportError)
},
args: {
method: {
inputType: 'options',
required: true,
skip: true,
defaultValue: isDesktop() ? 'newFile' : 'overwrite',
options() {
return [
{
value: 'overwrite',
name: 'Overwrite current code',
isCurrent: !isDesktop(),
},
...(isDesktop()
? [
{
value: 'newFile',
name: 'Create a new file',
isCurrent: true,
},
]
: []),
]
},
},
sample: {
inputType: 'options',
required: true,
valueSummary(value) {
const MAX_LENGTH = 12
if (typeof value === 'string') {
return value.length > MAX_LENGTH
? value.substring(0, MAX_LENGTH) + '...'
: value
}
return value
},
options: providedOptions,
},
},
},
]
}

View File

@ -0,0 +1,34 @@
[
"80-20-rail",
"a-parametric-bearing-pillow-block",
"ball-bearing",
"bracket",
"brake-caliper",
"car-wheel-assembly",
"car-wheel",
"enclosure",
"flange-with-patterns",
"flange-xy",
"focusrite-scarlett-mounting-bracket",
"french-press",
"gear-rack",
"gear",
"hex-nut",
"kitt",
"lego",
"lug-nut",
"mounting-plate",
"multi-axis-robot",
"pipe-flange-assembly",
"pipe",
"poopy-shoe",
"router-template-cross-bar",
"router-template-slate",
"screenshots",
"sheet-metal-bracket",
"socket-head-cap-screw",
"step",
"tire",
"washer",
"wheel-rotor"
]

View File

@ -28,12 +28,15 @@ import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
import { PathToNodeMap } from 'lang/std/sketchcombos' import { PathToNodeMap } from 'lang/std/sketchcombos'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { import {
Artifact,
getArtifactOfTypes, getArtifactOfTypes,
getArtifactsOfTypes, getArtifactsOfTypes,
getCapCodeRef, getCapCodeRef,
getSweepEdgeCodeRef, getSweepEdgeCodeRef,
getSolid2dCodeRef, getSolid2dCodeRef,
getWallCodeRef, getWallCodeRef,
CodeRef,
getCodeRefsByArtifactId,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b' export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
@ -41,7 +44,8 @@ export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
export type Axis = 'y-axis' | 'x-axis' | 'z-axis' export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
export type Selection = { /** @deprecated Use {@link Artifact} instead. */
export type Selection__old = {
type: type:
| 'default' | 'default'
| 'line-end' | 'line-end'
@ -58,9 +62,129 @@ export type Selection = {
| 'all' | 'all'
range: SourceRange range: SourceRange
} }
export type Selections = { /** @deprecated Use {@link Selection} instead. */
export type Selections__old = {
otherSelections: Axis[] otherSelections: Axis[]
codeBasedSelections: Selection[] codeBasedSelections: Selection__old[]
}
export interface Selection {
artifact: Artifact
codeRef: CodeRef
}
export type Selections = {
otherSelections: Array<Axis>
graphSelections: Array<Selection>
}
/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old}
* this function should only be used for backwards compatibility with old functions.
*/
export function convertSelectionToOld(
selection: Selection
): Selection__old | null {
// return {} as Selection__old
// TODO implementation
const _artifact = selection.artifact
if (_artifact.type === 'solid2D') {
const codeRef = getSolid2dCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return { range: codeRef.range, type: 'solid2D' }
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range: codeRef.range, type: 'solid2D' },
// },
// }
}
if (_artifact.type === 'cap') {
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
if (err(codeRef)) return null
return {
range: codeRef.range,
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
}
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: {
// range: codeRef.range,
// type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
// },
// },
// }
}
if (_artifact.type === 'wall') {
const codeRef = getWallCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return { range: codeRef.range, type: 'extrude-wall' }
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range: codeRef.range, type: 'extrude-wall' },
// },
// }
}
if (_artifact.type === 'segment' || _artifact.type === 'path') {
return { range: _artifact.codeRef.range, type: 'default' }
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range: _artifact.codeRef.range, type: 'default' },
// },
// }
}
if (_artifact.type === 'sweepEdge') {
const codeRef = getSweepEdgeCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
if (_artifact?.subType === 'adjacent') {
return { range: codeRef.range, type: 'adjacent-edge' }
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range: codeRef.range, type: 'adjacent-edge' },
// },
// }
}
return { range: codeRef.range, type: 'edge' }
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range: codeRef.range, type: 'edge' },
// },
// }
}
return null
}
/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old}
* this function should only be used for backwards compatibility with old functions.
*/
export function convertSelectionsToOld(selection: Selections): Selections__old {
const selections: Selection__old[] = []
for (const artifact of selection.graphSelections) {
const converted = convertSelectionToOld(artifact)
if (converted) selections.push(converted)
}
const selectionsOld: Selections__old = {
otherSelections: selection.otherSelections,
codeBasedSelections: selections,
}
return selectionsOld
} }
export async function getEventForSelectWithPoint({ export async function getEventForSelectWithPoint({
@ -85,85 +209,102 @@ export async function getEventForSelectWithPoint({
} }
} }
let _artifact = engineCommandManager.artifactGraph.get(data.entity_id) let _artifact = engineCommandManager.artifactGraph.get(data.entity_id)
if (!_artifact) const codeRefs = getCodeRefsByArtifactId(
return { data.entity_id,
type: 'Set selection',
data: { selectionType: 'singleCodeCursor' },
}
if (_artifact.type === 'solid2D') {
const codeRef = getSolid2dCodeRef(
_artifact,
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (err(codeRef)) return null if (_artifact && codeRefs) {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'solid2D' },
},
}
}
if (_artifact.type === 'cap') {
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
if (err(codeRef)) return null
return { return {
type: 'Set selection', type: 'Set selection',
data: { data: {
selectionType: 'singleCodeCursor', selectionType: 'singleCodeCursor',
selection: { selection: {
range: codeRef.range, artifact: _artifact,
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap', codeRef: codeRefs[0],
}, },
}, },
} }
} }
if (_artifact.type === 'wall') { // if (!_artifact)
const codeRef = getWallCodeRef( // return {
_artifact, // type: 'Set selection',
engineCommandManager.artifactGraph // data: { selectionType: 'singleCodeCursor' },
) // }
if (err(codeRef)) return null // if (_artifact.type === 'solid2D') {
return { // const codeRef = getSolid2dCodeRef(
type: 'Set selection', // _artifact,
data: { // engineCommandManager.artifactGraph
selectionType: 'singleCodeCursor', // )
selection: { range: codeRef.range, type: 'extrude-wall' }, // if (err(codeRef)) return null
}, // return {
} // type: 'Set selection',
} // data: {
if (_artifact.type === 'segment' || _artifact.type === 'path') { // selectionType: 'singleCodeCursor',
return {
type: 'Set selection', // // selection: { range: codeRef.range, type: 'solid2D' },
data: { // },
selectionType: 'singleCodeCursor', // }
selection: { range: _artifact.codeRef.range, type: 'default' }, // }
}, // if (_artifact.type === 'cap') {
} // const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
} // if (err(codeRef)) return null
if (_artifact.type === 'sweepEdge') { // return {
const codeRef = getSweepEdgeCodeRef( // type: 'Set selection',
_artifact, // data: {
engineCommandManager.artifactGraph // selectionType: 'singleCodeCursor',
) // selection: {
if (err(codeRef)) return null // range: codeRef.range,
if (_artifact?.subType === 'adjacent') { // type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
return { // },
type: 'Set selection', // },
data: { // }
selectionType: 'singleCodeCursor', // }
selection: { range: codeRef.range, type: 'adjacent-edge' }, // if (_artifact.type === 'wall') {
}, // const codeRef = getWallCodeRef(
} // _artifact,
} // engineCommandManager.artifactGraph
return { // )
type: 'Set selection', // if (err(codeRef)) return null
data: { // return {
selectionType: 'singleCodeCursor', // type: 'Set selection',
selection: { range: codeRef.range, type: 'edge' }, // data: {
}, // selectionType: 'singleCodeCursor',
} // selection: { range: codeRef.range, type: 'extrude-wall' },
} // },
// }
// }
// if (_artifact.type === 'segment' || _artifact.type === 'path') {
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range: _artifact.codeRef.range, type: 'default' },
// },
// }
// }
// if (_artifact.type === 'sweepEdge') {
// const codeRef = getSweepEdgeCodeRef(
// _artifact,
// engineCommandManager.artifactGraph
// )
// if (err(codeRef)) return null
// if (_artifact?.subType === 'adjacent') {
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range: codeRef.range, type: 'adjacent-edge' },
// },
// }
// }
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range: codeRef.range, type: 'edge' },
// },
// }
// }
return null return null
} }
@ -182,36 +323,56 @@ export function getEventForSegmentSelection(
}, },
} }
} }
const pathToNode = group?.userData?.pathToNode // console.log('group', group?.userData.)
if (!pathToNode) return null const id = group?.userData?.id
// previous drags don't update ast for efficiency reasons if (!id) return null
// So we want to make sure we have and updated ast with const artifact = engineCommandManager.artifactGraph.get(id)
// accurate source ranges const codeRefs = getCodeRefsByArtifactId(
const updatedAst = parse(codeManager.code) id,
if (err(updatedAst)) return null engineCommandManager.artifactGraph
const nodeMeta = getNodeFromPath<CallExpression>(
updatedAst,
pathToNode,
'CallExpression'
) )
if (err(nodeMeta)) return null console.log('artifact', artifact, group.userData)
if (!artifact || !codeRefs) return null
const node = nodeMeta.node
const range: SourceRange = [node.start, node.end]
return { return {
type: 'Set selection', type: 'Set selection',
data: { data: {
selectionType: 'singleCodeCursor', selectionType: 'singleCodeCursor',
selection: { range, type: 'default' }, selection: {
artifact,
codeRef: codeRefs[0],
},
}, },
} }
// const pathToNode = group?.userData?.pathToNode
// if (!pathToNode) return null
// // previous drags don't update ast for efficiency reasons
// // So we want to make sure we have and updated ast with
// // accurate source ranges
// const updatedAst = parse(codeManager.code)
// if (err(updatedAst)) return null
// const nodeMeta = getNodeFromPath<CallExpression>(
// updatedAst,
// pathToNode,
// 'CallExpression'
// )
// if (err(nodeMeta)) return null
// const node = nodeMeta.node
// const range: SourceRange = [node.start, node.end]
// return {
// type: 'Set selection',
// data: {
// selectionType: 'singleCodeCursor',
// selection: { range, type: 'default' },
// },
// }
} }
export function handleSelectionBatch({ export function handleSelectionBatch({
selections, selections,
}: { }: {
selections: Selections selections: Selections__old
}): { }): {
engineEvents: Models['WebSocketRequest_type'][] engineEvents: Models['WebSocketRequest_type'][]
codeMirrorSelection: EditorSelection codeMirrorSelection: EditorSelection
@ -252,7 +413,7 @@ export function handleSelectionBatch({
} }
} }
type SelectionToEngine = { type: Selection['type']; id: string } type SelectionToEngine = { type: Selection__old['type']; id: string }
export function processCodeMirrorRanges({ export function processCodeMirrorRanges({
codeMirrorRanges, codeMirrorRanges,
@ -260,14 +421,15 @@ export function processCodeMirrorRanges({
isShiftDown, isShiftDown,
}: { }: {
codeMirrorRanges: readonly SelectionRange[] codeMirrorRanges: readonly SelectionRange[]
selectionRanges: Selections selectionRanges: Selections__old
isShiftDown: boolean isShiftDown: boolean
}): null | { }): null | {
modelingEvent: ModelingMachineEvent modelingEvent: ModelingMachineEvent
engineEvents: Models['WebSocketRequest_type'][] engineEvents: Models['WebSocketRequest_type'][]
} { } {
const isChange = const isChange =
codeMirrorRanges.length !== selectionRanges.codeBasedSelections.length || // todo should this take old or new selections?
codeMirrorRanges.length !== selectionRanges?.codeBasedSelections?.length ||
codeMirrorRanges.some(({ from, to }, i) => { codeMirrorRanges.some(({ from, to }, i) => {
return ( return (
from !== selectionRanges.codeBasedSelections[i].range[0] || from !== selectionRanges.codeBasedSelections[i].range[0] ||
@ -276,7 +438,7 @@ export function processCodeMirrorRanges({
}) })
if (!isChange) return null if (!isChange) return null
const codeBasedSelections: Selections['codeBasedSelections'] = const codeBasedSelections: Selections__old['codeBasedSelections'] =
codeMirrorRanges.map(({ from, to }) => { codeMirrorRanges.map(({ from, to }) => {
return { return {
type: 'default', type: 'default',
@ -285,6 +447,15 @@ export function processCodeMirrorRanges({
}) })
const idBasedSelections: SelectionToEngine[] = const idBasedSelections: SelectionToEngine[] =
codeToIdSelections(codeBasedSelections) codeToIdSelections(codeBasedSelections)
const artifacts: Selection[] = []
for (const { id } of idBasedSelections) {
const artifact = engineCommandManager.artifactGraph.get(id)
const codeRefs = getCodeRefsByArtifactId(
id,
engineCommandManager.artifactGraph
)
if (artifact && codeRefs) artifacts.push({ artifact, codeRef: codeRefs[0] })
}
if (!selectionRanges) return null if (!selectionRanges) return null
updateSceneObjectColors(codeBasedSelections) updateSceneObjectColors(codeBasedSelections)
@ -295,7 +466,7 @@ export function processCodeMirrorRanges({
selectionType: 'mirrorCodeMirrorSelections', selectionType: 'mirrorCodeMirrorSelections',
selection: { selection: {
otherSelections: isShiftDown ? selectionRanges.otherSelections : [], otherSelections: isShiftDown ? selectionRanges.otherSelections : [],
codeBasedSelections, graphSelections: artifacts,
}, },
}, },
}, },
@ -303,7 +474,7 @@ export function processCodeMirrorRanges({
} }
} }
function updateSceneObjectColors(codeBasedSelections: Selection[]) { function updateSceneObjectColors(codeBasedSelections: Selection__old[]) {
const updated = kclManager.ast const updated = kclManager.ast
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => { Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
@ -358,7 +529,7 @@ function resetAndSetEngineEntitySelectionCmds(
] ]
} }
export function isSketchPipe(selectionRanges: Selections) { export function isSketchPipe(selectionRanges: Selections__old) {
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) return false if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) return false
return isCursorInSketchCommandRange( return isCursorInSketchCommandRange(
engineCommandManager.artifactGraph, engineCommandManager.artifactGraph,
@ -367,14 +538,14 @@ export function isSketchPipe(selectionRanges: Selections) {
} }
export function isSelectionLastLine( export function isSelectionLastLine(
selectionRanges: Selections, selectionRanges: Selections__old,
code: string, code: string,
i = 0 i = 0
) { ) {
return selectionRanges.codeBasedSelections[i].range[1] === code.length return selectionRanges.codeBasedSelections[i].range[1] === code.length
} }
export function isRangeBetweenCharacters(selectionRanges: Selections) { export function isRangeBetweenCharacters(selectionRanges: Selections__old) {
return ( return (
selectionRanges.codeBasedSelections.length === 1 && selectionRanges.codeBasedSelections.length === 1 &&
selectionRanges.codeBasedSelections[0].range[0] === 0 && selectionRanges.codeBasedSelections[0].range[0] === 0 &&
@ -383,11 +554,14 @@ export function isRangeBetweenCharacters(selectionRanges: Selections) {
} }
export type CommonASTNode = { export type CommonASTNode = {
selection: Selection selection: Selection__old
ast: Program ast: Program
} }
function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) { function buildCommonNodeFromSelection(
selectionRanges: Selections__old,
i: number
) {
return { return {
selection: selectionRanges.codeBasedSelections[i], selection: selectionRanges.codeBasedSelections[i],
ast: kclManager.ast, ast: kclManager.ast,
@ -420,7 +594,7 @@ function nodeHasCircle(node: CommonASTNode) {
}) })
} }
export function canSweepSelection(selection: Selections) { export function canSweepSelection(selection: Selections__old) {
const commonNodes = selection.codeBasedSelections.map((_, i) => const commonNodes = selection.codeBasedSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i) buildCommonNodeFromSelection(selection, i)
) )
@ -433,7 +607,7 @@ export function canSweepSelection(selection: Selections) {
) )
} }
export function canFilletSelection(selection: Selections) { export function canFilletSelection(selection: Selections__old) {
const commonNodes = selection.codeBasedSelections.map((_, i) => const commonNodes = selection.codeBasedSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i) buildCommonNodeFromSelection(selection, i)
) // TODO FILLET DUMMY PLACEHOLDER ) // TODO FILLET DUMMY PLACEHOLDER
@ -444,7 +618,7 @@ export function canFilletSelection(selection: Selections) {
) )
} }
function canExtrudeSelectionItem(selection: Selections, i: number) { function canExtrudeSelectionItem(selection: Selections__old, i: number) {
const isolatedSelection = { const isolatedSelection = {
...selection, ...selection,
codeBasedSelections: [selection.codeBasedSelections[i]], codeBasedSelections: [selection.codeBasedSelections[i]],
@ -459,7 +633,7 @@ function canExtrudeSelectionItem(selection: Selections, i: number) {
} }
// This accounts for non-geometry selections under "other" // This accounts for non-geometry selections under "other"
export type ResolvedSelectionType = [Selection['type'] | 'other', number] export type ResolvedSelectionType = [Selection__old['type'] | 'other', number]
/** /**
* In the future, I'd like this function to properly return the type of each selected entity based on * In the future, I'd like this function to properly return the type of each selected entity based on
@ -469,8 +643,9 @@ export type ResolvedSelectionType = [Selection['type'] | 'other', number]
* @returns * @returns
*/ */
export function getSelectionType( export function getSelectionType(
selection: Selections selection?: Selections__old
): ResolvedSelectionType[] { ): ResolvedSelectionType[] {
if (!selection) return []
const extrudableCount = selection.codeBasedSelections.filter((_, i) => { const extrudableCount = selection.codeBasedSelections.filter((_, i) => {
const singleSelection = { const singleSelection = {
...selection, ...selection,
@ -485,7 +660,7 @@ export function getSelectionType(
} }
export function getSelectionTypeDisplayText( export function getSelectionTypeDisplayText(
selection: Selections selection?: Selections__old
): string | null { ): string | null {
const selectionsByType = getSelectionType(selection) const selectionsByType = getSelectionType(selection)
@ -517,7 +692,7 @@ export function canSubmitSelectionArg(
} }
function codeToIdSelections( function codeToIdSelections(
codeBasedSelections: Selection[] codeBasedSelections: Selection__old[]
): SelectionToEngine[] { ): SelectionToEngine[] {
return codeBasedSelections return codeBasedSelections
.flatMap(({ type, range, ...rest }): null | SelectionToEngine[] => { .flatMap(({ type, range, ...rest }): null | SelectionToEngine[] => {
@ -683,13 +858,13 @@ export async function sendSelectEventToEngine(
export function updateSelections( export function updateSelections(
pathToNodeMap: PathToNodeMap, pathToNodeMap: PathToNodeMap,
prevSelectionRanges: Selections, prevSelectionRanges: Selections__old,
ast: Program | Error ast: Program | Error
): Selections | Error { ): Selections__old | Error {
if (err(ast)) return ast if (err(ast)) return ast
const newSelections = Object.entries(pathToNodeMap) const newSelections = Object.entries(pathToNodeMap)
.map(([index, pathToNode]): Selection | undefined => { .map(([index, pathToNode]): Selection__old | undefined => {
const nodeMeta = getNodeFromPath<Expr>(ast, pathToNode) const nodeMeta = getNodeFromPath<Expr>(ast, pathToNode)
if (err(nodeMeta)) return undefined if (err(nodeMeta)) return undefined
const node = nodeMeta.node const node = nodeMeta.node
@ -698,7 +873,7 @@ export function updateSelections(
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type, type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,
} }
}) })
.filter((x?: Selection) => x !== undefined) as Selection[] .filter((x?: Selection__old) => x !== undefined) as Selection__old[]
return { return {
codeBasedSelections: codeBasedSelections:
@ -708,3 +883,74 @@ export function updateSelections(
otherSelections: prevSelectionRanges.otherSelections, otherSelections: prevSelectionRanges.otherSelections,
} }
} }
// using artifact as the selection is maybe not such a good idea.
// is the artifact stable, once you add a constrain, there will a new artifact graph
// then the ids will not match up
export function updateSelections2(
pathToNodeMap: PathToNodeMap,
prevSelectionRanges: Selections,
ast: Program | Error
): Selections | Error {
if (err(ast)) return ast
const newSelections = Object.entries(pathToNodeMap)
.map(([index, pathToNode]): Selection | undefined => {
const previousSelection =
prevSelectionRanges.graphSelections[Number(index)]
const nodeMeta = getNodeFromPath<Expr>(ast, pathToNode)
if (err(nodeMeta)) return undefined
const node = nodeMeta.node
let artifact: Artifact | null = null
for (const [id, a] of engineCommandManager.artifactGraph) {
if (previousSelection.artifact.type === a.type) {
const codeRefs = getCodeRefsByArtifactId(
id,
engineCommandManager.artifactGraph
)
console.log('codeRef', codeRefs)
if (!codeRefs) continue
if (
JSON.stringify(codeRefs[0].pathToNode) ===
JSON.stringify(pathToNode)
) {
artifact = a
console.log('found artifact', a)
break
}
}
// if (
// a.codeRef.range[0] === node.start &&
// a.codeRef.range[1] === node.end
// ) {
// artifact = a
// break
// }
}
if (!artifact) return undefined
return {
artifact: artifact,
codeRef: {
range: [node.start, node.end],
pathToNode: pathToNode,
},
// codeRef: {
// range: [node.start, node.end],
// pathToNode: pathToNode,
// },
}
// return {
// range: [node.start, node.end],
// type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,
// }
})
.filter((x?: Selection) => x !== undefined) as Selection[]
return {
graphSelections:
newSelections.length > 0
? newSelections
: prevSelectionRanges.graphSelections,
otherSelections: prevSelectionRanges.otherSelections,
}
}

View File

@ -7,6 +7,7 @@ import { ProgramMemory, Expr, parse } from 'lang/wasm'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'lang/langHelpers'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { convertSelectionsToOld } from './selections'
const isValidVariableName = (name: string) => const isValidVariableName = (name: string) =>
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
@ -34,9 +35,10 @@ export function useCalculateKclExpression({
} { } {
const { programMemory, code } = useKclContext() const { programMemory, code } = useKclContext()
const { context } = useModelingContext() const { context } = useModelingContext()
const selectionOld = convertSelectionsToOld(context.selectionRanges)
const selectionRange: const selectionRange:
| (typeof context.selectionRanges.codeBasedSelections)[number]['range'] | (typeof selectionOld.codeBasedSelections)[number]['range']
| undefined = context.selectionRanges.codeBasedSelections[0]?.range | undefined = selectionOld.codeBasedSelections[0]?.range
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const [availableVarInfo, setAvailableVarInfo] = useState< const [availableVarInfo, setAvailableVarInfo] = useState<
ReturnType<typeof findAllPreviousVariables> ReturnType<typeof findAllPreviousVariables>

View File

@ -3,12 +3,13 @@ import { kclManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { findAllPreviousVariables } from 'lang/queryAst' import { findAllPreviousVariables } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { convertSelectionsToOld } from './selections'
export function usePreviousVariables() { export function usePreviousVariables() {
const { programMemory, code } = useKclContext() const { programMemory, code } = useKclContext()
const { context } = useModelingContext() const { context } = useModelingContext()
const selectionRange = context.selectionRanges.codeBasedSelections[0] const selectionRange = convertSelectionsToOld(context.selectionRanges)
?.range || [code.length, code.length] .codeBasedSelections[0]?.range || [code.length, code.length]
const [previousVariablesInfo, setPreviousVariablesInfo] = useState< const [previousVariablesInfo, setPreviousVariablesInfo] = useState<
ReturnType<typeof findAllPreviousVariables> ReturnType<typeof findAllPreviousVariables>
>({ >({

View File

@ -5,14 +5,14 @@ import {
CommandArgumentWithName, CommandArgumentWithName,
KclCommandValue, KclCommandValue,
} from 'lib/commandTypes' } from 'lib/commandTypes'
import { Selections } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils' import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
export type CommandBarContext = { export type CommandBarContext = {
commands: Command[] commands: Command[]
selectedCommand?: Command selectedCommand?: Command
currentArgument?: CommandArgument<unknown> & { name: string } currentArgument?: CommandArgument<unknown> & { name: string }
selectionRanges: Selections selectionRanges: Selections__old
argumentsToSubmit: { [x: string]: unknown } argumentsToSubmit: { [x: string]: unknown }
} }
@ -283,7 +283,7 @@ export const commandBarMachine = setup({
typeof argConfig.options === 'function' typeof argConfig.options === 'function'
? argConfig.options( ? argConfig.options(
input, input,
argConfig.machineActor.getSnapshot().context argConfig.machineActor?.getSnapshot().context
) )
: argConfig.options : argConfig.options
).some((o) => o.value === argValue) ).some((o) => o.value === argValue)

View File

@ -20,6 +20,7 @@ type FileMachineEvents =
makeDir: boolean makeDir: boolean
content?: string content?: string
silent?: boolean silent?: boolean
shouldSetToRename?: boolean
} }
} }
| { type: 'Delete file'; data: FileEntry } | { type: 'Delete file'; data: FileEntry }
@ -42,6 +43,7 @@ type FileMachineEvents =
output: { output: {
message: string message: string
path: string path: string
shouldSetToRename: boolean
} }
} }
| { | {
@ -107,6 +109,10 @@ export const fileMachine = setup({
}, },
'Is not silent': ({ event }) => 'Is not silent': ({ event }) =>
event.type === 'Create file' ? !event.data.silent : false, event.type === 'Create file' ? !event.data.silent : false,
'Should set to rename': ({ event }) =>
(event.type === 'xstate.done.actor.create-and-open-file' &&
event.output.shouldSetToRename) ||
false,
}, },
actors: { actors: {
readFiles: fromPromise(({ input }: { input: Project }) => readFiles: fromPromise(({ input }: { input: Project }) =>
@ -119,6 +125,7 @@ export const fileMachine = setup({
makeDir: boolean makeDir: boolean
selectedDirectory: FileEntry selectedDirectory: FileEntry
content: string content: string
shouldSetToRename: boolean
} }
}) => Promise.resolve({ message: '', path: '' }) }) => Promise.resolve({ message: '', path: '' })
), ),
@ -149,7 +156,7 @@ export const fileMachine = setup({
), ),
}, },
}).createMachine({ }).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iD6BgGQxGY1Wikm8gkQM0n2UknERm0qmUw2hOVhTkKJURBxq9TAjXOrW0-k42JueIQBKJw1GujJFOiqkkTE0X3M5mUuiYgxmbPseU5CPRhwAImBMGiDpcxcFumF8dptMp1GZRlqNYYdXo-ml1IlzMkHuZVAZJAY1PrtnCuftkQB5DjHE02wLi3EOqX6QmDWWkiZ-HSKf3gph6ZWqcwJIZRjm7bkmmoAZTAvCwsAtYAITQgWAgqG83a4njkqeuGbu+MkLPU7x+iiGszJfwj4ldTvB6UURh0klrht2-MaZCgWDwpF7XCTpBPJooEEEhTIADcuABrQoEVFgAC054gP5XscP7WpiVzpvak5SnO6hJD8RYfJ8ir4kw06zqoTDiMyTA4WGPz7js8JHvwpCnv+WBATepF3mAnieMO6gcOgjTuMOODqF+ApNH+F6AdeIEXGBto4pBoj4u8GjOiMqjiKMqhJFhfyVqqEbJIoCTkmk3wETG6huCcOC3gc96PuoL7voU3gGb+oGimmdq3GJCARuY8RMroEk6MMihKeWrpYepEZMKMSw6Ua+mnEZOQULR9GeIxzG8KxnjsVZpw2YJdnjqJ4QjK59KaoGLKhh6fyBpIsFgtqKjKuCYW7OalpRZgJnwuZH7qBAnbcbZWIOZKeXqAVyhFT8EbaOYK44f6kjlhYG6FVYNibOyB7wo1rbNZQsUMUxLFsZ13UZRiWUQY5uUakNskjdOY2lZSCAqhV24LlhHpMl89Xwm4eD9tRvKtU+pCvh1DQAbyY5nZKMwqZWwxqMorzltoZUrK6YbOlJoazIoX2FD9f2ngDD5tcDFnqGDAmYLADAin1InndMKrqD85jw8ySPvH8pgulqoYWEjc16HjekCoTjYxXRu2JclqVi1TcCQ-1mYwyzcMRhz6lcw9C56Cz4Yatd05ISLxFbYDZlkx1nGCgrSsM5KTJVgMMmjKkEYmAYfwrOkQ30i8WFSF8mTLTCa2FGb-3RTt8V7UlB02z1mX0xKmZMgu8R6C8YahqYwUow9TqBkNxXiLmUgGEyIsRYZUctSTQMg5ZxzpXbdPgcrUEuW57xYUyXkGD5D2Bhog9aKsLyzQywsbOUXXwAEYeEWAKcTk5P7KH8G+ujhuHDDJTJZ0t2QGsvxrlI2q85fiBhlgMZcQq8+iqDJ3OzAML2qCCqxDEkIsNryK+jMpSV1clIck3xB6ViLIWEwmhXiJF0EYIqptUS3nIpRLaQDHajAqvKCwqxEZTxkIXVChJNTqUDCkB4L9q4t1rkTHI2DMyRAeosIawxFxDESMoLCIsNokUYZgZhUF1IDGdK7LyWgX6TULqCIagYcKSHMAya6VdQ6rTPgTLaC9hKpygnSOY8FA7kj0J6WR0QISzn0J8IYN0tIi0TMcLBHcHZp1wf6cB5UiFZxIdEcEhJKyvQ9BqGSqCuIuL0WvXoHj8HeKSL472E0KrBRfrVRGL9cbWEsEAA */ /** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iF0ij08SGklUpikym0qkm8gkJie1KYklB70UBkkTChNk2OVhTkKJURBxq9TAjXOrW0-k42JueIQfQMAyGIzGq0UNOiqg5mi+5nMlM+gxm0N5eX5CPRhwAImBMGiDpcZcFumF8dptMp1GZRpT9YZOXo-ml1IlzMkHuZVOyDGpTfZzbtBVaagB5DjHK1OwKy3FuhX6JWDYajXTqzUSRKh8FMPTa1TmBJDePbOEC-bIgDKYF4WFgdrABCaECwEFQ3iHXE8cmz1zzd3xkmUSveP0UJJWyT+sfE3o94PSbK0KxbfN2osaZCgWDwpBHXAzpCvVooEEEhTIADcuABrQoEVEwAAWlvCAgIfY4gMdTErlzV0FwVH5dx3ZdtUSA9lD+B4qW9EkmE5ZkEnMAxT0TeEL34Uhr1ArAIKfKiXzfeEv1-f9AJAu9wMfKCLilLEXVuURpmUcw91JakDAsLQRiwplFHUIxlDeUxZlGRRSJ2cjUWfGi6OfA4KDATxPCndQOHQRp3CnHB1AAsUmg4sC6J4jFpRzAT5SpHDPRGalRlUJJxF+WkEAbJgFOUZJCQ+NJvg0tt1DcE4cH0nJX3fdQWL-dRvGS4DoLcud4KEhBY1EhJ3iCqkdGGRQ-jJDQmCCwlYyYUYlnii0ktOVLMHS5jSG-bLctOfLeMKuDBPCMr4i8qrqW+SS-nDDRJK0VYXmZcRwU63ZupShiDKMkzPDMizeCszwbJGs4XLAWdJvlEZRMkcQDXDVDY20cxltWdRXjZXQzDUSQdu5GEyMKW17V6ygmI-QbWPUCABwcgr+JxYrpv1dRXvepcfi+n6QoMfDQ2ZALzH3d6rHBs1NKh1HYcM4zTPMyzrOR1GxtcjG5XzZ7cbekSCejP0-g5SR-skprVD9Kkvl2+E3DwMdDuReHMsR4axTA4UHo8-MZnCn5iNjOXXjrbRlpWb12U9Hzo1mdS6YTBnEt12Gak1rLCgaPXqgYPjYMNhDjYUhthjUJTCXeP5TC9SlowsS260JJXChVtXr2FFmTrOjmrpy3W7tgA3Mam6YdVNqOLdj62QvXIkY31YWl0+dZXdbC0KOZn3tbY+yefumDnQrzyGyJNQ3teJTYxMAwsNmIkNpeIKpC+TIu7PLT7OZ462fOy6bLs8U7vL-mEKpdd4j0F52WjUw2ob6IPXDXHUPEYspAMKlrG5coKN4ABAhgzPm84SpAUwiFKBuF8L7iZGoEEPwM6WnKCmcBWN8Skynl-CErx9CqGpPHWYAw2TKRmBySSkhUHJmFJgyuiFvqaAmItN45gdB1RCngzQrxEi6CMB9VBvcGK6UfLDBhnlRhSzLBYVYSktoyBCg8UGTwlJ6HDCkB4RDUH7QkSHce+ZIghUWLjYYeFf4qCCqg6GPZ9Fj0viVQkAxPR+RqloIhxNX6glxuGfCkgOGCKTroz26tMDAIcRA8IkU5hIXXhqDRjYvHTFrOoakd98JlQjNoVB6Zjj2PcoYq+0jQxSDkUuJId8lHRHBCuUYTU-T6mpMI7SYSwCSPzN9JIpTkjhgqYorC30pZtSIdqWMbwAr-0sEAA */
id: 'File machine', id: 'File machine',
initial: 'Reading files', initial: 'Reading files',
@ -222,15 +229,41 @@ export const fileMachine = setup({
makeDir: false, makeDir: false,
selectedDirectory: context.selectedDirectory, selectedDirectory: context.selectedDirectory,
content: '', content: '',
shouldSetToRename: false,
} }
return { return {
name: event.data.name, name: event.data.name,
makeDir: event.data.makeDir, makeDir: event.data.makeDir,
selectedDirectory: context.selectedDirectory, selectedDirectory: context.selectedDirectory,
content: event.data.content ?? '', content: event.data.content ?? '',
shouldSetToRename: event.data.shouldSetToRename ?? false,
} }
}, },
onDone: [ onDone: [
{
target: 'Reading files',
actions: [
{
type: 'createToastSuccess',
params: ({
event,
}: {
// TODO: rely on type inference
event: Extract<
FileMachineEvents,
{ type: 'xstate.done.actor.create-and-open-file' }
>
}) => {
return { message: event.output.message }
},
},
'addFileToRenamingQueue',
'navigateToFile',
],
guard: 'Should set to rename',
},
{ {
target: 'Reading files', target: 'Reading files',
actions: [ actions: [
@ -248,7 +281,6 @@ export const fileMachine = setup({
return { message: event.output.message } return { message: event.output.message }
}, },
}, },
'addFileToRenamingQueue',
'navigateToFile', 'navigateToFile',
], ],
}, },

View File

@ -5,7 +5,14 @@ import {
parse, parse,
recast, recast,
} from 'lang/wasm' } from 'lang/wasm'
import { Axis, Selection, Selections, updateSelections } from 'lib/selections' import {
Axis,
convertSelectionsToOld,
convertSelectionToOld,
Selections,
Selection,
updateSelections,
} from 'lib/selections'
import { assign, fromPromise, setup } from 'xstate' import { assign, fromPromise, setup } from 'xstate'
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes' import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
import { import {
@ -299,7 +306,7 @@ export const modelingMachineDefaultContext: ModelingMachineContext = {
selection: [], selection: [],
selectionRanges: { selectionRanges: {
otherSelections: [], otherSelections: [],
codeBasedSelections: [], graphSelections: [],
}, },
sketchDetails: { sketchDetails: {
sketchPathToNode: [], sketchPathToNode: [],
@ -350,18 +357,24 @@ export const modelingMachine = setup({
'is editing existing sketch': ({ context: { sketchDetails } }) => 'is editing existing sketch': ({ context: { sketchDetails } }) =>
isEditingExistingSketch({ sketchDetails }), isEditingExistingSketch({ sketchDetails }),
'Can make selection horizontal': ({ context: { selectionRanges } }) => { 'Can make selection horizontal': ({ context: { selectionRanges } }) => {
const info = horzVertInfo(selectionRanges, 'horizontal') const info = horzVertInfo(
convertSelectionsToOld(selectionRanges),
'horizontal'
)
if (trap(info)) return false if (trap(info)) return false
return info.enabled return info.enabled
}, },
'Can make selection vertical': ({ context: { selectionRanges } }) => { 'Can make selection vertical': ({ context: { selectionRanges } }) => {
const info = horzVertInfo(selectionRanges, 'vertical') const info = horzVertInfo(
convertSelectionsToOld(selectionRanges),
'vertical'
)
if (trap(info)) return false if (trap(info)) return false
return info.enabled return info.enabled
}, },
'Can constrain horizontal distance': ({ context: { selectionRanges } }) => { 'Can constrain horizontal distance': ({ context: { selectionRanges } }) => {
const info = horzVertDistanceInfo({ const info = horzVertDistanceInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
}) })
if (trap(info)) return false if (trap(info)) return false
@ -369,47 +382,59 @@ export const modelingMachine = setup({
}, },
'Can constrain vertical distance': ({ context: { selectionRanges } }) => { 'Can constrain vertical distance': ({ context: { selectionRanges } }) => {
const info = horzVertDistanceInfo({ const info = horzVertDistanceInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'setVertDistance', constraint: 'setVertDistance',
}) })
if (trap(info)) return false if (trap(info)) return false
return info.enabled return info.enabled
}, },
'Can constrain ABS X': ({ context: { selectionRanges } }) => { 'Can constrain ABS X': ({ context: { selectionRanges } }) => {
const info = absDistanceInfo({ selectionRanges, constraint: 'xAbs' }) const info = absDistanceInfo({
selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'xAbs',
})
if (trap(info)) return false if (trap(info)) return false
return info.enabled return info.enabled
}, },
'Can constrain ABS Y': ({ context: { selectionRanges } }) => { 'Can constrain ABS Y': ({ context: { selectionRanges } }) => {
const info = absDistanceInfo({ selectionRanges, constraint: 'yAbs' }) const info = absDistanceInfo({
selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'yAbs',
})
if (trap(info)) return false if (trap(info)) return false
return info.enabled return info.enabled
}, },
'Can constrain angle': ({ context: { selectionRanges } }) => { 'Can constrain angle': ({ context: { selectionRanges } }) => {
const angleBetween = angleBetweenInfo({ selectionRanges }) const angleBetween = angleBetweenInfo({
selectionRanges: convertSelectionsToOld(selectionRanges),
})
if (trap(angleBetween)) return false if (trap(angleBetween)) return false
const angleLength = angleLengthInfo({ const angleLength = angleLengthInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
angleOrLength: 'setAngle', angleOrLength: 'setAngle',
}) })
if (trap(angleLength)) return false if (trap(angleLength)) return false
return angleBetween.enabled || angleLength.enabled return angleBetween.enabled || angleLength.enabled
}, },
'Can constrain length': ({ context: { selectionRanges } }) => { 'Can constrain length': ({ context: { selectionRanges } }) => {
const angleLength = angleLengthInfo({ selectionRanges }) const angleLength = angleLengthInfo({
selectionRanges: convertSelectionsToOld(selectionRanges),
})
if (trap(angleLength)) return false if (trap(angleLength)) return false
return angleLength.enabled return angleLength.enabled
}, },
'Can constrain perpendicular distance': ({ 'Can constrain perpendicular distance': ({
context: { selectionRanges }, context: { selectionRanges },
}) => { }) => {
const info = intersectInfo({ selectionRanges }) const info = intersectInfo({
selectionRanges: convertSelectionsToOld(selectionRanges),
})
if (trap(info)) return false if (trap(info)) return false
return info.enabled return info.enabled
}, },
'Can constrain horizontally align': ({ context: { selectionRanges } }) => { 'Can constrain horizontally align': ({ context: { selectionRanges } }) => {
const info = horzVertDistanceInfo({ const info = horzVertDistanceInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
}) })
if (trap(info)) return false if (trap(info)) return false
@ -417,7 +442,7 @@ export const modelingMachine = setup({
}, },
'Can constrain vertically align': ({ context: { selectionRanges } }) => { 'Can constrain vertically align': ({ context: { selectionRanges } }) => {
const info = horzVertDistanceInfo({ const info = horzVertDistanceInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
}) })
if (trap(info)) return false if (trap(info)) return false
@ -425,7 +450,7 @@ export const modelingMachine = setup({
}, },
'Can constrain snap to X': ({ context: { selectionRanges } }) => { 'Can constrain snap to X': ({ context: { selectionRanges } }) => {
const info = absDistanceInfo({ const info = absDistanceInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'snapToXAxis', constraint: 'snapToXAxis',
}) })
if (trap(info)) return false if (trap(info)) return false
@ -433,19 +458,23 @@ export const modelingMachine = setup({
}, },
'Can constrain snap to Y': ({ context: { selectionRanges } }) => { 'Can constrain snap to Y': ({ context: { selectionRanges } }) => {
const info = absDistanceInfo({ const info = absDistanceInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'snapToYAxis', constraint: 'snapToYAxis',
}) })
if (trap(info)) return false if (trap(info)) return false
return info.enabled return info.enabled
}, },
'Can constrain equal length': ({ context: { selectionRanges } }) => { 'Can constrain equal length': ({ context: { selectionRanges } }) => {
const info = setEqualLengthInfo({ selectionRanges }) const info = setEqualLengthInfo({
selectionRanges: convertSelectionsToOld(selectionRanges),
})
if (trap(info)) return false if (trap(info)) return false
return info.enabled return info.enabled
}, },
'Can canstrain parallel': ({ context: { selectionRanges } }) => { 'Can canstrain parallel': ({ context: { selectionRanges } }) => {
const info = equalAngleInfo({ selectionRanges }) const info = equalAngleInfo({
selectionRanges: convertSelectionsToOld(selectionRanges),
})
if (err(info)) return false if (err(info)) return false
return info.enabled return info.enabled
}, },
@ -455,7 +484,7 @@ export const modelingMachine = setup({
}) => { }) => {
if (event.type !== 'Constrain remove constraints') return false if (event.type !== 'Constrain remove constraints') return false
const info = removeConstrainingValuesInfo({ const info = removeConstrainingValuesInfo({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
pathToNodes: event.data && [event.data], pathToNodes: event.data && [event.data],
}) })
if (trap(info)) return false if (trap(info)) return false
@ -636,10 +665,14 @@ export const modelingMachine = setup({
'AST delete selection': ({ context: { selectionRanges } }) => { 'AST delete selection': ({ context: { selectionRanges } }) => {
;(async () => { ;(async () => {
let ast = kclManager.ast let ast = kclManager.ast
const oldSelection = convertSelectionToOld(
selectionRanges.graphSelections[0]
)
if (!oldSelection) return
const modifiedAst = await deleteFromSelection( const modifiedAst = await deleteFromSelection(
ast, ast,
selectionRanges.codeBasedSelections[0], oldSelection,
kclManager.programMemory, kclManager.programMemory,
getFaceDetails getFaceDetails
) )
@ -709,7 +742,7 @@ export const modelingMachine = setup({
up: sketchDetails.yAxis, up: sketchDetails.yAxis,
position: sketchDetails.origin, position: sketchDetails.origin,
maybeModdedAst: kclManager.ast, maybeModdedAst: kclManager.ast,
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
}) })
sceneInfra.resetMouseListeners() sceneInfra.resetMouseListeners()
sceneEntitiesManager.setupSketchIdleCallbacks({ sceneEntitiesManager.setupSketchIdleCallbacks({
@ -939,7 +972,7 @@ export const modelingMachine = setup({
> & { data?: PathToNode } > & { data?: PathToNode }
}) => { }) => {
const constraint = applyRemoveConstrainingValues({ const constraint = applyRemoveConstrainingValues({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
pathToNodes: data && [data], pathToNodes: data && [data],
}) })
if (trap(constraint)) return if (trap(constraint)) return
@ -958,7 +991,7 @@ export const modelingMachine = setup({
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
), ),
} }
@ -971,7 +1004,7 @@ export const modelingMachine = setup({
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'> input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => { }) => {
const constraint = applyConstraintHorzVert( const constraint = applyConstraintHorzVert(
selectionRanges, convertSelectionsToOld(selectionRanges),
'horizontal', 'horizontal',
kclManager.ast, kclManager.ast,
kclManager.programMemory kclManager.programMemory
@ -992,7 +1025,7 @@ export const modelingMachine = setup({
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
), ),
} }
@ -1005,7 +1038,7 @@ export const modelingMachine = setup({
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'> input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => { }) => {
const constraint = applyConstraintHorzVert( const constraint = applyConstraintHorzVert(
selectionRanges, convertSelectionsToOld(selectionRanges),
'vertical', 'vertical',
kclManager.ast, kclManager.ast,
kclManager.programMemory kclManager.programMemory
@ -1026,7 +1059,7 @@ export const modelingMachine = setup({
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
), ),
} }
@ -1039,7 +1072,7 @@ export const modelingMachine = setup({
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'> input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => { }) => {
const constraint = applyConstraintHorzVertAlign({ const constraint = applyConstraintHorzVertAlign({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'setVertDistance', constraint: 'setVertDistance',
}) })
if (trap(constraint)) return if (trap(constraint)) return
@ -1056,7 +1089,7 @@ export const modelingMachine = setup({
if (!updatedAst) return if (!updatedAst) return
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
) )
return { return {
@ -1072,7 +1105,7 @@ export const modelingMachine = setup({
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'> input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => { }) => {
const constraint = applyConstraintHorzVertAlign({ const constraint = applyConstraintHorzVertAlign({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'setHorzDistance', constraint: 'setHorzDistance',
}) })
if (trap(constraint)) return if (trap(constraint)) return
@ -1089,7 +1122,7 @@ export const modelingMachine = setup({
if (!updatedAst) return if (!updatedAst) return
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
) )
return { return {
@ -1105,7 +1138,7 @@ export const modelingMachine = setup({
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'> input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => { }) => {
const constraint = applyConstraintAxisAlign({ const constraint = applyConstraintAxisAlign({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'snapToXAxis', constraint: 'snapToXAxis',
}) })
if (err(constraint)) return false if (err(constraint)) return false
@ -1122,7 +1155,7 @@ export const modelingMachine = setup({
if (!updatedAst) return if (!updatedAst) return
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
) )
return { return {
@ -1138,7 +1171,7 @@ export const modelingMachine = setup({
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'> input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => { }) => {
const constraint = applyConstraintAxisAlign({ const constraint = applyConstraintAxisAlign({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
constraint: 'snapToYAxis', constraint: 'snapToYAxis',
}) })
if (trap(constraint)) return false if (trap(constraint)) return false
@ -1155,7 +1188,7 @@ export const modelingMachine = setup({
if (!updatedAst) return if (!updatedAst) return
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
) )
return { return {
@ -1171,7 +1204,7 @@ export const modelingMachine = setup({
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'> input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => { }) => {
const constraint = applyConstraintEqualAngle({ const constraint = applyConstraintEqualAngle({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
}) })
if (trap(constraint)) return false if (trap(constraint)) return false
const { modifiedAst, pathToNodeMap } = constraint const { modifiedAst, pathToNodeMap } = constraint
@ -1192,7 +1225,7 @@ export const modelingMachine = setup({
if (!updatedAst) return if (!updatedAst) return
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
) )
return { return {
@ -1208,7 +1241,7 @@ export const modelingMachine = setup({
input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'> input: Pick<ModelingMachineContext, 'selectionRanges' | 'sketchDetails'>
}) => { }) => {
const constraint = applyConstraintEqualLength({ const constraint = applyConstraintEqualLength({
selectionRanges, selectionRanges: convertSelectionsToOld(selectionRanges),
}) })
if (trap(constraint)) return false if (trap(constraint)) return false
const { modifiedAst, pathToNodeMap } = constraint const { modifiedAst, pathToNodeMap } = constraint
@ -1224,7 +1257,7 @@ export const modelingMachine = setup({
if (!updatedAst) return if (!updatedAst) return
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, convertSelectionsToOld(selectionRanges),
updatedAst.newAst updatedAst.newAst
) )
return { return {

View File

@ -251,18 +251,14 @@ export function getAutoUpdater(): AppUpdater {
return autoUpdater return autoUpdater
} }
export async function checkForUpdates(autoUpdater: AppUpdater) {
// TODO: figure out how to get the update modal back
const result = await autoUpdater.checkForUpdatesAndNotify()
console.log(result)
}
app.on('ready', () => { app.on('ready', () => {
const autoUpdater = getAutoUpdater() const autoUpdater = getAutoUpdater()
checkForUpdates(autoUpdater).catch(reportRejection) setTimeout(() => {
autoUpdater.checkForUpdates().catch(reportRejection)
}, 1000)
const fifteenMinutes = 15 * 60 * 1000 const fifteenMinutes = 15 * 60 * 1000
setInterval(() => { setInterval(() => {
checkForUpdates(autoUpdater).catch(reportRejection) autoUpdater.checkForUpdates().catch(reportRejection)
}, fifteenMinutes) }, fifteenMinutes)
autoUpdater.on('update-available', (info) => { autoUpdater.on('update-available', (info) => {
@ -271,6 +267,11 @@ app.on('ready', () => {
autoUpdater.on('update-downloaded', (info) => { autoUpdater.on('update-downloaded', (info) => {
console.log('update-downloaded', info) console.log('update-downloaded', info)
mainWindow?.webContents.send('update-downloaded', info.version)
})
ipcMain.handle('app.restart', () => {
autoUpdater.quitAndInstall()
}) })
}) })

View File

@ -15,6 +15,9 @@ const startDeviceFlow = (host: string): Promise<string> =>
ipcRenderer.invoke('startDeviceFlow', host) ipcRenderer.invoke('startDeviceFlow', host)
const loginWithDeviceFlow = (): Promise<string> => const loginWithDeviceFlow = (): Promise<string> =>
ipcRenderer.invoke('loginWithDeviceFlow') ipcRenderer.invoke('loginWithDeviceFlow')
const onUpdateDownloaded = (callback: (value: string) => void) =>
ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const appRestart = () => ipcRenderer.invoke('app.restart')
const isMac = os.platform() === 'darwin' const isMac = os.platform() === 'darwin'
const isWindows = os.platform() === 'win32' const isWindows = os.platform() === 'win32'
@ -123,4 +126,6 @@ contextBridge.exposeInMainWorld('electron', {
kittycad, kittycad,
listMachines, listMachines,
getMachineApiIp, getMachineApiIp,
onUpdateDownloaded,
appRestart,
}) })

View File

@ -388,9 +388,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.17" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -398,9 +398,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.17" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@ -408,9 +408,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.13" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -1400,7 +1400,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.17" version = "0.2.18"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -3079,18 +3079,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.63" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.63" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -16,7 +16,7 @@ async-recursion = "1.1.1"
async-trait = "0.1.82" async-trait = "0.1.82"
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.17", default-features = false, optional = true, features = ["std", "derive"] } clap = { version = "4.5.18", default-features = false, optional = true, features = ["std", "derive"] }
convert_case = "0.6.0" convert_case = "0.6.0"
dashmap = "6.1.0" dashmap = "6.1.0"
databake = { version = "0.1.8", features = ["derive"] } databake = { version = "0.1.8", features = ["derive"] }
@ -42,7 +42,7 @@ serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128" serde_json = "1.0.128"
sha2 = "0.10.8" sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true } tabled = { version = "0.15.0", optional = true }
thiserror = "1.0.63" thiserror = "1.0.64"
toml = "0.8.19" toml = "0.8.19"
ts-rs = { version = "10.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] } 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"] } url = { version = "2.5.2", features = ["serde"] }

View File

@ -520,7 +520,7 @@ pub fn get_autocomplete_snippet_from_schema(
let mut fn_docs = String::new(); let mut fn_docs = String::new();
fn_docs.push_str("{\n"); fn_docs.push_str("{\n");
// Let's print out the object's properties. // Let's print out the object's properties.
let mut i = 0; let mut i = index;
for (prop_name, prop) in obj_val.properties.iter() { for (prop_name, prop) in obj_val.properties.iter() {
if prop_name.starts_with('_') { if prop_name.starts_with('_') {
continue; continue;
@ -532,15 +532,15 @@ pub fn get_autocomplete_snippet_from_schema(
continue; continue;
} }
if let Some((_, snippet)) = get_autocomplete_snippet_from_schema(prop, index + i)? { if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(prop, i)? {
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet)); fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
i += 1; i = new_index + 1;
} }
} }
fn_docs.push('}'); fn_docs.push('}');
return Ok(Some((index + i - 1, fn_docs))); return Ok(Some((i - 1, fn_docs)));
} }
if let Some(array_val) = &o.array { if let Some(array_val) = &o.array {
@ -589,6 +589,7 @@ pub fn get_autocomplete_snippet_from_schema(
if let Some(subschemas) = &o.subschemas { if let Some(subschemas) = &o.subschemas {
let mut fn_docs = String::new(); let mut fn_docs = String::new();
let mut i = index;
if let Some(items) = &subschemas.one_of { if let Some(items) = &subschemas.one_of {
let mut had_enum_string = false; let mut had_enum_string = false;
let mut parsed_enum_values: Vec<String> = Vec::new(); let mut parsed_enum_values: Vec<String> = Vec::new();
@ -620,13 +621,15 @@ pub fn get_autocomplete_snippet_from_schema(
if had_enum_string && !parsed_enum_values.is_empty() { if had_enum_string && !parsed_enum_values.is_empty() {
return Ok(Some((index, parsed_enum_values[0].to_string()))); return Ok(Some((index, parsed_enum_values[0].to_string())));
} else if let Some(item) = items.iter().next() { } else if let Some(item) = items.iter().next() {
if let Some((_, snippet)) = get_autocomplete_snippet_from_schema(item, index)? { if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(item, index)? {
i = new_index + 1;
fn_docs.push_str(&snippet); fn_docs.push_str(&snippet);
} }
} }
} else if let Some(items) = &subschemas.any_of { } else if let Some(items) = &subschemas.any_of {
if let Some(item) = items.iter().next() { if let Some(item) = items.iter().next() {
if let Some((_, snippet)) = get_autocomplete_snippet_from_schema(item, index)? { if let Some((new_index, snippet)) = get_autocomplete_snippet_from_schema(item, index)? {
i = new_index + 1;
fn_docs.push_str(&snippet); fn_docs.push_str(&snippet);
} }
} }
@ -634,7 +637,7 @@ pub fn get_autocomplete_snippet_from_schema(
anyhow::bail!("unknown subschemas: {:#?}", subschemas); anyhow::bail!("unknown subschemas: {:#?}", subschemas);
} }
return Ok(Some((index, fn_docs))); return Ok(Some((i - 1, fn_docs)));
} }
if let Some(schemars::schema::SingleOrVec::Single(single)) = &o.instance_type { if let Some(schemars::schema::SingleOrVec::Single(single)) = &o.instance_type {
@ -915,10 +918,10 @@ mod tests {
r#"patternCircular3d({ r#"patternCircular3d({
arcDegrees: ${0:3.14}, arcDegrees: ${0:3.14},
axis: [${1:3.14}, ${2:3.14}, ${3:3.14}], axis: [${1:3.14}, ${2:3.14}, ${3:3.14}],
center: [${2:3.14}, ${3:3.14}, ${4:3.14}], center: [${4:3.14}, ${5:3.14}, ${6:3.14}],
repetitions: ${3:10}, repetitions: ${7:10},
rotateDuplicates: ${4:false}, rotateDuplicates: ${8:false},
}, ${5:%})${}"# }, ${9:%})${}"#
); );
} }
@ -942,8 +945,36 @@ mod tests {
snippet, snippet,
r#"circle({ r#"circle({
center: [${0:3.14}, ${1:3.14}], center: [${0:3.14}, ${1:3.14}],
radius: ${1:3.14}, radius: ${2:3.14},
}, ${2:%})${}"# }, ${3:%})${}"#
);
}
#[test]
fn get_autocomplete_snippet_arc() {
let arc_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::Arc);
let snippet = arc_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"arc({
angleEnd: ${0:3.14},
angleStart: ${1:3.14},
radius: ${2:3.14},
}, ${3:%})${}"#
);
}
#[test]
fn get_autocomplete_snippet_pattern_linear_2d() {
let pattern_fn: Box<dyn StdLibFn> = Box::new(crate::std::patterns::PatternLinear2D);
let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"patternLinear2d({
axis: [${0:3.14}, ${1:3.14}],
distance: ${2:3.14},
repetitions: ${3:10},
}, ${4:%})${}"#
); );
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 134 KiB