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 }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
await u.waitForAuthSkipAppStart()
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
listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null>
onUpdateDownloaded: (
callback: (value: string) => void
) => Electron.IpcRenderer
appRestart: () => void
}
declare global {

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.25.3",
"version": "0.25.4",
"private": true,
"productName": "Zoo Modeling App",
"author": {
@ -83,6 +83,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"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)",
"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",
@ -90,7 +91,7 @@
"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",
"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)\"",
"make:dev": "make dev",
"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'
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { convertSelectionsToOld } from 'lib/selections'
export function Toolbar({
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'
const sketchPathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
if (
!isSingleCursorInPipe(
convertSelectionsToOld(context.selectionRanges),
kclManager.ast
)
) {
return false
}
return isCursorInSketchCommandRange(
engineCommandManager.artifactGraph,
context.selectionRanges
convertSelectionsToOld(context.selectionRanges)
)
}, [engineCommandManager.artifactGraph, context.selectionRanges])

View File

@ -77,7 +77,7 @@ import {
createPipeSubstitution,
findUniqueName,
} from 'lang/modifyAst'
import { Selections, getEventForSegmentSelection } from 'lib/selections'
import { Selections__old, getEventForSegmentSelection } from 'lib/selections'
import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib'
import { uuidv4 } from 'lib/utils'
@ -374,7 +374,7 @@ export class SceneEntities {
forward: [number, number, number]
up: [number, number, number]
position?: [number, number, number]
selectionRanges?: Selections
selectionRanges?: Selections__old
}): Promise<{
truncatedAst: Program
programMemoryOverride: ProgramMemory
@ -1171,6 +1171,7 @@ export class SceneEntities {
},
onMove: () => {},
onClick: (args) => {
console.log('onClick', args)
if (args?.mouseEvent.which !== 1) return
if (!args || !args.selected) {
sceneInfra.modelingSend({
@ -1183,6 +1184,7 @@ export class SceneEntities {
}
const { selected } = args
const event = getEventForSegmentSelection(selected)
console.log('event', event)
if (!event) return
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 ${
props.iconStart
? props.iconEnd
? 'px-0'
: 'pr-2'
? 'px-0' // No padding if both icons are present
: 'pr-2' // Padding on the right if only the start icon is present
: props.iconEnd
? 'px-2'
: 'pl-2'
? 'pl-2' // Padding on the left if only the end icon is present
: 'px-2' // Padding on both sides if no icons are present
} ${props.className ? props.className : ''}`
switch (props.Element) {

View File

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

View File

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

View File

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

View File

@ -58,7 +58,17 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
return (
<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
id="review-form"
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 { CommandArgument } from 'lib/commandTypes'
import {
Selection,
Selection__old,
canSubmitSelectionArg,
convertSelectionToOld,
convertSelectionsToOld,
getSelectionType,
getSelectionTypeDisplayText,
} from 'lib/selections'
@ -12,13 +14,15 @@ import { modelingMachine } from 'machines/modelingMachine'
import { useEffect, useMemo, useRef, useState } from 'react'
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'],
edge: ['edge', 'line', 'arc'],
point: ['point', 'line-end', 'line-mid'],
}
function getSemanticSelectionType(selectionType: Array<Selection['type']>) {
function getSemanticSelectionType(
selectionType: Array<Selection__old['type']>
) {
const semanticSelectionType = new Set()
selectionType.forEach((type) => {
Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => {
@ -31,8 +35,8 @@ function getSemanticSelectionType(selectionType: Array<Selection['type']>) {
return Array.from(semanticSelectionType)
}
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
snapshot.context.selectionRanges
const selectionSelector = (snapshot?: StateFrom<typeof modelingMachine>) =>
snapshot?.context.selectionRanges
function CommandBarSelectionInput({
arg,
@ -49,10 +53,14 @@ function CommandBarSelectionInput({
const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector)
const selectionsByType = useMemo(() => {
const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
return !selectionRangeEnd || selectionRangeEnd === code.length
const selectionRangeEnd = !selection
? null
: convertSelectionsToOld(selection)?.codeBasedSelections[0]?.range[1]
return !selectionRangeEnd || selectionRangeEnd === code.length || !selection
? 'none'
: getSelectionType(selection)
: !selection
? 'none'
: getSelectionType(convertSelectionsToOld(selection))
}, [selection, code])
const canSubmitSelection = useMemo<boolean>(
() => canSubmitSelectionArg(selectionsByType, arg),
@ -87,6 +95,8 @@ function CommandBarSelectionInput({
onSubmit(selection)
}
const selectionOld = selection && convertSelectionsToOld(selection)
return (
<form id="arg-form" onSubmit={handleSubmit}>
<label
@ -96,7 +106,7 @@ function CommandBarSelectionInput({
}
>
{canSubmitSelection
? getSelectionTypeDisplayText(selection) + ' selected'
? getSelectionTypeDisplayText(selectionOld) + ' selected'
: `Please select ${
arg.multiple ? 'one or more ' : 'one '
}${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 [containsFilter, setContainsFilter] = useState('')
const [customCmd, setCustomCmd] = useState('')
console.log(JSON.stringify(engineCommands, null, 2))
return (
<div>
<input
@ -64,7 +65,10 @@ export const EngineCommands = () => {
)
})}
</div>
<button data-testid="clear-commands" onClick={clearEngineCommands}>
<button
data-testid="clear-commands"
onClick={() => clearEngineCommands()}
>
Clear
</button>
<br />

View File

@ -2,7 +2,7 @@ import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import React, { createContext } from 'react'
import React, { createContext, useEffect, useMemo } from 'react'
import { toast } from 'react-hot-toast'
import {
Actor,
@ -22,6 +22,12 @@ import {
} from 'lib/constants'
import { getProjectInfo } from 'lib/desktop'
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> = {
state: StateFrom<T>
@ -41,6 +47,16 @@ export const FileMachineProvider = ({
const navigate = useNavigate()
const { commandBarSend } = useCommandsContext()
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(
fileMachine.provide({
@ -121,6 +137,7 @@ export const FileMachineProvider = ({
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
shouldSetToRename: input.shouldSetToRename,
}
}),
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 (
<FileContext.Provider
value={{

View File

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

View File

@ -37,13 +37,17 @@ import {
} from './Toolbar/SetAngleBetween'
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import {
Selections,
Selections__old,
canSweepSelection,
handleSelectionBatch,
isSelectionLastLine,
isRangeBetweenCharacters,
isSketchPipe,
updateSelections,
convertSelectionsToOld,
convertSelectionToOld,
Selections,
updateSelections2,
} from 'lib/selections'
import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
@ -249,6 +253,7 @@ export const ModelingMachineProvider = ({
'Set selection': assign(
({ context: { selectionRanges, sketchDetails }, event }) => {
// this was needed for ts after adding 'Set selection' action to on done modal events
// const oldSelections = convertSelectionsToOld(selectionRanges)
const setSelections =
('data' in event &&
event.data &&
@ -275,8 +280,12 @@ export const ModelingMachineProvider = ({
})
})
}
// let selections: Selections__old = {
// codeBasedSelections: [],
// otherSelections: [],
// }
let selections: Selections = {
codeBasedSelections: [],
graphSelections: [],
otherSelections: [],
}
if (setSelections.selectionType === 'singleCodeCursor') {
@ -286,21 +295,28 @@ export const ModelingMachineProvider = ({
!editorManager.isShiftDown
) {
selections = {
codeBasedSelections: [],
graphSelections: [],
otherSelections: [],
}
} else if (
setSelections.selection &&
!editorManager.isShiftDown
) {
// const oldSelection = convertSelectionToOld(setSelections.selection)
// if (oldSelection) {
// }
selections = {
codeBasedSelections: [setSelections.selection],
graphSelections: [setSelections.selection],
otherSelections: [],
}
} else if (setSelections.selection && editorManager.isShiftDown) {
// const oldSelection = convertSelectionToOld(setSelections.selection)
// if (oldSelection) {
// }
selections = {
codeBasedSelections: [
...selectionRanges.codeBasedSelections,
graphSelections: [
...selectionRanges.graphSelections,
setSelections.selection,
],
otherSelections: selectionRanges.otherSelections,
@ -312,7 +328,7 @@ export const ModelingMachineProvider = ({
codeMirrorSelection,
updateSceneObjectColors,
} = handleSelectionBatch({
selections,
selections: convertSelectionsToOld(selections),
})
codeMirrorSelection && dispatchSelection(codeMirrorSelection)
engineEvents &&
@ -336,18 +352,18 @@ export const ModelingMachineProvider = ({
if (setSelections.selectionType === 'otherSelection') {
if (editorManager.isShiftDown) {
selections = {
codeBasedSelections: selectionRanges.codeBasedSelections,
graphSelections: selectionRanges.graphSelections,
otherSelections: [setSelections.selection],
}
} else {
selections = {
codeBasedSelections: [],
graphSelections: [],
otherSelections: [setSelections.selection],
}
}
const { engineEvents, updateSceneObjectColors } =
handleSelectionBatch({
selections,
selections: convertSelectionsToOld(selections),
})
engineEvents &&
engineEvents.forEach((event) => {
@ -360,7 +376,9 @@ export const ModelingMachineProvider = ({
}
}
if (setSelections.selectionType === 'completeSelection') {
editorManager.selectRange(setSelections.selection)
editorManager.selectRange(
convertSelectionsToOld(setSelections.selection)
)
if (!sketchDetails)
return {
selectionRanges: setSelections.selection,
@ -494,10 +512,11 @@ export const ModelingMachineProvider = ({
'has valid sweep selection': ({ context: { selectionRanges } }) => {
// 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
const _selections = convertSelectionsToOld(selectionRanges)
const hasNoSelection =
selectionRanges.codeBasedSelections.length === 0 ||
isRangeBetweenCharacters(selectionRanges) ||
isSelectionLastLine(selectionRanges, codeManager.code)
_selections.codeBasedSelections.length === 0 ||
isRangeBetweenCharacters(_selections) ||
isSelectionLastLine(_selections, codeManager.code)
if (hasNoSelection) {
// they have no selection, we should enable the button
@ -505,31 +524,34 @@ export const ModelingMachineProvider = ({
// BUT only if there's extrudable geometry
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': ({
context: { selectionRanges },
}) => {
const _selections = convertSelectionsToOld(selectionRanges)
if (!commandBarState.matches('Closed')) return false
if (selectionRanges.codeBasedSelections.length <= 0) return false
if (_selections.codeBasedSelections.length <= 0) return false
return true
},
'has valid fillet selection': ({ context: { selectionRanges } }) =>
hasValidFilletSelection({
selectionRanges,
'has valid fillet selection': ({ context: { selectionRanges } }) => {
const _selections = convertSelectionsToOld(selectionRanges)
return hasValidFilletSelection({
selectionRanges: _selections,
ast: kclManager.ast,
code: codeManager.code,
}),
})
},
'Selection is on face': ({ context: { selectionRanges }, event }) => {
if (event.type !== 'Enter sketch') return false
if (event.data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
return false
const _selections = convertSelectionsToOld(selectionRanges)
if (!isSingleCursorInPipe(_selections, kclManager.ast)) return false
return !!isCursorInSketchCommandRange(
engineCommandManager.artifactGraph,
selectionRanges
_selections
)
},
'Has exportable geometry': () => {
@ -619,7 +641,8 @@ export const ModelingMachineProvider = ({
}),
'animate-to-sketch': fromPromise(
async ({ input: { selectionRanges } }) => {
const sourceRange = selectionRanges.codeBasedSelections[0].range
const _selections = convertSelectionsToOld(selectionRanges)
const sourceRange = _selections.codeBasedSelections[0].range
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
sourceRange
@ -643,10 +666,11 @@ export const ModelingMachineProvider = ({
),
'Get horizontal info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const _selections = convertSelectionsToOld(selectionRanges)
const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance',
selectionRanges,
selectionRanges: _selections,
})
const _modifiedAst = parse(recast(modifiedAst))
if (!sketchDetails)
@ -664,7 +688,8 @@ export const ModelingMachineProvider = ({
sketchDetails.origin
)
if (err(updatedAst)) return Promise.reject(updatedAst)
const selection = updateSelections(
// const selection = updateSelections(
const selection = updateSelections2(
pathToNodeMap,
selectionRanges,
updatedAst.newAst
@ -682,7 +707,7 @@ export const ModelingMachineProvider = ({
const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({
constraint: 'setVertDistance',
selectionRanges,
selectionRanges: convertSelectionsToOld(selectionRanges),
})
const _modifiedAst = parse(recast(modifiedAst))
if (!sketchDetails)
@ -702,7 +727,7 @@ export const ModelingMachineProvider = ({
if (err(updatedAst)) return Promise.reject(updatedAst)
const selection = updateSelections(
pathToNodeMap,
selectionRanges,
convertSelectionsToOld(selectionRanges),
updatedAst.newAst
)
if (err(selection)) return Promise.reject(selection)
@ -716,15 +741,15 @@ export const ModelingMachineProvider = ({
'Get angle info': fromPromise(
async ({ input: { selectionRanges, sketchDetails } }) => {
const info = angleBetweenInfo({
selectionRanges,
selectionRanges: convertSelectionsToOld(selectionRanges),
})
if (err(info)) return Promise.reject(info)
const { modifiedAst, pathToNodeMap } = await (info.enabled
? applyConstraintAngleBetween({
selectionRanges,
selectionRanges: convertSelectionsToOld(selectionRanges),
})
: applyConstraintAngleLength({
selectionRanges,
selectionRanges: convertSelectionsToOld(selectionRanges),
angleOrLength: 'setAngle',
}))
const _modifiedAst = parse(recast(modifiedAst))
@ -747,7 +772,7 @@ export const ModelingMachineProvider = ({
if (err(updatedAst)) return Promise.reject(updatedAst)
const selection = updateSelections(
pathToNodeMap,
selectionRanges,
convertSelectionsToOld(selectionRanges),
updatedAst.newAst
)
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 { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { reportRejection } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
const { commandBarSend } = useCommandsContext()
return (
<Menu>
@ -77,6 +79,22 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
</small>
</a>
</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>
<a
className={styles.button}
@ -85,7 +103,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()}
>
<span>KCL samples</span>
<span>View all samples</span>
<small>
zoo.dev
<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 { Selections } from 'lib/selections'
import { Selections__old } from 'lib/selections'
import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
import {
getNodePathFromSourceRange,
@ -18,7 +18,7 @@ import { TransformInfo } from 'lang/std/stdTypes'
export function equalAngleInfo({
selectionRanges,
}: {
selectionRanges: Selections
selectionRanges: Selections__old
}):
| {
transforms: TransformInfo[]
@ -82,7 +82,7 @@ export function equalAngleInfo({
export function applyConstraintEqualAngle({
selectionRanges,
}: {
selectionRanges: Selections
selectionRanges: Selections__old
}):
| {
modifiedAst: Program

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
import ReactDOM from 'react-dom/client'
import './index.css'
import reportWebVitals from './reportWebVitals'
import { Toaster } from 'react-hot-toast'
import toast, { Toaster } from 'react-hot-toast'
import { Router } from './Router'
import { HotkeysProvider } from 'react-hotkeys-hook'
import ModalContainer from 'react-modal-promise'
import { isDesktop } from 'lib/isDesktop'
import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate'
// uncomment for xstate inspector
// import { DEV } from 'env'
@ -52,4 +53,17 @@ root.render(
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
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 { useLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes'
const KclContext = createContext({
code: codeManager?.code || '',
@ -37,7 +35,6 @@ export function KclContextProvider({
const [errors, setErrors] = useState<KCLError[]>([])
const [logs, setLogs] = useState<string[]>([])
const [wasmInitFailed, setWasmInitFailed] = useState(false)
const { commandBarSend } = useCommandsContext()
useEffect(() => {
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 (
<KclContext.Provider
value={{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import {
TransformInfo,
} from './stdTypes'
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 {
CallExpression,
@ -1470,7 +1470,7 @@ export function getConstraintType(
}
export function getTransformInfos(
selectionRanges: Selections,
selectionRanges: Selections__old,
ast: Program,
constraintType: ConstraintType
): TransformInfo[] {
@ -1502,7 +1502,7 @@ export function getTransformInfos(
}
export function getRemoveConstraintsTransforms(
selectionRanges: Selections,
selectionRanges: Selections__old,
ast: Program,
constraintType: ConstraintType
): TransformInfo[] | Error {
@ -1542,7 +1542,7 @@ export function transformSecondarySketchLinesTagFirst({
forceValueUsedInTransform,
}: {
ast: Program
selectionRanges: Selections
selectionRanges: Selections__old
transformInfos: TransformInfo[]
programMemory: ProgramMemory
forceSegName?: string
@ -1613,12 +1613,12 @@ export function transformAstSketchLines({
referencedSegmentRange,
}: {
ast: Program
selectionRanges: Selections | PathToNode[]
selectionRanges: Selections__old | PathToNode[]
transformInfos: TransformInfo[]
programMemory: ProgramMemory
referenceSegName: string
referencedSegmentRange?: Selection__old['range']
forceValueUsedInTransform?: BinaryPart
referencedSegmentRange?: Selection['range']
}):
| {
modifiedAst: Program
@ -1658,6 +1658,7 @@ export function transformAstSketchLines({
''
const inputs: InputArgs = []
console.log('getConstraintInfo', callExp.node, _pathToNode)
getConstraintInfo(callExp.node, '', _pathToNode).forEach((a) => {
if (
a.type === 'tangentialWithPrevious' ||
@ -1822,7 +1823,7 @@ function getArgLiteralVal(arg: Literal): number | Error {
export type ConstraintLevel = 'free' | 'partial' | 'full'
export function getConstraintLevelFromSourceRange(
cursorRange: Selection['range'],
cursorRange: Selection__old['range'],
ast: Program | Error
): Error | { range: [number, number]; level: ConstraintLevel } {
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 { getNodeFromPath } from './queryAst'
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
@ -7,10 +7,10 @@ import { err } from 'lib/trap'
export function pathMapToSelections(
ast: Program,
prevSelections: Selections,
prevSelections: Selections__old,
pathToNodeMap: { [key: number]: PathToNode }
): Selections {
const newSelections: Selections = {
): Selections__old {
const newSelections: Selections__old = {
...prevSelections,
codeBasedSelections: [],
}
@ -47,7 +47,7 @@ export function updatePathToNodeFromMap(
export function isCursorInSketchCommandRange(
artifactGraph: ArtifactGraph,
selectionRanges: Selections
selectionRanges: Selections__old
): string | false {
const overlappingEntries = filterArtifacts(
{

View File

@ -2,7 +2,7 @@ import { Models } from '@kittycad/lib'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections'
import { Selections__old } from 'lib/selections'
import { machineManager } from 'lib/machineManager'
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
@ -28,17 +28,17 @@ export type ModelingCommandSchema = {
machine: components['schemas']['MachineInfoResponse']
}
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]
distance: KclCommandValue
}
Revolve: {
selection: Selections
selection: Selections__old
angle: KclCommandValue
}
Fillet: {
// todo
selection: Selections
selection: Selections__old
radius: KclCommandValue
}
'change tool': {

View File

@ -1,9 +1,10 @@
import { CustomIconName } from 'components/CustomIcon'
import { AllMachines } from 'hooks/useStateMachineCommands'
import { Actor, AnyStateMachine, ContextFrom, EventFrom } from 'xstate'
import { Selection } from './selections'
import { Selection__old } from './selections'
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
import { commandBarMachine } from 'machines/commandBarMachine'
import { ReactNode } from 'react'
type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const
@ -67,6 +68,12 @@ export type Command<
name: CommandName
groupId: T['id']
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
onCancel?: () => void
args?: {
@ -133,7 +140,7 @@ export type CommandArgumentConfig<
}
| {
inputType: 'selection'
selectionTypes: Selection['type'][]
selectionTypes: Selection__old['type'][]
multiple: boolean
}
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
@ -181,7 +188,7 @@ export type CommandArgument<
machineContext?: ContextFrom<T>
) => boolean)
skip?: boolean
machineActor: Actor<T>
machineActor?: Actor<T>
/** For showing a summary display of the current value, such as in
* the command bar's header
*/
@ -206,7 +213,7 @@ export type CommandArgument<
}
| {
inputType: 'selection'
selectionTypes: Selection['type'][]
selectionTypes: Selection__old['type'][]
multiple: boolean
}
| { 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',
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 { err } from 'lib/trap'
import {
Artifact,
getArtifactOfTypes,
getArtifactsOfTypes,
getCapCodeRef,
getSweepEdgeCodeRef,
getSolid2dCodeRef,
getWallCodeRef,
CodeRef,
getCodeRefsByArtifactId,
} from 'lang/std/artifactGraph'
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 Selection = {
/** @deprecated Use {@link Artifact} instead. */
export type Selection__old = {
type:
| 'default'
| 'line-end'
@ -58,9 +62,129 @@ export type Selection = {
| 'all'
range: SourceRange
}
export type Selections = {
/** @deprecated Use {@link Selection} instead. */
export type Selections__old = {
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({
@ -85,85 +209,102 @@ export async function getEventForSelectWithPoint({
}
}
let _artifact = engineCommandManager.artifactGraph.get(data.entity_id)
if (!_artifact)
return {
type: 'Set selection',
data: { selectionType: 'singleCodeCursor' },
}
if (_artifact.type === 'solid2D') {
const codeRef = getSolid2dCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
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
const codeRefs = getCodeRefsByArtifactId(
data.entity_id,
engineCommandManager.artifactGraph
)
if (_artifact && codeRefs) {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: codeRef.range,
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
artifact: _artifact,
codeRef: codeRefs[0],
},
},
}
}
if (_artifact.type === 'wall') {
const codeRef = getWallCodeRef(
_artifact,
engineCommandManager.artifactGraph
)
if (err(codeRef)) return null
return {
type: 'Set selection',
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' },
},
}
}
// if (!_artifact)
// return {
// type: 'Set selection',
// data: { selectionType: 'singleCodeCursor' },
// }
// if (_artifact.type === 'solid2D') {
// const codeRef = getSolid2dCodeRef(
// _artifact,
// engineCommandManager.artifactGraph
// )
// if (err(codeRef)) return null
// 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 {
// 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 {
// type: 'Set selection',
// 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
}
@ -182,36 +323,56 @@ export function getEventForSegmentSelection(
},
}
}
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'
// console.log('group', group?.userData.)
const id = group?.userData?.id
if (!id) return null
const artifact = engineCommandManager.artifactGraph.get(id)
const codeRefs = getCodeRefsByArtifactId(
id,
engineCommandManager.artifactGraph
)
if (err(nodeMeta)) return null
const node = nodeMeta.node
const range: SourceRange = [node.start, node.end]
console.log('artifact', artifact, group.userData)
if (!artifact || !codeRefs) return null
return {
type: 'Set selection',
data: {
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({
selections,
}: {
selections: Selections
selections: Selections__old
}): {
engineEvents: Models['WebSocketRequest_type'][]
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({
codeMirrorRanges,
@ -260,14 +421,15 @@ export function processCodeMirrorRanges({
isShiftDown,
}: {
codeMirrorRanges: readonly SelectionRange[]
selectionRanges: Selections
selectionRanges: Selections__old
isShiftDown: boolean
}): null | {
modelingEvent: ModelingMachineEvent
engineEvents: Models['WebSocketRequest_type'][]
} {
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) => {
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
@ -276,7 +438,7 @@ export function processCodeMirrorRanges({
})
if (!isChange) return null
const codeBasedSelections: Selections['codeBasedSelections'] =
const codeBasedSelections: Selections__old['codeBasedSelections'] =
codeMirrorRanges.map(({ from, to }) => {
return {
type: 'default',
@ -285,6 +447,15 @@ export function processCodeMirrorRanges({
})
const idBasedSelections: SelectionToEngine[] =
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
updateSceneObjectColors(codeBasedSelections)
@ -295,7 +466,7 @@ export function processCodeMirrorRanges({
selectionType: 'mirrorCodeMirrorSelections',
selection: {
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
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
return isCursorInSketchCommandRange(
engineCommandManager.artifactGraph,
@ -367,14 +538,14 @@ export function isSketchPipe(selectionRanges: Selections) {
}
export function isSelectionLastLine(
selectionRanges: Selections,
selectionRanges: Selections__old,
code: string,
i = 0
) {
return selectionRanges.codeBasedSelections[i].range[1] === code.length
}
export function isRangeBetweenCharacters(selectionRanges: Selections) {
export function isRangeBetweenCharacters(selectionRanges: Selections__old) {
return (
selectionRanges.codeBasedSelections.length === 1 &&
selectionRanges.codeBasedSelections[0].range[0] === 0 &&
@ -383,11 +554,14 @@ export function isRangeBetweenCharacters(selectionRanges: Selections) {
}
export type CommonASTNode = {
selection: Selection
selection: Selection__old
ast: Program
}
function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) {
function buildCommonNodeFromSelection(
selectionRanges: Selections__old,
i: number
) {
return {
selection: selectionRanges.codeBasedSelections[i],
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) =>
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) =>
buildCommonNodeFromSelection(selection, i)
) // 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 = {
...selection,
codeBasedSelections: [selection.codeBasedSelections[i]],
@ -459,7 +633,7 @@ function canExtrudeSelectionItem(selection: Selections, i: number) {
}
// 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
@ -469,8 +643,9 @@ export type ResolvedSelectionType = [Selection['type'] | 'other', number]
* @returns
*/
export function getSelectionType(
selection: Selections
selection?: Selections__old
): ResolvedSelectionType[] {
if (!selection) return []
const extrudableCount = selection.codeBasedSelections.filter((_, i) => {
const singleSelection = {
...selection,
@ -485,7 +660,7 @@ export function getSelectionType(
}
export function getSelectionTypeDisplayText(
selection: Selections
selection?: Selections__old
): string | null {
const selectionsByType = getSelectionType(selection)
@ -517,7 +692,7 @@ export function canSubmitSelectionArg(
}
function codeToIdSelections(
codeBasedSelections: Selection[]
codeBasedSelections: Selection__old[]
): SelectionToEngine[] {
return codeBasedSelections
.flatMap(({ type, range, ...rest }): null | SelectionToEngine[] => {
@ -683,13 +858,13 @@ export async function sendSelectEventToEngine(
export function updateSelections(
pathToNodeMap: PathToNodeMap,
prevSelectionRanges: Selections,
prevSelectionRanges: Selections__old,
ast: Program | Error
): Selections | Error {
): Selections__old | Error {
if (err(ast)) return ast
const newSelections = Object.entries(pathToNodeMap)
.map(([index, pathToNode]): Selection | undefined => {
.map(([index, pathToNode]): Selection__old | undefined => {
const nodeMeta = getNodeFromPath<Expr>(ast, pathToNode)
if (err(nodeMeta)) return undefined
const node = nodeMeta.node
@ -698,7 +873,7 @@ export function updateSelections(
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,
}
})
.filter((x?: Selection) => x !== undefined) as Selection[]
.filter((x?: Selection__old) => x !== undefined) as Selection__old[]
return {
codeBasedSelections:
@ -708,3 +883,74 @@ export function updateSelections(
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 { executeAst } from 'lang/langHelpers'
import { err, trap } from 'lib/trap'
import { convertSelectionsToOld } from './selections'
const isValidVariableName = (name: string) =>
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
@ -34,9 +35,10 @@ export function useCalculateKclExpression({
} {
const { programMemory, code } = useKclContext()
const { context } = useModelingContext()
const selectionOld = convertSelectionsToOld(context.selectionRanges)
const selectionRange:
| (typeof context.selectionRanges.codeBasedSelections)[number]['range']
| undefined = context.selectionRanges.codeBasedSelections[0]?.range
| (typeof selectionOld.codeBasedSelections)[number]['range']
| undefined = selectionOld.codeBasedSelections[0]?.range
const inputRef = useRef<HTMLInputElement>(null)
const [availableVarInfo, setAvailableVarInfo] = useState<
ReturnType<typeof findAllPreviousVariables>

View File

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

View File

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

View File

@ -20,6 +20,7 @@ type FileMachineEvents =
makeDir: boolean
content?: string
silent?: boolean
shouldSetToRename?: boolean
}
}
| { type: 'Delete file'; data: FileEntry }
@ -42,6 +43,7 @@ type FileMachineEvents =
output: {
message: string
path: string
shouldSetToRename: boolean
}
}
| {
@ -107,6 +109,10 @@ export const fileMachine = setup({
},
'Is not silent': ({ event }) =>
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: {
readFiles: fromPromise(({ input }: { input: Project }) =>
@ -119,6 +125,7 @@ export const fileMachine = setup({
makeDir: boolean
selectedDirectory: FileEntry
content: string
shouldSetToRename: boolean
}
}) => Promise.resolve({ message: '', path: '' })
),
@ -149,7 +156,7 @@ export const fileMachine = setup({
),
},
}).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',
initial: 'Reading files',
@ -222,15 +229,41 @@ export const fileMachine = setup({
makeDir: false,
selectedDirectory: context.selectedDirectory,
content: '',
shouldSetToRename: false,
}
return {
name: event.data.name,
makeDir: event.data.makeDir,
selectedDirectory: context.selectedDirectory,
content: event.data.content ?? '',
shouldSetToRename: event.data.shouldSetToRename ?? false,
}
},
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',
actions: [
@ -248,7 +281,6 @@ export const fileMachine = setup({
return { message: event.output.message }
},
},
'addFileToRenamingQueue',
'navigateToFile',
],
},

View File

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

View File

@ -251,18 +251,14 @@ export function getAutoUpdater(): AppUpdater {
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', () => {
const autoUpdater = getAutoUpdater()
checkForUpdates(autoUpdater).catch(reportRejection)
setTimeout(() => {
autoUpdater.checkForUpdates().catch(reportRejection)
}, 1000)
const fifteenMinutes = 15 * 60 * 1000
setInterval(() => {
checkForUpdates(autoUpdater).catch(reportRejection)
autoUpdater.checkForUpdates().catch(reportRejection)
}, fifteenMinutes)
autoUpdater.on('update-available', (info) => {
@ -271,6 +267,11 @@ app.on('ready', () => {
autoUpdater.on('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)
const loginWithDeviceFlow = (): Promise<string> =>
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 isWindows = os.platform() === 'win32'
@ -123,4 +126,6 @@ contextBridge.exposeInMainWorld('electron', {
kittycad,
listMachines,
getMachineApiIp,
onUpdateDownloaded,
appRestart,
})

View File

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

View File

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

View File

@ -520,7 +520,7 @@ pub fn get_autocomplete_snippet_from_schema(
let mut fn_docs = String::new();
fn_docs.push_str("{\n");
// 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() {
if prop_name.starts_with('_') {
continue;
@ -532,15 +532,15 @@ pub fn get_autocomplete_snippet_from_schema(
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));
i += 1;
i = new_index + 1;
}
}
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 {
@ -589,6 +589,7 @@ pub fn get_autocomplete_snippet_from_schema(
if let Some(subschemas) = &o.subschemas {
let mut fn_docs = String::new();
let mut i = index;
if let Some(items) = &subschemas.one_of {
let mut had_enum_string = false;
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() {
return Ok(Some((index, parsed_enum_values[0].to_string())));
} 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);
}
}
} else if let Some(items) = &subschemas.any_of {
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);
}
}
@ -634,7 +637,7 @@ pub fn get_autocomplete_snippet_from_schema(
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 {
@ -915,10 +918,10 @@ mod tests {
r#"patternCircular3d({
arcDegrees: ${0:3.14},
axis: [${1:3.14}, ${2:3.14}, ${3:3.14}],
center: [${2:3.14}, ${3:3.14}, ${4:3.14}],
repetitions: ${3:10},
rotateDuplicates: ${4:false},
}, ${5:%})${}"#
center: [${4:3.14}, ${5:3.14}, ${6:3.14}],
repetitions: ${7:10},
rotateDuplicates: ${8:false},
}, ${9:%})${}"#
);
}
@ -942,8 +945,36 @@ mod tests {
snippet,
r#"circle({
center: [${0:3.14}, ${1:3.14}],
radius: ${1:3.14},
}, ${2:%})${}"#
radius: ${2:3.14},
}, ${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