Compare commits
21 Commits
pierremtb/
...
nightly-v2
| Author | SHA1 | Date | |
|---|---|---|---|
| f321ecdff0 | |||
| d114ab798c | |||
| 69fec37107 | |||
| 8ca8c49cc3 | |||
| b25fc302fd | |||
| 648b37c1dd | |||
| 18e5da5ca4 | |||
| 46524cda10 | |||
| 4585671a5d | |||
| e7203b9e7a | |||
| ab375f4b92 | |||
| 04ed6f52ee | |||
| 2332338ca1 | |||
| 41b97de3d1 | |||
| 8ef31a0be1 | |||
| 3adb42b5f2 | |||
| 20016b101e | |||
| 8d9dbf36c3 | |||
| 440704ed9f | |||
| 2261217a5d | |||
| 10da986649 |
@ -4,8 +4,12 @@ excerpt: "Import a CAD file."
|
|||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**WARNING:** This function is deprecated.
|
||||||
|
|
||||||
Import a CAD file.
|
Import a CAD file.
|
||||||
|
|
||||||
|
**DEPRECATED** Prefer to use import statements.
|
||||||
|
|
||||||
For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.
|
For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.
|
||||||
|
|
||||||
Note: The import command currently only works when using the native Modeling App.
|
Note: The import command currently only works when using the native Modeling App.
|
||||||
|
|||||||
@ -51,7 +51,6 @@ layout: manual
|
|||||||
* [`helixRevolutions`](kcl/helixRevolutions)
|
* [`helixRevolutions`](kcl/helixRevolutions)
|
||||||
* [`hole`](kcl/hole)
|
* [`hole`](kcl/hole)
|
||||||
* [`hollow`](kcl/hollow)
|
* [`hollow`](kcl/hollow)
|
||||||
* [`import`](kcl/import)
|
|
||||||
* [`inch`](kcl/inch)
|
* [`inch`](kcl/inch)
|
||||||
* [`lastSegX`](kcl/lastSegX)
|
* [`lastSegX`](kcl/lastSegX)
|
||||||
* [`lastSegY`](kcl/lastSegY)
|
* [`lastSegY`](kcl/lastSegY)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -92765,7 +92765,7 @@
|
|||||||
{
|
{
|
||||||
"name": "import",
|
"name": "import",
|
||||||
"summary": "Import a CAD file.",
|
"summary": "Import a CAD file.",
|
||||||
"description": "For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.\n\nNote: The import command currently only works when using the native Modeling App.\n\nFor importing KCL functions using the `import` statement, see the docs on [KCL modules](/docs/kcl/modules).",
|
"description": "**DEPRECATED** Prefer to use import statements.\n\nFor formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.\n\nNote: The import command currently only works when using the native Modeling App.\n\nFor importing KCL functions using the `import` statement, see the docs on [KCL modules](/docs/kcl/modules).",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"keywordArguments": false,
|
"keywordArguments": false,
|
||||||
"args": [
|
"args": [
|
||||||
@ -93168,7 +93168,7 @@
|
|||||||
"labelRequired": true
|
"labelRequired": true
|
||||||
},
|
},
|
||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": true,
|
||||||
"examples": [
|
"examples": [
|
||||||
"model = import(\"tests/inputs/cube.obj\")",
|
"model = import(\"tests/inputs/cube.obj\")",
|
||||||
"model = import(\"tests/inputs/cube.obj\", { format = \"obj\", units = \"m\" })",
|
"model = import(\"tests/inputs/cube.obj\", { format = \"obj\", units = \"m\" })",
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { test, expect } from './zoo-test'
|
import { test, expect } from './zoo-test'
|
||||||
|
import * as fsp from 'fs/promises'
|
||||||
import { getUtils } from './test-utils'
|
import { executorInputPath, getUtils } from './test-utils'
|
||||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
test.describe('Command bar tests', () => {
|
test.describe('Command bar tests', () => {
|
||||||
test('Extrude from command bar selects extrude line after', async ({
|
test('Extrude from command bar selects extrude line after', async ({
|
||||||
@ -305,4 +306,132 @@ test.describe('Command bar tests', () => {
|
|||||||
await arcToolCommand.click()
|
await arcToolCommand.click()
|
||||||
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
|
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Reacts to query param to open "import from URL" command`, async ({
|
||||||
|
page,
|
||||||
|
cmdBar,
|
||||||
|
editor,
|
||||||
|
homePage,
|
||||||
|
}) => {
|
||||||
|
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||||
|
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||||
|
await homePage.expectState({
|
||||||
|
projectCards: [],
|
||||||
|
sortBy: 'last-modified-desc',
|
||||||
|
})
|
||||||
|
await page.goto(page.url() + targetURL)
|
||||||
|
expect(page.url()).toContain(targetURL)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Submit the command`, async () => {
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
currentArgKey: 'method',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: '',
|
||||||
|
Name: 'test',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'method',
|
||||||
|
})
|
||||||
|
await cmdBar.selectOption({ name: 'New Project' }).click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'New project',
|
||||||
|
Name: 'test',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||||
|
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`"import from URL" can add to existing project`, async ({
|
||||||
|
page,
|
||||||
|
cmdBar,
|
||||||
|
editor,
|
||||||
|
homePage,
|
||||||
|
toolbar,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
const testProjectDir = path.join(dir, 'testProjectDir')
|
||||||
|
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
|
||||||
|
await Promise.all([
|
||||||
|
fsp.copyFile(
|
||||||
|
executorInputPath('cylinder.kcl'),
|
||||||
|
path.join(testProjectDir, 'main.kcl')
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||||
|
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||||
|
await homePage.expectState({
|
||||||
|
projectCards: [
|
||||||
|
{
|
||||||
|
fileCount: 1,
|
||||||
|
title: 'testProjectDir',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sortBy: 'last-modified-desc',
|
||||||
|
})
|
||||||
|
await page.goto(page.url() + targetURL)
|
||||||
|
expect(page.url()).toContain(targetURL)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Submit the command`, async () => {
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
currentArgKey: 'method',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: '',
|
||||||
|
Name: 'test',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'method',
|
||||||
|
})
|
||||||
|
await cmdBar.selectOption({ name: 'Existing Project' }).click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
currentArgKey: 'projectName',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'Existing project',
|
||||||
|
Name: 'test',
|
||||||
|
ProjectName: '',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'projectName',
|
||||||
|
})
|
||||||
|
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
commandName: 'Import file from URL',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'Existing project',
|
||||||
|
ProjectName: 'testProjectDir',
|
||||||
|
Name: 'test',
|
||||||
|
Code: '1 line',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||||
|
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||||
|
await toolbar.openPane('files')
|
||||||
|
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -38,14 +38,14 @@ test.describe('Debug pane', () => {
|
|||||||
// Set the code in the code editor.
|
// Set the code in the code editor.
|
||||||
await u.codeLocator.click()
|
await u.codeLocator.click()
|
||||||
await page.keyboard.type(code, { delay: 0 })
|
await page.keyboard.type(code, { delay: 0 })
|
||||||
// Scroll to the feature tree.
|
// Scroll to the artifact graph.
|
||||||
await tree.scrollIntoViewIfNeeded()
|
await tree.scrollIntoViewIfNeeded()
|
||||||
// Expand the feature tree.
|
// Expand the artifact graph.
|
||||||
await tree.getByText('Feature Tree').click()
|
await tree.getByText('Artifact Graph').click()
|
||||||
// Just expanded the details, making the element taller, so scroll again.
|
// Just expanded the details, making the element taller, so scroll again.
|
||||||
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
|
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
|
||||||
})
|
})
|
||||||
// Extract the artifact IDs from the debug feature tree.
|
// Extract the artifact IDs from the debug artifact graph.
|
||||||
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
|
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
|
||||||
// The artifact ID should include a UUID.
|
// The artifact ID should include a UUID.
|
||||||
expect(initialSegmentIds).toMatch(
|
expect(initialSegmentIds).toMatch(
|
||||||
|
|||||||
@ -151,4 +151,11 @@ export class CmdBarFixture {
|
|||||||
chooseCommand = async (commandName: string) => {
|
chooseCommand = async (commandName: string) => {
|
||||||
await this.cmdOptions.getByText(commandName).click()
|
await this.cmdOptions.getByText(commandName).click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select an option from the command bar
|
||||||
|
*/
|
||||||
|
selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
|
||||||
|
return this.page.getByRole('option', options)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -963,37 +963,31 @@ sketch002 = startSketchOn('XZ')
|
|||||||
await toolbar.sweepButton.click()
|
await toolbar.sweepButton.click()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Sweep',
|
commandName: 'Sweep',
|
||||||
currentArgKey: 'profile',
|
currentArgKey: 'target',
|
||||||
currentArgValue: '',
|
currentArgValue: '',
|
||||||
headerArguments: {
|
headerArguments: {
|
||||||
Path: '',
|
Target: '',
|
||||||
Profile: '',
|
Trajectory: '',
|
||||||
},
|
},
|
||||||
highlightedHeaderArg: 'profile',
|
highlightedHeaderArg: 'target',
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
})
|
})
|
||||||
await clickOnSketch1()
|
await clickOnSketch1()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Sweep',
|
commandName: 'Sweep',
|
||||||
currentArgKey: 'path',
|
currentArgKey: 'trajectory',
|
||||||
currentArgValue: '',
|
currentArgValue: '',
|
||||||
headerArguments: {
|
headerArguments: {
|
||||||
Path: '',
|
Target: '1 face',
|
||||||
Profile: '1 face',
|
Trajectory: '',
|
||||||
},
|
},
|
||||||
highlightedHeaderArg: 'path',
|
highlightedHeaderArg: 'trajectory',
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
})
|
})
|
||||||
await clickOnSketch2()
|
await clickOnSketch2()
|
||||||
await cmdBar.expectState({
|
await page.waitForTimeout(500)
|
||||||
commandName: 'Sweep',
|
|
||||||
headerArguments: {
|
|
||||||
Path: '1 face',
|
|
||||||
Profile: '1 face',
|
|
||||||
},
|
|
||||||
stage: 'review',
|
|
||||||
})
|
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
@ -1020,6 +1014,75 @@ sketch002 = startSketchOn('XZ')
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Sweep point-and-click failing validation`, async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||||
|
|> circle({
|
||||||
|
center = [0, 0],
|
||||||
|
radius = 500
|
||||||
|
}, %)
|
||||||
|
sketch002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> xLine(-500, %)
|
||||||
|
|> lineTo([-2000, 500], %)
|
||||||
|
`
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 700, y: 250 }
|
||||||
|
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
|
||||||
|
|
||||||
|
await test.step(`Look for sketch001`, async () => {
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow and fail validation with a toast`, async () => {
|
||||||
|
await toolbar.sweepButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Sweep',
|
||||||
|
currentArgKey: 'target',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Target: '',
|
||||||
|
Trajectory: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'target',
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await clickOnSketch1()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Sweep',
|
||||||
|
currentArgKey: 'trajectory',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Target: '1 face',
|
||||||
|
Trajectory: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'trajectory',
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await clickOnSketch2()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await expect(
|
||||||
|
page.getByText('Unable to sweep with the provided selection')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test(`Fillet point-and-click`, async ({
|
test(`Fillet point-and-click`, async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
@ -113,9 +113,9 @@
|
|||||||
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
||||||
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
|
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
|
||||||
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||||
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet",
|
||||||
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot' --quiet",
|
||||||
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot' --quiet",
|
||||||
"test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
"test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||||
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||||
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||||
@ -201,7 +201,7 @@
|
|||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.19.1",
|
"typescript-eslint": "^8.19.1",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.12",
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
|
|||||||
@ -683,9 +683,9 @@ vite-tsconfig-paths@^4.3.2:
|
|||||||
tsconfck "^3.0.3"
|
tsconfck "^3.0.3"
|
||||||
|
|
||||||
vite@^5.0.0:
|
vite@^5.0.0:
|
||||||
version "5.4.11"
|
version "5.4.14"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
|
||||||
integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==
|
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.21.3"
|
esbuild "^0.21.3"
|
||||||
postcss "^8.4.43"
|
postcss "^8.4.43"
|
||||||
|
|||||||
15
src/App.tsx
15
src/App.tsx
@ -22,13 +22,28 @@ import Gizmo from 'components/Gizmo'
|
|||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||||
|
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
maybeWriteToDisk()
|
maybeWriteToDisk()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
|
|
||||||
|
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||||
|
useCreateFileLinkQuery((argDefaultValues) => {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'projects',
|
||||||
|
name: 'Import file from URL',
|
||||||
|
argDefaultValues,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
|
|||||||
@ -31,11 +31,10 @@ import {
|
|||||||
settingsLoader,
|
settingsLoader,
|
||||||
telemetryLoader,
|
telemetryLoader,
|
||||||
} from 'lib/routeLoaders'
|
} from 'lib/routeLoaders'
|
||||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
|
||||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||||
import LspProvider from 'components/LspProvider'
|
import LspProvider from 'components/LspProvider'
|
||||||
import { KclContextProvider } from 'lang/KclProvider'
|
import { KclContextProvider } from 'lang/KclProvider'
|
||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
@ -47,6 +46,7 @@ import { AppStateProvider } from 'AppState'
|
|||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { RouteProvider } from 'components/RouteProvider'
|
import { RouteProvider } from 'components/RouteProvider'
|
||||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||||
|
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ const router = createRouter([
|
|||||||
/* Make sure auth is the outermost provider or else we will have
|
/* Make sure auth is the outermost provider or else we will have
|
||||||
* inefficient re-renders, use the react profiler to see. */
|
* inefficient re-renders, use the react profiler to see. */
|
||||||
element: (
|
element: (
|
||||||
<CommandBarProvider>
|
<OpenInDesktopAppHandler>
|
||||||
<RouteProvider>
|
<RouteProvider>
|
||||||
<SettingsAuthProvider>
|
<SettingsAuthProvider>
|
||||||
<LspProvider>
|
<LspProvider>
|
||||||
@ -74,17 +74,26 @@ const router = createRouter([
|
|||||||
</LspProvider>
|
</LspProvider>
|
||||||
</SettingsAuthProvider>
|
</SettingsAuthProvider>
|
||||||
</RouteProvider>
|
</RouteProvider>
|
||||||
</CommandBarProvider>
|
</OpenInDesktopAppHandler>
|
||||||
),
|
),
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: PATHS.INDEX,
|
path: PATHS.INDEX,
|
||||||
loader: async () => {
|
loader: async ({ request }) => {
|
||||||
const onDesktop = isDesktop()
|
const onDesktop = isDesktop()
|
||||||
return onDesktop
|
const url = new URL(request.url)
|
||||||
? redirect(PATHS.HOME)
|
if (onDesktop) {
|
||||||
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
return redirect(PATHS.HOME + (url.search || ''))
|
||||||
|
} else {
|
||||||
|
const searchParams = new URLSearchParams(url.search)
|
||||||
|
if (!searchParams.has(ASK_TO_OPEN_QUERY_PARAM)) {
|
||||||
|
return redirect(
|
||||||
|
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useRef, useMemo, memo, useCallback, useState } from 'react'
|
|||||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
@ -22,13 +21,13 @@ import {
|
|||||||
} from 'lib/toolbar'
|
} from 'lib/toolbar'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLElement>) {
|
}: React.HTMLAttributes<HTMLElement>) {
|
||||||
const { state, send, context } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const iconClassName =
|
const iconClassName =
|
||||||
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
|
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
|
||||||
const bgClassName = '!bg-transparent'
|
const bgClassName = '!bg-transparent'
|
||||||
@ -71,10 +70,9 @@ export function Toolbar({
|
|||||||
() => ({
|
() => ({
|
||||||
modelingState: state,
|
modelingState: state,
|
||||||
modelingSend: send,
|
modelingSend: send,
|
||||||
commandBarSend,
|
|
||||||
sketchPathId,
|
sketchPathId,
|
||||||
}),
|
}),
|
||||||
[state, send, commandBarSend, sketchPathId]
|
[state, send, commandBarActor.send, sketchPathId]
|
||||||
)
|
)
|
||||||
|
|
||||||
const tooltipContentClassName = !showRichContent
|
const tooltipContentClassName = !showRichContent
|
||||||
|
|||||||
@ -46,8 +46,8 @@ import {
|
|||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||||
@ -510,7 +510,6 @@ const ConstraintSymbol = ({
|
|||||||
constrainInfo: ConstrainInfo
|
constrainInfo: ConstrainInfo
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
}) => {
|
}) => {
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const { context } = useModelingContext()
|
const { context } = useModelingContext()
|
||||||
const varNameMap: {
|
const varNameMap: {
|
||||||
[key in ConstrainInfo['type']]: {
|
[key in ConstrainInfo['type']]: {
|
||||||
@ -630,7 +629,7 @@ const ConstraintSymbol = ({
|
|||||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||||
onClick={toSync(async () => {
|
onClick={toSync(async () => {
|
||||||
if (!isConstrained) {
|
if (!isConstrained) {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
name: 'Constrain with named value',
|
name: 'Constrain with named value',
|
||||||
@ -756,7 +755,6 @@ export const CamDebugSettings = () => {
|
|||||||
sceneInfra.camControls.reactCameraProperties
|
sceneInfra.camControls.reactCameraProperties
|
||||||
)
|
)
|
||||||
const [fov, setFov] = useState(12)
|
const [fov, setFov] = useState(12)
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
||||||
@ -775,7 +773,7 @@ export const CamDebugSettings = () => {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={camSettings.type === 'perspective'}
|
checked={camSettings.type === 'perspective'}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'settings',
|
groupId: 'settings',
|
||||||
|
|||||||
@ -69,7 +69,8 @@ import {
|
|||||||
codeManager,
|
codeManager,
|
||||||
editorManager,
|
editorManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { executeAst, ToolTip } from 'lang/langHelpers'
|
import { executeAst, ToolTip } from 'lang/langHelpers'
|
||||||
import {
|
import {
|
||||||
createProfileStartHandle,
|
createProfileStartHandle,
|
||||||
|
|||||||
@ -61,6 +61,7 @@ import { SegmentInputs } from 'lang/std/stdTypes'
|
|||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { editorManager, sceneInfra } from 'lib/singletons'
|
import { editorManager, sceneInfra } from 'lib/singletons'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
interface CreateSegmentArgs {
|
interface CreateSegmentArgs {
|
||||||
input: SegmentInputs
|
input: SegmentInputs
|
||||||
@ -847,7 +848,7 @@ function createLengthIndicator({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Command Bar
|
// Command Bar
|
||||||
editorManager.commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
name: 'Constrain length',
|
name: 'Constrain length',
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
|
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { trap } from 'lib/trap'
|
import { trap } from 'lib/trap'
|
||||||
import { codeToIdSelections } from 'lib/selections'
|
import { codeToIdSelections } from 'lib/selections'
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Combobox } from '@headlessui/react'
|
import { Combobox } from '@headlessui/react'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ function CommandArgOptionInput({
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
}) {
|
}) {
|
||||||
const actorContext = useSelector(arg.machineActor, contextSelector)
|
const actorContext = useSelector(arg.machineActor, contextSelector)
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const resolvedOptions = useMemo(
|
const resolvedOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
typeof arg.options === 'function'
|
typeof arg.options === 'function'
|
||||||
@ -129,6 +129,7 @@ function CommandArgOptionInput({
|
|||||||
<label
|
<label
|
||||||
htmlFor="option-input"
|
htmlFor="option-input"
|
||||||
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
||||||
|
data-testid="cmd-bar-arg-name"
|
||||||
>
|
>
|
||||||
{argName}
|
{argName}
|
||||||
</label>
|
</label>
|
||||||
@ -142,7 +143,7 @@ function CommandArgOptionInput({
|
|||||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.metaKey && event.key === 'k')
|
if (event.metaKey && event.key === 'k')
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||||
stepBack()
|
stepBack()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import CommandBarArgument from './CommandBarArgument'
|
import CommandBarArgument from './CommandBarArgument'
|
||||||
import CommandComboBox from '../CommandComboBox'
|
import CommandComboBox from '../CommandComboBox'
|
||||||
import CommandBarReview from './CommandBarReview'
|
import CommandBarReview from './CommandBarReview'
|
||||||
@ -8,12 +7,13 @@ import { useLocation } from 'react-router-dom'
|
|||||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
||||||
|
|
||||||
export const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -23,16 +23,16 @@ export const CommandBar = () => {
|
|||||||
// Close the command bar when navigating
|
// Close the command bar when navigating
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (commandBarState.matches('Closed')) return
|
if (commandBarState.matches('Closed')) return
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
// Hook up keyboard shortcuts
|
// Hook up keyboard shortcuts
|
||||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||||
if (commandBarState.context.commands.length === 0) return
|
if (commandBarState.context.commands.length === 0) return
|
||||||
if (commandBarState.matches('Closed')) {
|
if (commandBarState.matches('Closed')) {
|
||||||
commandBarSend({ type: 'Open' })
|
commandBarActor.send({ type: 'Open' })
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -52,14 +52,14 @@ export const CommandBar = () => {
|
|||||||
...entries[entries.length - 1][1],
|
...entries[entries.length - 1][1],
|
||||||
}
|
}
|
||||||
|
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Edit argument',
|
type: 'Edit argument',
|
||||||
data: {
|
data: {
|
||||||
arg: currentArg,
|
arg: currentArg,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarActor.send({ type: 'Deselect command' })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const entries = Object.entries(selectedCommand?.args || {})
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
@ -68,9 +68,9 @@ export const CommandBar = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarActor.send({ type: 'Deselect command' })
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Change current argument',
|
type: 'Change current argument',
|
||||||
data: {
|
data: {
|
||||||
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
|
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
|
||||||
@ -85,14 +85,14 @@ export const CommandBar = () => {
|
|||||||
show={!commandBarState.matches('Closed') || false}
|
show={!commandBarState.matches('Closed') || false}
|
||||||
afterLeave={() => {
|
afterLeave={() => {
|
||||||
if (selectedCommand?.onCancel) selectedCommand.onCancel()
|
if (selectedCommand?.onCancel) selectedCommand.onCancel()
|
||||||
commandBarSend({ type: 'Clear' })
|
commandBarActor.send({ type: 'Clear' })
|
||||||
}}
|
}}
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
>
|
||||||
<WrapperComponent
|
<WrapperComponent
|
||||||
open={!commandBarState.matches('Closed') || isSelectionArgument}
|
open={!commandBarState.matches('Closed') || isSelectionArgument}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
||||||
@ -122,7 +122,7 @@ export const CommandBar = () => {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => commandBarSend({ type: 'Close' })}
|
onClick={() => commandBarActor.send({ type: 'Close' })}
|
||||||
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
|
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
|
||||||
>
|
>
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import CommandArgOptionInput from './CommandArgOptionInput'
|
|||||||
import CommandBarBasicInput from './CommandBarBasicInput'
|
import CommandBarBasicInput from './CommandBarBasicInput'
|
||||||
import CommandBarSelectionInput from './CommandBarSelectionInput'
|
import CommandBarSelectionInput from './CommandBarSelectionInput'
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import CommandBarHeader from './CommandBarHeader'
|
import CommandBarHeader from './CommandBarHeader'
|
||||||
import CommandBarKclInput from './CommandBarKclInput'
|
import CommandBarKclInput from './CommandBarKclInput'
|
||||||
import CommandBarTextareaInput from './CommandBarTextareaInput'
|
import CommandBarTextareaInput from './CommandBarTextareaInput'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const {
|
const {
|
||||||
context: { currentArgument },
|
context: { currentArgument },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -16,7 +16,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
|||||||
function onSubmit(data: unknown) {
|
function onSubmit(data: unknown) {
|
||||||
if (!currentArgument) return
|
if (!currentArgument) return
|
||||||
|
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Submit argument',
|
type: 'Submit argument',
|
||||||
data: {
|
data: {
|
||||||
[currentArgument.name]: data,
|
[currentArgument.name]: data,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
@ -15,8 +15,8 @@ function CommandBarBasicInput({
|
|||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (event: unknown) => void
|
onSubmit: (event: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CustomIcon } from '../CustomIcon'
|
import { CustomIcon } from '../CustomIcon'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { ActionButton } from '../ActionButton'
|
import { ActionButton } from '../ActionButton'
|
||||||
@ -7,9 +6,10 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
|||||||
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
|
import { KclCommandValue, KclExpressionWithVariable } from 'lib/commandTypes'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, argumentsToSubmit },
|
context: { selectedCommand, currentArgument, argumentsToSubmit },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -49,7 +49,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
]
|
]
|
||||||
const arg = selectedCommand?.args[argName]
|
const arg = selectedCommand?.args[argName]
|
||||||
if (!argName || !arg) return
|
if (!argName || !arg) return
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Change current argument',
|
type: 'Change current argument',
|
||||||
data: { arg: { ...arg, name: argName } },
|
data: { arg: { ...arg, name: argName } },
|
||||||
})
|
})
|
||||||
@ -100,7 +100,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
disabled={!isReviewing && currentArgument?.name === argName}
|
disabled={!isReviewing && currentArgument?.name === argName}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: isReviewing
|
type: isReviewing
|
||||||
? 'Edit argument'
|
? 'Edit argument'
|
||||||
: 'Change current argument',
|
: 'Change current argument',
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
|
import { EditorView, keymap, ViewUpdate } from '@codemirror/view'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
import { CommandArgument, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { getSystemTheme } from 'lib/theme'
|
import { getSystemTheme } from 'lib/theme'
|
||||||
@ -20,6 +19,7 @@ import styles from './CommandBarKclInput.module.css'
|
|||||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
const machineContextSelector = (snapshot?: {
|
const machineContextSelector = (snapshot?: {
|
||||||
context: Record<string, unknown>
|
context: Record<string, unknown>
|
||||||
@ -37,7 +37,7 @@ function CommandBarKclInput({
|
|||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (event: unknown) => void
|
onSubmit: (event: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||||
arg.name
|
arg.name
|
||||||
] as KclCommandValue | undefined
|
] as KclCommandValue | undefined
|
||||||
@ -82,7 +82,7 @@ function CommandBarKclInput({
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
const [canSubmit, setCanSubmit] = useState(true)
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
const editorRef = useRef<HTMLDivElement>(null)
|
const editorRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
import { createActorContext } from '@xstate/react'
|
|
||||||
import { editorManager } from 'lib/singletons'
|
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
export const CommandsContext = createActorContext(
|
|
||||||
commandBarMachine.provide({
|
|
||||||
guards: {
|
|
||||||
'Command has no arguments': ({ context }) => {
|
|
||||||
return (
|
|
||||||
!context.selectedCommand?.args ||
|
|
||||||
Object.keys(context.selectedCommand?.args).length === 0
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'All arguments are skippable': ({ context }) => {
|
|
||||||
return Object.values(context.selectedCommand!.args!).every(
|
|
||||||
(argConfig) => argConfig.skip
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
export const CommandBarProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<CommandsContext.Provider>
|
|
||||||
<CommandBarProviderInner>{children}</CommandBarProviderInner>
|
|
||||||
</CommandsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function CommandBarProviderInner({ children }: { children: React.ReactNode }) {
|
|
||||||
const commandBarActor = CommandsContext.useActorRef()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
editorManager.setCommandBarSend(commandBarActor.send)
|
|
||||||
})
|
|
||||||
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import CommandBarHeader from './CommandBarHeader'
|
import CommandBarHeader from './CommandBarHeader'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const {
|
const {
|
||||||
context: { argumentsToSubmit, selectedCommand },
|
context: { argumentsToSubmit, selectedCommand },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -33,7 +33,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
parseInt(b.keys[0], 10) - 1
|
parseInt(b.keys[0], 10) - 1
|
||||||
]
|
]
|
||||||
const arg = selectedCommand?.args[argName]
|
const arg = selectedCommand?.args[argName]
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Edit argument',
|
type: 'Edit argument',
|
||||||
data: { arg: { ...arg, name: argName } },
|
data: { arg: { ...arg, name: argName } },
|
||||||
})
|
})
|
||||||
@ -50,7 +50,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
|
|
||||||
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
|
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Submit command',
|
type: 'Submit command',
|
||||||
output: argumentsToSubmit,
|
output: argumentsToSubmit,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { Artifact } from 'lang/std/artifactGraph'
|
import { Artifact } from 'lang/std/artifactGraph'
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
@ -10,6 +9,7 @@ import {
|
|||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { StateFrom } from 'xstate'
|
import { StateFrom } from 'xstate'
|
||||||
@ -49,7 +49,7 @@ function CommandBarSelectionInput({
|
|||||||
onSubmit: (data: unknown) => void
|
onSubmit: (data: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||||
const selection = useSelector(arg.machineActor, selectionSelector)
|
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||||
const selectionsByType = useMemo(() => {
|
const selectionsByType = useMemo(() => {
|
||||||
@ -145,7 +145,7 @@ function CommandBarSelectionInput({
|
|||||||
if (event.key === 'Backspace') {
|
if (event.key === 'Backspace') {
|
||||||
stepBack()
|
stepBack()
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CommandArgument } from 'lib/commandTypes'
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
import { RefObject, useEffect, useRef } from 'react'
|
import { RefObject, useEffect, useRef } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
@ -15,8 +15,8 @@ function CommandBarTextareaInput({
|
|||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (event: unknown) => void
|
onSubmit: (event: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
useTextareaAutoGrow(inputRef)
|
useTextareaAutoGrow(inputRef)
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||||
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export function CommandBarOpenButton() {
|
export function CommandBarOpenButton() {
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||||
onClick={() => commandBarSend({ type: 'Open' })}
|
onClick={() => commandBarActor.send({ type: 'Open' })}
|
||||||
data-testid="command-bar-open-button"
|
data-testid="command-bar-open-button"
|
||||||
>
|
>
|
||||||
<span>Commands</span>
|
<span>Commands</span>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Combobox } from '@headlessui/react'
|
import { Combobox } from '@headlessui/react'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { Command } from 'lib/commandTypes'
|
import { Command } from 'lib/commandTypes'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { getActorNextEvents } from 'lib/utils'
|
import { getActorNextEvents } from 'lib/utils'
|
||||||
import { sortCommands } from 'lib/commandUtils'
|
import { sortCommands } from 'lib/commandUtils'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
function CommandComboBox({
|
function CommandComboBox({
|
||||||
options,
|
options,
|
||||||
@ -14,7 +14,6 @@ function CommandComboBox({
|
|||||||
options: Command[]
|
options: Command[]
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ function CommandComboBox({
|
|||||||
}, [query])
|
}, [query])
|
||||||
|
|
||||||
function handleSelection(command: Command) {
|
function handleSelection(command: Command) {
|
||||||
commandBarSend({ type: 'Select command', data: { command } })
|
commandBarActor.send({ type: 'Select command', data: { command } })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,7 +60,7 @@ function CommandComboBox({
|
|||||||
(event.key === 'Backspace' && !event.currentTarget.value)
|
(event.key === 'Backspace' && !event.currentTarget.value)
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={
|
placeholder={
|
||||||
@ -76,34 +75,40 @@ function CommandComboBox({
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Combobox.Options
|
{filteredOptions?.length ? (
|
||||||
static
|
<Combobox.Options
|
||||||
className="overflow-y-auto max-h-96 cursor-pointer"
|
static
|
||||||
>
|
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||||
{filteredOptions?.map((option) => (
|
>
|
||||||
<Combobox.Option
|
{filteredOptions?.map((option) => (
|
||||||
key={option.groupId + option.name + (option.displayName || '')}
|
<Combobox.Option
|
||||||
value={option}
|
key={option.groupId + option.name + (option.displayName || '')}
|
||||||
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
|
value={option}
|
||||||
disabled={optionIsDisabled(option)}
|
className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
|
||||||
data-testid={`cmd-bar-option`}
|
disabled={optionIsDisabled(option)}
|
||||||
>
|
data-testid={`cmd-bar-option`}
|
||||||
{'icon' in option && option.icon && (
|
>
|
||||||
<CustomIcon name={option.icon} className="w-5 h-5" />
|
{'icon' in option && option.icon && (
|
||||||
)}
|
<CustomIcon name={option.icon} className="w-5 h-5" />
|
||||||
<div className="flex-grow flex flex-col">
|
|
||||||
<p className="my-0 leading-tight">
|
|
||||||
{option.displayName || option.name}{' '}
|
|
||||||
</p>
|
|
||||||
{option.description && (
|
|
||||||
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
|
|
||||||
{option.description}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="flex-grow flex flex-col">
|
||||||
</Combobox.Option>
|
<p className="my-0 leading-tight">
|
||||||
))}
|
{option.displayName || option.name}{' '}
|
||||||
</Combobox.Options>
|
</p>
|
||||||
|
{option.description && (
|
||||||
|
<p className="my-0 text-xs text-chalkboard-60 dark:text-chalkboard-50">
|
||||||
|
{option.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
) : (
|
||||||
|
<p className="px-4 pt-2 text-chalkboard-60 dark:text-chalkboard-50">
|
||||||
|
No results found
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,18 +4,18 @@ import { expandPlane, PlaneArtifactRich } from 'lang/std/artifactGraph'
|
|||||||
import { ArtifactGraph } from 'lang/wasm'
|
import { ArtifactGraph } from 'lang/wasm'
|
||||||
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
|
import { DebugDisplayArray, GenericObj } from './DebugDisplayObj'
|
||||||
|
|
||||||
export function DebugFeatureTree() {
|
export function DebugArtifactGraph() {
|
||||||
const featureTree = useMemo(() => {
|
const artifactGraphTree = useMemo(() => {
|
||||||
return computeTree(engineCommandManager.artifactGraph)
|
return computeTree(engineCommandManager.artifactGraph)
|
||||||
}, [engineCommandManager.artifactGraph])
|
}, [engineCommandManager.artifactGraph])
|
||||||
|
|
||||||
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
|
const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode']
|
||||||
return (
|
return (
|
||||||
<details data-testid="debug-feature-tree" className="relative">
|
<details data-testid="debug-feature-tree" className="relative">
|
||||||
<summary>Feature Tree</summary>
|
<summary>Artifact Graph</summary>
|
||||||
{featureTree.length > 0 ? (
|
{artifactGraphTree.length > 0 ? (
|
||||||
<pre className="text-xs">
|
<pre className="text-xs">
|
||||||
<DebugDisplayArray arr={featureTree} filterKeys={filterKeys} />
|
<DebugDisplayArray arr={artifactGraphTree} filterKeys={filterKeys} />
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<p>(Empty)</p>
|
<p>(Empty)</p>
|
||||||
@ -12,7 +12,6 @@ import {
|
|||||||
StateFrom,
|
StateFrom,
|
||||||
fromPromise,
|
fromPromise,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import {
|
import {
|
||||||
@ -30,6 +29,7 @@ import {
|
|||||||
} from 'lib/getKclSamplesManifest'
|
} from 'lib/getKclSamplesManifest'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { markOnce } from 'lib/performance'
|
import { markOnce } from 'lib/performance'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -47,9 +47,9 @@ export const FileMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { settings, auth } = useSettingsAuthContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const { project, file } = projectData
|
||||||
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@ -90,7 +90,7 @@ export const FileMachineProvider = ({
|
|||||||
navigateToFile: ({ context, event }) => {
|
navigateToFile: ({ context, event }) => {
|
||||||
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
if (event.type !== 'xstate.done.actor.create-and-open-file') return
|
||||||
if (event.output && 'name' in event.output) {
|
if (event.output && 'name' in event.output) {
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`..${PATHS.FILE}/${encodeURIComponent(
|
`..${PATHS.FILE}/${encodeURIComponent(
|
||||||
context.selectedDirectory +
|
context.selectedDirectory +
|
||||||
@ -296,55 +296,65 @@ export const FileMachineProvider = ({
|
|||||||
|
|
||||||
const kclCommandMemo = useMemo(
|
const kclCommandMemo = useMemo(
|
||||||
() =>
|
() =>
|
||||||
kclCommands(
|
kclCommands({
|
||||||
async (data) => {
|
authToken: auth?.context?.token ?? '',
|
||||||
if (data.method === 'overwrite') {
|
projectData,
|
||||||
codeManager.updateCodeStateEditor(data.code)
|
settings: {
|
||||||
await kclManager.executeCode(true)
|
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
|
||||||
await codeManager.writeToFile()
|
|
||||||
} else if (data.method === 'newFile' && isDesktop()) {
|
|
||||||
send({
|
|
||||||
type: 'Create file',
|
|
||||||
data: {
|
|
||||||
name: data.sampleName,
|
|
||||||
content: data.code,
|
|
||||||
makeDir: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either way, we want to overwrite the defaultUnit project setting
|
|
||||||
// with the sample's setting.
|
|
||||||
if (data.sampleUnits) {
|
|
||||||
settings.send({
|
|
||||||
type: 'set.modeling.defaultUnit',
|
|
||||||
data: {
|
|
||||||
level: 'project',
|
|
||||||
value: data.sampleUnits,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
kclSamples.map((sample) => ({
|
specialPropsForSampleCommand: {
|
||||||
value: sample.pathFromProjectDirectoryToFirstFile,
|
onSubmit: async (data) => {
|
||||||
name: sample.title,
|
if (data.method === 'overwrite') {
|
||||||
}))
|
codeManager.updateCodeStateEditor(data.code)
|
||||||
).filter(
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either way, we want to overwrite the defaultUnit project setting
|
||||||
|
// with the sample's setting.
|
||||||
|
if (data.sampleUnits) {
|
||||||
|
settings.send({
|
||||||
|
type: 'set.modeling.defaultUnit',
|
||||||
|
data: {
|
||||||
|
level: 'project',
|
||||||
|
value: data.sampleUnits,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
providedOptions: kclSamples.map((sample) => ({
|
||||||
|
value: sample.pathFromProjectDirectoryToFirstFile,
|
||||||
|
name: sample.title,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}).filter(
|
||||||
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
||||||
),
|
),
|
||||||
[codeManager, kclManager, send, kclSamples]
|
[codeManager, kclManager, send, kclSamples]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
commandBarSend({ type: 'Add commands', data: { commands: kclCommandMemo } })
|
commandBarActor.send({
|
||||||
|
type: 'Add commands',
|
||||||
|
data: { commands: kclCommandMemo },
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: { commands: kclCommandMemo },
|
data: { commands: kclCommandMemo },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [commandBarSend, kclCommandMemo])
|
}, [commandBarActor.send, kclCommandMemo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileContext.Provider
|
<FileContext.Provider
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { createContext, useEffect, useState } from 'react'
|
import { createContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { components } from 'lib/machine-api'
|
import { components } from 'lib/machine-api'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export type MachinesListing = Array<
|
export type MachinesListing = Array<
|
||||||
components['schemas']['MachineInfoResponse']
|
components['schemas']['MachineInfoResponse']
|
||||||
@ -42,8 +42,6 @@ export const MachineManagerProvider = ({
|
|||||||
components['schemas']['MachineInfoResponse'] | null
|
components['schemas']['MachineInfoResponse'] | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
const commandBarActor = CommandsContext.useActorRef()
|
|
||||||
|
|
||||||
// Get the reason message for why there are no machines.
|
// Get the reason message for why there are no machines.
|
||||||
const noMachinesReason = (): string | undefined => {
|
const noMachinesReason = (): string | undefined => {
|
||||||
if (machines.length > 0) {
|
if (machines.length > 0) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { useMachine, useSelector } from '@xstate/react'
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
AnyStateMachine,
|
AnyStateMachine,
|
||||||
ContextFrom,
|
ContextFrom,
|
||||||
Prop,
|
Prop,
|
||||||
|
SnapshotFrom,
|
||||||
StateFrom,
|
StateFrom,
|
||||||
assign,
|
assign,
|
||||||
fromPromise,
|
fromPromise,
|
||||||
@ -67,18 +68,14 @@ import {
|
|||||||
startSketchOnDefault,
|
startSketchOnDefault,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
|
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm'
|
||||||
import {
|
import { artifactIsPlaneWithPaths, isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
artifactIsPlaneWithPaths,
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
getNodePathFromSourceRange,
|
|
||||||
isSingleCursorInPipe,
|
|
||||||
} from 'lang/queryAst'
|
|
||||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||||
import { Models } from '@kittycad/lib/dist/types/src'
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import {
|
import {
|
||||||
ExportIntent,
|
ExportIntent,
|
||||||
EngineConnectionStateType,
|
EngineConnectionStateType,
|
||||||
@ -91,6 +88,7 @@ import { IndexLoaderData } from 'lib/types'
|
|||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { promptToEditFlow } from 'lib/promptToEdit'
|
import { promptToEditFlow } from 'lib/promptToEdit'
|
||||||
import { kclEditorActor } from 'machines/kclEditorMachine'
|
import { kclEditorActor } from 'machines/kclEditorMachine'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -102,6 +100,10 @@ export const ModelingMachineContext = createContext(
|
|||||||
{} as MachineContext<typeof modelingMachine>
|
{} as MachineContext<typeof modelingMachine>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const commandBarIsClosedSelector = (
|
||||||
|
state: SnapshotFrom<typeof commandBarActor>
|
||||||
|
) => state.matches('Closed')
|
||||||
|
|
||||||
export const ModelingMachineProvider = ({
|
export const ModelingMachineProvider = ({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@ -132,8 +134,10 @@ export const ModelingMachineProvider = ({
|
|||||||
let [searchParams] = useSearchParams()
|
let [searchParams] = useSearchParams()
|
||||||
const pool = searchParams.get('pool')
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const isCommandBarClosed = useSelector(
|
||||||
|
commandBarActor,
|
||||||
|
commandBarIsClosedSelector
|
||||||
|
)
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
// const retrievedSettings = useRef(
|
// const retrievedSettings = useRef(
|
||||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||||
@ -388,7 +392,16 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (setSelections.selectionType === 'completeSelection') {
|
if (setSelections.selectionType === 'completeSelection') {
|
||||||
editorManager.selectRange(setSelections.selection)
|
const codeMirrorSelection = editorManager.createEditorSelection(
|
||||||
|
setSelections.selection
|
||||||
|
)
|
||||||
|
kclEditorActor.send({
|
||||||
|
type: 'setLastSelectionEvent',
|
||||||
|
data: {
|
||||||
|
codeMirrorSelection,
|
||||||
|
scrollIntoView: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
if (!sketchDetails)
|
if (!sketchDetails)
|
||||||
return {
|
return {
|
||||||
selectionRanges: setSelections.selection,
|
selectionRanges: setSelections.selection,
|
||||||
@ -529,7 +542,6 @@ export const ModelingMachineProvider = ({
|
|||||||
trimmedPrompt,
|
trimmedPrompt,
|
||||||
fileMachineSend,
|
fileMachineSend,
|
||||||
navigate,
|
navigate,
|
||||||
commandBarSend,
|
|
||||||
context,
|
context,
|
||||||
token,
|
token,
|
||||||
settings: {
|
settings: {
|
||||||
@ -543,7 +555,7 @@ export const ModelingMachineProvider = ({
|
|||||||
'has valid selection for deletion': ({
|
'has valid selection for deletion': ({
|
||||||
context: { selectionRanges },
|
context: { selectionRanges },
|
||||||
}) => {
|
}) => {
|
||||||
if (!commandBarState.matches('Closed')) return false
|
if (!isCommandBarClosed) return false
|
||||||
if (selectionRanges.graphSelections.length <= 0) return false
|
if (selectionRanges.graphSelections.length <= 0) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DebugFeatureTree } from 'components/DebugFeatureTree'
|
import { DebugArtifactGraph } from 'components/DebugArtifactGraph'
|
||||||
import { AstExplorer } from '../../AstExplorer'
|
import { AstExplorer } from '../../AstExplorer'
|
||||||
import { EngineCommands } from '../../EngineCommands'
|
import { EngineCommands } from '../../EngineCommands'
|
||||||
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||||
@ -14,7 +14,7 @@ export const DebugPane = () => {
|
|||||||
<EngineCommands />
|
<EngineCommands />
|
||||||
<CamDebugSettings />
|
<CamDebugSettings />
|
||||||
<AstExplorer />
|
<AstExplorer />
|
||||||
<DebugFeatureTree />
|
<DebugArtifactGraph />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
@apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90;
|
@apply font-mono !no-underline text-xs font-bold select-none text-chalkboard-90;
|
||||||
@apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit;
|
@apply ui-active:bg-primary/10 ui-active:text-primary ui-active:text-inherit;
|
||||||
@apply transition-colors ease-out;
|
@apply transition-colors ease-out;
|
||||||
|
@apply m-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .button {
|
:global(.dark) .button {
|
||||||
|
|||||||
@ -9,12 +9,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
@ -85,7 +84,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'code',
|
groupId: 'code',
|
||||||
|
|||||||
@ -15,12 +15,12 @@ import { ModelingPane } from './ModelingPane'
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -37,7 +37,6 @@ function getPlatformString(): 'web' | 'desktop' {
|
|||||||
|
|
||||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const kclContext = useKclContext()
|
const kclContext = useKclContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
@ -66,7 +65,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
icon: 'floppyDiskArrow',
|
icon: 'floppyDiskArrow',
|
||||||
keybinding: 'Ctrl + Shift + E',
|
keybinding: 'Ctrl + Shift + E',
|
||||||
action: () =>
|
action: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Export', groupId: 'modeling' },
|
data: { name: 'Export', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -79,7 +78,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
keybinding: 'Ctrl + Shift + M',
|
keybinding: 'Ctrl + Shift + M',
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
action: async () => {
|
action: async () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Make', groupId: 'modeling' },
|
data: { name: 'Make', groupId: 'modeling' },
|
||||||
})
|
})
|
||||||
@ -298,7 +297,7 @@ function ModelingPaneButton({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={paneConfig.id + '-button-holder'}>
|
<div id={paneConfig.id + '-button-holder'} className="relative">
|
||||||
<button
|
<button
|
||||||
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@ -340,7 +339,7 @@ function ModelingPaneButton({
|
|||||||
<p
|
<p
|
||||||
id={`${paneConfig.id}-badge`}
|
id={`${paneConfig.id}-badge`}
|
||||||
className={
|
className={
|
||||||
'absolute m-0 p-0 top-1 right-0 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
|
||||||
}
|
}
|
||||||
onClick={showBadge.onClick}
|
onClick={showBadge.onClick}
|
||||||
title={`Click to view ${showBadge.value} notification${
|
title={`Click to view ${showBadge.value} notification${
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
|
||||||
import {
|
import {
|
||||||
NETWORK_HEALTH_TEXT,
|
NETWORK_HEALTH_TEXT,
|
||||||
NetworkHealthIndicator,
|
NetworkHealthIndicator,
|
||||||
@ -12,9 +11,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
|||||||
// wrap in router and xState context
|
// wrap in router and xState context
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/components/OpenInDesktopAppHandler.test.tsx
Normal file
68
src/components/OpenInDesktopAppHandler.test.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||||
|
import { OpenInDesktopAppHandler } from './OpenInDesktopAppHandler'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The behavior under test requires a router,
|
||||||
|
* so we wrap the component in a minimal router setup.
|
||||||
|
*/
|
||||||
|
function TestingMinimalRouterWrapper({
|
||||||
|
children,
|
||||||
|
location,
|
||||||
|
}: {
|
||||||
|
location?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Routes location={location}>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={<OpenInDesktopAppHandler>{children}</OpenInDesktopAppHandler>}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OpenInDesktopAppHandler tests', () => {
|
||||||
|
test(`does not render the modal if no query param is present`, () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<TestingMinimalRouterWrapper>
|
||||||
|
<p>Dummy app contents</p>
|
||||||
|
</TestingMinimalRouterWrapper>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
|
||||||
|
const dummyAppContents = screen.getByText('Dummy app contents')
|
||||||
|
const modalContents = screen.queryByText('Open in desktop app')
|
||||||
|
|
||||||
|
expect(dummyAppContents).toBeInTheDocument()
|
||||||
|
expect(modalContents).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`renders the modal if the query param is present`, () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<TestingMinimalRouterWrapper location="/?ask-open-desktop">
|
||||||
|
<p>Dummy app contents</p>
|
||||||
|
</TestingMinimalRouterWrapper>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
|
||||||
|
let dummyAppContents = screen.queryByText('Dummy app contents')
|
||||||
|
let modalButton = screen.queryByText('Continue to web app')
|
||||||
|
|
||||||
|
// Starts as disconnected
|
||||||
|
expect(dummyAppContents).not.toBeInTheDocument()
|
||||||
|
expect(modalButton).not.toBeFalsy()
|
||||||
|
expect(modalButton).toBeInTheDocument()
|
||||||
|
fireEvent.click(modalButton as Element)
|
||||||
|
|
||||||
|
// I don't like that you have to re-query the screen here
|
||||||
|
dummyAppContents = screen.queryByText('Dummy app contents')
|
||||||
|
modalButton = screen.queryByText('Continue to web app')
|
||||||
|
|
||||||
|
expect(dummyAppContents).toBeInTheDocument()
|
||||||
|
expect(modalButton).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
125
src/components/OpenInDesktopAppHandler.tsx
Normal file
125
src/components/OpenInDesktopAppHandler.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { getSystemTheme, Themes } from 'lib/theme'
|
||||||
|
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
|
||||||
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { ASK_TO_OPEN_QUERY_PARAM } from 'lib/constants'
|
||||||
|
import { VITE_KC_SITE_BASE_URL } from 'env'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import { Transition } from '@headlessui/react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is a handler that checks if a certain query parameter
|
||||||
|
* is present, and if so, it will show a modal asking the user if they
|
||||||
|
* want to open the current page in the desktop app.
|
||||||
|
*/
|
||||||
|
export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
|
||||||
|
const theme = getSystemTheme()
|
||||||
|
const buttonClasses =
|
||||||
|
'bg-transparent flex-0 hover:bg-primary/10 dark:hover:bg-primary/10'
|
||||||
|
const pathLogomarkSvg = `${isDesktop() ? '.' : ''}/zma-logomark${
|
||||||
|
theme === Themes.Light ? '-dark' : ''
|
||||||
|
}.svg`
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
// We also ignore this param on desktop, as it is redundant
|
||||||
|
const hasAskToOpenParam =
|
||||||
|
!isDesktop() && searchParams.has(ASK_TO_OPEN_QUERY_PARAM)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function removes the query param to ask to open in desktop app
|
||||||
|
* and then navigates to the same route but with our custom protocol
|
||||||
|
* `zoo-studio:` instead of `https://${BASE_URL}`, to trigger the user's
|
||||||
|
* desktop app to open.
|
||||||
|
*/
|
||||||
|
function onOpenInDesktopApp() {
|
||||||
|
const newSearchParams = new URLSearchParams(globalThis.location.search)
|
||||||
|
newSearchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
|
||||||
|
const newURL = `${ZOO_STUDIO_PROTOCOL}${globalThis.location.pathname.replace(
|
||||||
|
'/',
|
||||||
|
''
|
||||||
|
)}${searchParams.size > 0 ? `?${newSearchParams.toString()}` : ''}`
|
||||||
|
globalThis.location.href = newURL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just remove the query param to ask to open in desktop app
|
||||||
|
* and continue to the web app.
|
||||||
|
*/
|
||||||
|
function continueToWebApp() {
|
||||||
|
searchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
|
||||||
|
setSearchParams(searchParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasAskToOpenParam ? (
|
||||||
|
<Transition
|
||||||
|
appear
|
||||||
|
show={true}
|
||||||
|
as="div"
|
||||||
|
className={
|
||||||
|
theme +
|
||||||
|
` fixed inset-0 grid p-4 place-content-center ${
|
||||||
|
theme === Themes.Dark ? '!bg-chalkboard-110 text-chalkboard-20' : ''
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Transition.Child
|
||||||
|
as="div"
|
||||||
|
className={`max-w-3xl py-6 px-10 flex flex-col items-center gap-8
|
||||||
|
mx-auto border rounded-lg shadow-lg dark:bg-chalkboard-100`}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
style={{ zIndex: 10 }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl">
|
||||||
|
Launching{' '}
|
||||||
|
<img
|
||||||
|
src={pathLogomarkSvg}
|
||||||
|
className="w-48"
|
||||||
|
alt="Zoo Modeling App"
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-primary flex items-center gap-2">
|
||||||
|
Choose where to open this link...
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col md:flex-row items-start justify-between gap-4 xl:gap-8">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
className={buttonClasses + ' !text-base'}
|
||||||
|
onClick={onOpenInDesktopApp}
|
||||||
|
iconEnd={{ icon: 'arrowRight' }}
|
||||||
|
>
|
||||||
|
Open in desktop app
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="externalLink"
|
||||||
|
className={
|
||||||
|
buttonClasses +
|
||||||
|
' text-sm border-transparent justify-center dark:bg-transparent'
|
||||||
|
}
|
||||||
|
to={`${VITE_KC_SITE_BASE_URL}/modeling-app/download`}
|
||||||
|
iconEnd={{ icon: 'link', bgClassName: '!bg-transparent' }}
|
||||||
|
>
|
||||||
|
Download desktop app
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
className={buttonClasses + ' -order-1 !text-base'}
|
||||||
|
onClick={continueToWebApp}
|
||||||
|
iconStart={{ icon: 'arrowLeft' }}
|
||||||
|
>
|
||||||
|
Continue to web app
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</Transition>
|
||||||
|
) : (
|
||||||
|
props.children
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react'
|
|||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@ -33,11 +32,9 @@ describe('ProjectSidebarMenu tests', () => {
|
|||||||
test('Disables popover menu by default', () => {
|
test('Disables popover menu by default', () => {
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<CommandBarProvider>
|
<SettingsAuthProviderJest>
|
||||||
<SettingsAuthProviderJest>
|
<ProjectSidebarMenu project={projectWellFormed} />
|
||||||
<ProjectSidebarMenu project={projectWellFormed} />
|
</SettingsAuthProviderJest>
|
||||||
</SettingsAuthProviderJest>
|
|
||||||
</CommandBarProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -7,14 +7,19 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import { Fragment, useMemo, useContext } from 'react'
|
import { Fragment, useMemo, useContext } from 'react'
|
||||||
import { Logo } from './Logo'
|
import { Logo } from './Logo'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
|
import { SnapshotFrom } from 'xstate'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { copyFileShareLink } from 'lib/links'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { DEV } from 'env'
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
project,
|
project,
|
||||||
@ -84,6 +89,9 @@ function AppLogoLink({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commandsSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
||||||
|
state.context.commands
|
||||||
|
|
||||||
function ProjectMenuPopover({
|
function ProjectMenuPopover({
|
||||||
project,
|
project,
|
||||||
file,
|
file,
|
||||||
@ -95,17 +103,16 @@ function ProjectMenuPopover({
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
|
const { settings, auth } = useSettingsAuthContext()
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
|
const commands = useSelector(commandBarActor, commandsSelector)
|
||||||
|
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
|
||||||
const { onProjectClose } = useLspContext()
|
const { onProjectClose } = useLspContext()
|
||||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||||
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
||||||
const findCommand = (obj: { name: string; groupId: string }) =>
|
const findCommand = (obj: { name: string; groupId: string }) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
commandBarState.context.commands.find(
|
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId)
|
||||||
(c) => c.name === obj.name && c.groupId === obj.groupId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
const machineCount = machineManager.machines.length
|
const machineCount = machineManager.machines.length
|
||||||
|
|
||||||
@ -150,12 +157,11 @@ function ProjectMenuPopover({
|
|||||||
),
|
),
|
||||||
disabled: !findCommand(exportCommandInfo),
|
disabled: !findCommand(exportCommandInfo),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: exportCommandInfo,
|
data: exportCommandInfo,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'break',
|
|
||||||
{
|
{
|
||||||
id: 'make',
|
id: 'make',
|
||||||
Element: 'button',
|
Element: 'button',
|
||||||
@ -175,12 +181,26 @@ function ProjectMenuPopover({
|
|||||||
),
|
),
|
||||||
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
|
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: makeCommandInfo,
|
data: makeCommandInfo,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'share-link',
|
||||||
|
Element: 'button',
|
||||||
|
children: 'Share link to file',
|
||||||
|
disabled: !DEV,
|
||||||
|
onClick: async () => {
|
||||||
|
await copyFileShareLink({
|
||||||
|
token: auth?.context.token || '',
|
||||||
|
code: codeManager.code,
|
||||||
|
name: project?.name || '',
|
||||||
|
units: settings.context.modeling.defaultUnit.current,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
'break',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'go-home',
|
id: 'go-home',
|
||||||
@ -200,7 +220,7 @@ function ProjectMenuPopover({
|
|||||||
[
|
[
|
||||||
platform,
|
platform,
|
||||||
findCommand,
|
findCommand,
|
||||||
commandBarSend,
|
commandBarActor.send,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
onProjectClose,
|
onProjectClose,
|
||||||
isDesktop,
|
isDesktop,
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||||
import { projectsMachine } from 'machines/projectsMachine'
|
import { projectsMachine } from 'machines/projectsMachine'
|
||||||
import { createContext, useEffect, useState } from 'react'
|
import { createContext, useCallback, useEffect, useState } from 'react'
|
||||||
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
|
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
|
||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import {
|
import {
|
||||||
createNewProjectDirectory,
|
createNewProjectDirectory,
|
||||||
@ -19,11 +18,28 @@ import {
|
|||||||
interpolateProjectNameWithIndex,
|
interpolateProjectNameWithIndex,
|
||||||
doesProjectNameNeedInterpolated,
|
doesProjectNameNeedInterpolated,
|
||||||
getUniqueProjectName,
|
getUniqueProjectName,
|
||||||
|
getNextFileName,
|
||||||
} from 'lib/desktopFS'
|
} from 'lib/desktopFS'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||||
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import {
|
||||||
|
CREATE_FILE_URL_PARAM,
|
||||||
|
FILE_EXT,
|
||||||
|
PROJECT_ENTRYPOINT,
|
||||||
|
} from 'lib/constants'
|
||||||
|
import { DeepPartial } from 'lib/types'
|
||||||
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
|
import { codeManager } from 'lib/singletons'
|
||||||
|
import {
|
||||||
|
loadAndValidateSettings,
|
||||||
|
projectConfigurationToSettingsPayload,
|
||||||
|
saveSettings,
|
||||||
|
setSettingsAtLevel,
|
||||||
|
} from 'lib/settings/settingsUtils'
|
||||||
|
import { Project } from 'lib/project'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state?: StateFrom<T>
|
state?: StateFrom<T>
|
||||||
@ -53,12 +69,110 @@ export const ProjectsContextProvider = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need some of the functionality of the ProjectsContextProvider in the web version
|
||||||
|
* but we can't perform file system operations in the browser,
|
||||||
|
* so most of the behavior of this machine is stubbed out.
|
||||||
|
*/
|
||||||
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const clearImportSearchParams = useCallback(() => {
|
||||||
|
// Clear the search parameters related to the "Import file from URL" command
|
||||||
|
// or we'll never be able cancel or submit it.
|
||||||
|
searchParams.delete(CREATE_FILE_URL_PARAM)
|
||||||
|
searchParams.delete('code')
|
||||||
|
searchParams.delete('name')
|
||||||
|
searchParams.delete('units')
|
||||||
|
setSearchParams(searchParams)
|
||||||
|
}, [searchParams, setSearchParams])
|
||||||
|
const {
|
||||||
|
settings: { context: settings, send: settingsSend },
|
||||||
|
} = useSettingsAuthContext()
|
||||||
|
|
||||||
|
const [state, send, actor] = useMachine(
|
||||||
|
projectsMachine.provide({
|
||||||
|
actions: {
|
||||||
|
navigateToProject: () => {},
|
||||||
|
navigateToProjectIfNeeded: () => {},
|
||||||
|
navigateToFile: () => {},
|
||||||
|
toastSuccess: ({ event }) =>
|
||||||
|
toast.success(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
'message' in event.output &&
|
||||||
|
typeof event.output.message === 'string' &&
|
||||||
|
event.output.message) ||
|
||||||
|
''
|
||||||
|
),
|
||||||
|
toastError: ({ event }) =>
|
||||||
|
toast.error(
|
||||||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
|
('output' in event &&
|
||||||
|
typeof event.output === 'string' &&
|
||||||
|
event.output) ||
|
||||||
|
''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
readProjects: fromPromise(async () => [] as Project[]),
|
||||||
|
createProject: fromPromise(async () => ({
|
||||||
|
message: 'not implemented on web',
|
||||||
|
})),
|
||||||
|
renameProject: fromPromise(async () => ({
|
||||||
|
message: 'not implemented on web',
|
||||||
|
oldName: '',
|
||||||
|
newName: '',
|
||||||
|
})),
|
||||||
|
deleteProject: fromPromise(async () => ({
|
||||||
|
message: 'not implemented on web',
|
||||||
|
name: '',
|
||||||
|
})),
|
||||||
|
createFile: fromPromise(async ({ input }) => {
|
||||||
|
// Browser version doesn't navigate, just overwrites the current file
|
||||||
|
clearImportSearchParams()
|
||||||
|
codeManager.updateCodeStateEditor(input.code || '')
|
||||||
|
await codeManager.writeToFile()
|
||||||
|
|
||||||
|
settingsSend({
|
||||||
|
type: 'set.modeling.defaultUnit',
|
||||||
|
data: {
|
||||||
|
level: 'project',
|
||||||
|
value: input.units,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'File and units overwritten successfully',
|
||||||
|
fileName: input.name,
|
||||||
|
projectName: '',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
projects: [],
|
||||||
|
defaultProjectName: settings.projects.defaultProjectName.current,
|
||||||
|
defaultDirectory: settings.app.projectDirectory.current,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// register all project-related command palette commands
|
||||||
|
useStateMachineCommands({
|
||||||
|
machineId: 'projects',
|
||||||
|
send,
|
||||||
|
state,
|
||||||
|
commandBarConfig: projectsCommandBarConfig,
|
||||||
|
actor,
|
||||||
|
onCancel: clearImportSearchParams,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectsMachineContext.Provider
|
<ProjectsMachineContext.Provider
|
||||||
value={{
|
value={{
|
||||||
state: undefined,
|
state,
|
||||||
send: () => {},
|
send,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -73,19 +187,21 @@ const ProjectsContextDesktop = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { commandBarSend } = useCommandsContext()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const clearImportSearchParams = useCallback(() => {
|
||||||
|
// Clear the search parameters related to the "Import file from URL" command
|
||||||
|
// or we'll never be able cancel or submit it.
|
||||||
|
searchParams.delete(CREATE_FILE_URL_PARAM)
|
||||||
|
searchParams.delete('code')
|
||||||
|
searchParams.delete('name')
|
||||||
|
searchParams.delete('units')
|
||||||
|
setSearchParams(searchParams)
|
||||||
|
}, [searchParams, setSearchParams])
|
||||||
const { onProjectOpen } = useLspContext()
|
const { onProjectOpen } = useLspContext()
|
||||||
const {
|
const {
|
||||||
settings: { context: settings },
|
settings: { context: settings },
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(
|
|
||||||
'project directory changed',
|
|
||||||
settings.app.projectDirectory.current
|
|
||||||
)
|
|
||||||
}, [settings.app.projectDirectory.current])
|
|
||||||
|
|
||||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
const { projectPaths, projectsDir } = useProjectsLoader([
|
||||||
projectsLoaderTrigger,
|
projectsLoaderTrigger,
|
||||||
@ -126,7 +242,7 @@ const ProjectsContextDesktop = ({
|
|||||||
},
|
},
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
commandBarSend({ type: 'Close' })
|
commandBarActor.send({ type: 'Close' })
|
||||||
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
|
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
|
||||||
projectPath
|
projectPath
|
||||||
)}`
|
)}`
|
||||||
@ -169,6 +285,31 @@ const ProjectsContextDesktop = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
navigateToFile: ({ context, event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.create-file') return
|
||||||
|
// For now, the browser version of create-file doesn't need to navigate
|
||||||
|
// since it just overwrites the current file.
|
||||||
|
if (!isDesktop()) return
|
||||||
|
let projectPath = window.electron.join(
|
||||||
|
context.defaultDirectory,
|
||||||
|
event.output.projectName
|
||||||
|
)
|
||||||
|
let filePath = window.electron.join(
|
||||||
|
projectPath,
|
||||||
|
event.output.fileName
|
||||||
|
)
|
||||||
|
onProjectOpen(
|
||||||
|
{
|
||||||
|
name: event.output.projectName,
|
||||||
|
path: projectPath,
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent(
|
||||||
|
filePath
|
||||||
|
)}`
|
||||||
|
navigate(pathToNavigateTo)
|
||||||
|
},
|
||||||
toastSuccess: ({ event }) =>
|
toastSuccess: ({ event }) =>
|
||||||
toast.success(
|
toast.success(
|
||||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||||
@ -218,8 +359,6 @@ const ProjectsContextDesktop = ({
|
|||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('from Project')
|
|
||||||
|
|
||||||
await renameProjectDirectory(
|
await renameProjectDirectory(
|
||||||
window.electron.path.join(defaultDirectory, oldName),
|
window.electron.path.join(defaultDirectory, oldName),
|
||||||
name
|
name
|
||||||
@ -242,13 +381,82 @@ const ProjectsContextDesktop = ({
|
|||||||
name: input.name,
|
name: input.name,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
createFile: fromPromise(async ({ input }) => {
|
||||||
guards: {
|
let projectName =
|
||||||
'Has at least 1 project': ({ event }) => {
|
(input.method === 'newProject' ? input.name : input.projectName) ||
|
||||||
if (event.type !== 'xstate.done.actor.read-projects') return false
|
settings.projects.defaultProjectName.current
|
||||||
console.log(`from has at least 1 project: ${event.output.length}`)
|
let fileName =
|
||||||
return event.output.length ? event.output.length >= 1 : false
|
input.method === 'newProject'
|
||||||
},
|
? PROJECT_ENTRYPOINT
|
||||||
|
: input.name.endsWith(FILE_EXT)
|
||||||
|
? input.name
|
||||||
|
: input.name + FILE_EXT
|
||||||
|
let message = 'File created successfully'
|
||||||
|
const unitsConfiguration: DeepPartial<Configuration> = {
|
||||||
|
settings: {
|
||||||
|
project: {
|
||||||
|
directory: settings.app.projectDirectory.current,
|
||||||
|
},
|
||||||
|
modeling: {
|
||||||
|
base_unit: input.units,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
|
||||||
|
if (needsInterpolated) {
|
||||||
|
const nextIndex = getNextProjectIndex(projectName, input.projects)
|
||||||
|
projectName = interpolateProjectNameWithIndex(
|
||||||
|
projectName,
|
||||||
|
nextIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the project around the file if newProject
|
||||||
|
if (input.method === 'newProject') {
|
||||||
|
await createNewProjectDirectory(
|
||||||
|
projectName,
|
||||||
|
input.code,
|
||||||
|
unitsConfiguration
|
||||||
|
)
|
||||||
|
message = `Project "${projectName}" created successfully with link contents`
|
||||||
|
} else {
|
||||||
|
let projectPath = window.electron.join(
|
||||||
|
settings.app.projectDirectory.current,
|
||||||
|
projectName
|
||||||
|
)
|
||||||
|
|
||||||
|
message = `File "${fileName}" created successfully`
|
||||||
|
const existingConfiguration = await loadAndValidateSettings(
|
||||||
|
projectPath
|
||||||
|
)
|
||||||
|
const settingsToSave = setSettingsAtLevel(
|
||||||
|
existingConfiguration.settings,
|
||||||
|
'project',
|
||||||
|
projectConfigurationToSettingsPayload(unitsConfiguration)
|
||||||
|
)
|
||||||
|
await saveSettings(settingsToSave, projectPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the file
|
||||||
|
let baseDir = window.electron.join(
|
||||||
|
settings.app.projectDirectory.current,
|
||||||
|
projectName
|
||||||
|
)
|
||||||
|
const { name, path } = getNextFileName({
|
||||||
|
entryName: fileName,
|
||||||
|
baseDir,
|
||||||
|
})
|
||||||
|
|
||||||
|
fileName = name
|
||||||
|
await window.electron.writeFile(path, input.code || '')
|
||||||
|
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
fileName,
|
||||||
|
projectName,
|
||||||
|
}
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@ -271,6 +479,7 @@ const ProjectsContextDesktop = ({
|
|||||||
state,
|
state,
|
||||||
commandBarConfig: projectsCommandBarConfig,
|
commandBarConfig: projectsCommandBarConfig,
|
||||||
actor,
|
actor,
|
||||||
|
onCancel: clearImportSearchParams,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -29,7 +29,6 @@ import {
|
|||||||
createSettingsCommand,
|
createSettingsCommand,
|
||||||
settingsWithCommandConfigs,
|
settingsWithCommandConfigs,
|
||||||
} from 'lib/commandBarConfigs/settingsCommandConfig'
|
} from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { Command } from 'lib/commandTypes'
|
import { Command } from 'lib/commandTypes'
|
||||||
import { BaseUnit } from 'lib/settings/settingsTypes'
|
import { BaseUnit } from 'lib/settings/settingsTypes'
|
||||||
import {
|
import {
|
||||||
@ -42,6 +41,7 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { codeManager } from 'lib/singletons'
|
import { codeManager } from 'lib/singletons'
|
||||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -109,7 +109,6 @@ export const SettingsAuthProviderBase = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
const [settingsPath, setSettingsPath] = useState<string | undefined>(
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
@ -278,10 +277,10 @@ export const SettingsAuthProviderBase = ({
|
|||||||
)
|
)
|
||||||
.filter((c) => c !== null) as Command[]
|
.filter((c) => c !== null) as Command[]
|
||||||
|
|
||||||
commandBarSend({ type: 'Add commands', data: { commands: commands } })
|
commandBarActor.send({ type: 'Add commands', data: { commands: commands } })
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: { commands },
|
data: { commands },
|
||||||
})
|
})
|
||||||
@ -290,7 +289,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
settingsState,
|
settingsState,
|
||||||
settingsSend,
|
settingsSend,
|
||||||
settingsActor,
|
settingsActor,
|
||||||
commandBarSend,
|
commandBarActor.send,
|
||||||
settingsWithCommandConfigs,
|
settingsWithCommandConfigs,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -303,7 +302,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
||||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||||
createRouteCommands(navigate, location, filePath)
|
createRouteCommands(navigate, location, filePath)
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: {
|
data: {
|
||||||
commands: [
|
commands: [
|
||||||
@ -314,12 +313,12 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (location.pathname === PATHS.HOME) {
|
if (location.pathname === PATHS.HOME) {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Add commands',
|
type: 'Add commands',
|
||||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||||
})
|
})
|
||||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Add commands',
|
type: 'Add commands',
|
||||||
data: {
|
data: {
|
||||||
commands: [
|
commands: [
|
||||||
|
|||||||
@ -17,10 +17,11 @@ import {
|
|||||||
import { useRouteLoaderData } from 'react-router-dom'
|
import { useRouteLoaderData } from 'react-router-dom'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||||
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
enum StreamState {
|
enum StreamState {
|
||||||
Playing = 'playing',
|
Playing = 'playing',
|
||||||
@ -35,7 +36,7 @@ export const Stream = () => {
|
|||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { state, send } = useModelingContext()
|
const { state, send } = useModelingContext()
|
||||||
const { commandBarState } = useCommandsContext()
|
const commandBarState = useCommandBarState()
|
||||||
const { mediaStream } = useAppStream()
|
const { mediaStream } = useAppStream()
|
||||||
const { overallState, immediateState } = useNetworkContext()
|
const { overallState, immediateState } = useNetworkContext()
|
||||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
const [streamState, setStreamState] = useState(StreamState.Unset)
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import { base64Decode } from 'lang/wasm'
|
|||||||
import { sendTelemetry } from 'lib/textToCad'
|
import { sendTelemetry } from 'lib/textToCad'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import { EventFrom } from 'xstate'
|
import { EventFrom } from 'xstate'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
@ -43,15 +43,10 @@ export function ToastTextToCadError({
|
|||||||
toastId,
|
toastId,
|
||||||
message,
|
message,
|
||||||
prompt,
|
prompt,
|
||||||
commandBarSend,
|
|
||||||
}: {
|
}: {
|
||||||
toastId: string
|
toastId: string
|
||||||
message: string
|
message: string
|
||||||
prompt: string
|
prompt: string
|
||||||
commandBarSend: (
|
|
||||||
event: EventFrom<typeof commandBarMachine>,
|
|
||||||
data?: unknown
|
|
||||||
) => void
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-between gap-6">
|
<div className="flex flex-col justify-between gap-6">
|
||||||
@ -81,7 +76,7 @@ export function ToastTextToCadError({
|
|||||||
}}
|
}}
|
||||||
name="Edit prompt"
|
name="Edit prompt"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'modeling',
|
groupId: 'modeling',
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { toolTips } from 'lang/langHelpers'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
|
import { Program, Expr, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import { getNodeFromPath } from '../../lang/queryAst'
|
||||||
getNodePathFromSourceRange,
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
getNodeFromPath,
|
|
||||||
} from '../../lang/queryAst'
|
|
||||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||||
import {
|
import {
|
||||||
transformSecondarySketchLinesTagFirst,
|
transformSecondarySketchLinesTagFirst,
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import {
|
|||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
@ -124,9 +123,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
|||||||
<Route
|
<Route
|
||||||
path="/file/:id"
|
path="/file/:id"
|
||||||
element={
|
element={
|
||||||
<CommandBarProvider>
|
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
||||||
<SettingsAuthProviderJest>{children}</SettingsAuthProviderJest>
|
|
||||||
</CommandBarProvider>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { engineCommandManager, kclManager } from 'lib/singletons'
|
|||||||
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { Selections, Selection, processCodeMirrorRanges } from 'lib/selections'
|
import { Selections, Selection, processCodeMirrorRanges } from 'lib/selections'
|
||||||
import { undo, redo } from '@codemirror/commands'
|
import { undo, redo } from '@codemirror/commands'
|
||||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
|
||||||
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
||||||
import {
|
import {
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
@ -52,9 +51,6 @@ export default class EditorManager {
|
|||||||
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||||
private _modelingState: StateFrom<typeof modelingMachine> | null = null
|
private _modelingState: StateFrom<typeof modelingMachine> | null = null
|
||||||
|
|
||||||
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
|
|
||||||
() => {}
|
|
||||||
|
|
||||||
private _convertToVariableEnabled: boolean = false
|
private _convertToVariableEnabled: boolean = false
|
||||||
private _convertToVariableCallback: () => void = () => {}
|
private _convertToVariableCallback: () => void = () => {}
|
||||||
|
|
||||||
@ -161,14 +157,6 @@ export default class EditorManager {
|
|||||||
this._modelingState = state
|
this._modelingState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
|
|
||||||
this._commandBarSend = send
|
|
||||||
}
|
|
||||||
|
|
||||||
commandBarSend(eventInfo: CommandBarMachineEvent): void {
|
|
||||||
return this._commandBarSend(eventInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
get highlightRange(): Array<[number, number]> {
|
get highlightRange(): Array<[number, number]> {
|
||||||
return this._highlightRange
|
return this._highlightRange
|
||||||
}
|
}
|
||||||
@ -315,6 +303,21 @@ export default class EditorManager {
|
|||||||
if (selections?.graphSelections?.length === 0) {
|
if (selections?.graphSelections?.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this._editorView) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const codeBaseSelections = this.createEditorSelection(selections)
|
||||||
|
this._editorView.dispatch({
|
||||||
|
selection: codeBaseSelections,
|
||||||
|
annotations: [
|
||||||
|
updateOutsideEditorEvent,
|
||||||
|
Transaction.addToHistory.of(false),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createEditorSelection(selections: Selections) {
|
||||||
let codeBasedSelections = []
|
let codeBasedSelections = []
|
||||||
for (const selection of selections.graphSelections) {
|
for (const selection of selections.graphSelections) {
|
||||||
const safeEnd = Math.min(
|
const safeEnd = Math.min(
|
||||||
@ -331,18 +334,7 @@ export default class EditorManager {
|
|||||||
.range[1]
|
.range[1]
|
||||||
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end)
|
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end)
|
||||||
codeBasedSelections.push(EditorSelection.cursor(safeEnd))
|
codeBasedSelections.push(EditorSelection.cursor(safeEnd))
|
||||||
|
return EditorSelection.create(codeBasedSelections, 1)
|
||||||
if (!this._editorView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._editorView.dispatch({
|
|
||||||
selection: EditorSelection.create(codeBasedSelections, 1),
|
|
||||||
annotations: [
|
|
||||||
updateOutsideEditorEvent,
|
|
||||||
Transaction.addToHistory.of(false),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We will ONLY get here if the user called a select event.
|
// We will ONLY get here if the user called a select event.
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
|
||||||
|
|
||||||
export const useCommandsContext = () => {
|
|
||||||
const commandBarActor = CommandsContext.useActorRef()
|
|
||||||
const commandBarState = CommandsContext.useSelector((state) => state)
|
|
||||||
return {
|
|
||||||
commandBarSend: commandBarActor.send,
|
|
||||||
commandBarState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
65
src/hooks/useCreateFileLinkQueryWatcher.ts
Normal file
65
src/hooks/useCreateFileLinkQueryWatcher.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { base64ToString } from 'lib/base64'
|
||||||
|
import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { useSettingsAuthContext } from './useSettingsAuthContext'
|
||||||
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { FileLinkParams } from 'lib/links'
|
||||||
|
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||||
|
import { baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||||
|
|
||||||
|
// For initializing the command arguments, we actually want `method` to be undefined
|
||||||
|
// so that we don't skip it in the command palette.
|
||||||
|
export type CreateFileSchemaMethodOptional = Omit<
|
||||||
|
ProjectsCommandSchema['Import file from URL'],
|
||||||
|
'method'
|
||||||
|
> & {
|
||||||
|
method?: 'newProject' | 'existingProject'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* companion to createFileLink. This hook runs an effect on mount that
|
||||||
|
* checks the URL for the CREATE_FILE_URL_PARAM and triggers the "Create file"
|
||||||
|
* command if it is present, loading the command's default values from the other
|
||||||
|
* URL parameters.
|
||||||
|
*/
|
||||||
|
export function useCreateFileLinkQuery(
|
||||||
|
callback: (args: CreateFileSchemaMethodOptional) => void
|
||||||
|
) {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const { settings } = useSettingsAuthContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const createFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
|
||||||
|
|
||||||
|
if (createFileParam) {
|
||||||
|
const params: FileLinkParams = {
|
||||||
|
code: base64ToString(
|
||||||
|
decodeURIComponent(searchParams.get('code') ?? '')
|
||||||
|
),
|
||||||
|
|
||||||
|
name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
|
||||||
|
|
||||||
|
units:
|
||||||
|
(baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
|
||||||
|
settings.context.modeling.defaultUnit.default) ??
|
||||||
|
settings.context.modeling.defaultUnit.current,
|
||||||
|
}
|
||||||
|
|
||||||
|
const argDefaultValues: CreateFileSchemaMethodOptional = {
|
||||||
|
name: params.name
|
||||||
|
? isDesktop()
|
||||||
|
? params.name.replace('.kcl', '')
|
||||||
|
: params.name
|
||||||
|
: isDesktop()
|
||||||
|
? settings.context.projects.defaultProjectName.current
|
||||||
|
: DEFAULT_FILE_NAME,
|
||||||
|
code: params.code || '',
|
||||||
|
units: params.units,
|
||||||
|
method: isDesktop() ? undefined : 'existingProject',
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(argDefaultValues)
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
}
|
||||||
@ -17,7 +17,8 @@ import {
|
|||||||
} from 'lang/std/artifactGraph'
|
} from 'lang/std/artifactGraph'
|
||||||
import { err, reportRejection } from 'lib/trap'
|
import { err, reportRejection } from 'lib/trap'
|
||||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { CallExpression, defaultSourceRange } from 'lang/wasm'
|
import { CallExpression, defaultSourceRange } from 'lang/wasm'
|
||||||
import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine'
|
import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine'
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export const useProjectsLoader = (deps?: [number]) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Useless on web, until we get fake filesystems over there.
|
// Useless on web, until we get fake filesystems over there.
|
||||||
if (!isDesktop) return
|
if (!isDesktop()) return
|
||||||
|
|
||||||
if (deps && deps[0] === lastTs) return
|
if (deps && deps[0] === lastTs) return
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AnyStateMachine, Actor, StateFrom, EventFrom } from 'xstate'
|
import { AnyStateMachine, Actor, StateFrom, EventFrom } from 'xstate'
|
||||||
import { createMachineCommand } from '../lib/createMachineCommand'
|
import { createMachineCommand } from '../lib/createMachineCommand'
|
||||||
import { useCommandsContext } from './useCommandsContext'
|
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
import { authMachine } from 'machines/authMachine'
|
import { authMachine } from 'machines/authMachine'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
@ -15,6 +14,7 @@ import { useKclContext } from 'lang/KclProvider'
|
|||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { useAppState } from 'AppState'
|
import { useAppState } from 'AppState'
|
||||||
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
export type AllMachines =
|
export type AllMachines =
|
||||||
@ -48,7 +48,6 @@ export default function useStateMachineCommands<
|
|||||||
allCommandsRequireNetwork = false,
|
allCommandsRequireNetwork = false,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: UseStateMachineCommandsArgs<T, S>) {
|
}: UseStateMachineCommandsArgs<T, S>) {
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
@ -76,10 +75,13 @@ export default function useStateMachineCommands<
|
|||||||
})
|
})
|
||||||
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
|
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
|
||||||
|
|
||||||
commandBarSend({ type: 'Add commands', data: { commands: newCommands } })
|
commandBarActor.send({
|
||||||
|
type: 'Add commands',
|
||||||
|
data: { commands: newCommands },
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: { commands: newCommands },
|
data: { commands: newCommands },
|
||||||
})
|
})
|
||||||
|
|||||||
@ -322,6 +322,7 @@ export class KclManager {
|
|||||||
await this.ensureWasmInit()
|
await this.ensureWasmInit()
|
||||||
const { logs, errors, execState, isInterrupted } = await executeAst({
|
const { logs, errors, execState, isInterrupted } = await executeAst({
|
||||||
ast,
|
ast,
|
||||||
|
path: codeManager.currentFilePath || undefined,
|
||||||
engineCommandManager: this.engineCommandManager,
|
engineCommandManager: this.engineCommandManager,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -80,6 +80,10 @@ export default class CodeManager {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentFilePath(): string | null {
|
||||||
|
return this._currentFilePath
|
||||||
|
}
|
||||||
|
|
||||||
updateCurrentFilePath(path: string) {
|
updateCurrentFilePath(path: string) {
|
||||||
this._currentFilePath = path
|
this._currentFilePath = path
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
|
import { getNodeFromPath } from './queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import {
|
import {
|
||||||
Identifier,
|
Identifier,
|
||||||
assertParse,
|
assertParse,
|
||||||
|
|||||||
@ -52,27 +52,22 @@ afterAll(async () => {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
describe('Test KCL Samples from public Github repository', () => {
|
||||||
process.chdir('..')
|
describe('when performing enginelessExecutor', () => {
|
||||||
})
|
|
||||||
|
|
||||||
// The tests have to be sequential because we need to change directories
|
|
||||||
// to support `import` working properly.
|
|
||||||
// @ts-expect-error
|
|
||||||
describe.sequential('Test KCL Samples from public Github repository', () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
describe.sequential('when performing enginelessExecutor', () => {
|
|
||||||
manifest.forEach((file: KclSampleFile) => {
|
manifest.forEach((file: KclSampleFile) => {
|
||||||
// @ts-expect-error
|
it(
|
||||||
it.sequential(
|
|
||||||
`should execute ${file.title} (${file.file}) successfully`,
|
`should execute ${file.title} (${file.file}) successfully`,
|
||||||
async () => {
|
async () => {
|
||||||
const [dirProject, fileKcl] =
|
const code = await fs.readFile(
|
||||||
file.pathFromProjectDirectoryToFirstFile.split('/')
|
file.pathFromProjectDirectoryToFirstFile,
|
||||||
process.chdir(dirProject)
|
'utf-8'
|
||||||
const code = await fs.readFile(fileKcl, 'utf-8')
|
)
|
||||||
const ast = assertParse(code)
|
const ast = assertParse(code)
|
||||||
await enginelessExecutor(ast, programMemoryInit())
|
await enginelessExecutor(
|
||||||
|
ast,
|
||||||
|
programMemoryInit(),
|
||||||
|
file.pathFromProjectDirectoryToFirstFile
|
||||||
|
)
|
||||||
},
|
},
|
||||||
files.length * 1000
|
files.length * 1000
|
||||||
)
|
)
|
||||||
|
|||||||
@ -46,12 +46,14 @@ export const toolTips: Array<ToolTip> = [
|
|||||||
|
|
||||||
export async function executeAst({
|
export async function executeAst({
|
||||||
ast,
|
ast,
|
||||||
|
path,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
// If you set programMemoryOverride we assume you mean mock mode. Since that
|
// If you set programMemoryOverride we assume you mean mock mode. Since that
|
||||||
// is the only way to go about it.
|
// is the only way to go about it.
|
||||||
programMemoryOverride,
|
programMemoryOverride,
|
||||||
}: {
|
}: {
|
||||||
ast: Node<Program>
|
ast: Node<Program>
|
||||||
|
path?: string
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
programMemoryOverride?: ProgramMemory
|
programMemoryOverride?: ProgramMemory
|
||||||
isInterrupted?: boolean
|
isInterrupted?: boolean
|
||||||
@ -63,8 +65,8 @@ export async function executeAst({
|
|||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const execState = await (programMemoryOverride
|
const execState = await (programMemoryOverride
|
||||||
? enginelessExecutor(ast, programMemoryOverride)
|
? enginelessExecutor(ast, programMemoryOverride, path)
|
||||||
: executor(ast, engineCommandManager))
|
: executor(ast, engineCommandManager, path))
|
||||||
|
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands()
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,8 @@ import {
|
|||||||
deleteFromSelection,
|
deleteFromSelection,
|
||||||
} from './modifyAst'
|
} from './modifyAst'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst'
|
import { findUsesOfTagInPipe } from './queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { SimplifiedArgDetails } from './std/stdTypes'
|
import { SimplifiedArgDetails } from './std/stdTypes'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
|||||||
@ -26,10 +26,10 @@ import {
|
|||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
findAllPreviousVariablesPath,
|
findAllPreviousVariablesPath,
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
getNodePathFromSourceRange,
|
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
traverse,
|
traverse,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
|
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
|
||||||
import {
|
import {
|
||||||
PathToNodeMap,
|
PathToNodeMap,
|
||||||
|
|||||||
@ -21,7 +21,8 @@ import {
|
|||||||
ChamferParameters,
|
ChamferParameters,
|
||||||
EdgeTreatmentParameters,
|
EdgeTreatmentParameters,
|
||||||
} from './addEdgeTreatment'
|
} from './addEdgeTreatment'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
import { getNodeFromPath } from '../queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { createLiteral } from 'lang/modifyAst'
|
import { createLiteral } from 'lang/modifyAst'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { Selection, Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
|
|||||||
@ -20,10 +20,10 @@ import {
|
|||||||
} from '../modifyAst'
|
} from '../modifyAst'
|
||||||
import {
|
import {
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
getNodePathFromSourceRange,
|
|
||||||
hasSketchPipeBeenExtruded,
|
hasSketchPipeBeenExtruded,
|
||||||
traverse,
|
traverse,
|
||||||
} from '../queryAst'
|
} from '../queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import {
|
import {
|
||||||
addTagForSketchOnFace,
|
addTagForSketchOnFace,
|
||||||
getTagFromCallExpression,
|
getTagFromCallExpression,
|
||||||
|
|||||||
@ -19,7 +19,8 @@ import {
|
|||||||
findUniqueName,
|
findUniqueName,
|
||||||
createVariableDeclaration,
|
createVariableDeclaration,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import {
|
import {
|
||||||
mutateAstWithTagForSketchSegment,
|
mutateAstWithTagForSketchSegment,
|
||||||
getEdgeTagCall,
|
getEdgeTagCall,
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import {
|
|||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
isTypeInValue,
|
isTypeInValue,
|
||||||
getNodePathFromSourceRange,
|
|
||||||
hasExtrudeSketch,
|
hasExtrudeSketch,
|
||||||
findUsesOfTagInPipe,
|
findUsesOfTagInPipe,
|
||||||
hasSketchPipeBeenExtruded,
|
hasSketchPipeBeenExtruded,
|
||||||
@ -19,6 +18,7 @@ import {
|
|||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
doesSceneHaveExtrudedSketch,
|
doesSceneHaveExtrudedSketch,
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import {
|
import {
|
||||||
createArrayExpression,
|
createArrayExpression,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
} from './wasm'
|
} from './wasm'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||||
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
||||||
import { getAngle } from '../lib/utils'
|
import { getAngle } from '../lib/utils'
|
||||||
@ -125,311 +126,6 @@ export function getNodeFromPathCurry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moreNodePathFromSourceRange(
|
|
||||||
node: Node<
|
|
||||||
| Expr
|
|
||||||
| ImportStatement
|
|
||||||
| ExpressionStatement
|
|
||||||
| VariableDeclaration
|
|
||||||
| ReturnStatement
|
|
||||||
>,
|
|
||||||
sourceRange: SourceRange,
|
|
||||||
previousPath: PathToNode = [['body', '']]
|
|
||||||
): PathToNode {
|
|
||||||
const [start, end] = sourceRange
|
|
||||||
let path: PathToNode = [...previousPath]
|
|
||||||
const _node = { ...node }
|
|
||||||
|
|
||||||
if (start < _node.start || end > _node.end) return path
|
|
||||||
|
|
||||||
const isInRange = _node.start <= start && _node.end >= end
|
|
||||||
|
|
||||||
if (
|
|
||||||
(_node.type === 'Identifier' ||
|
|
||||||
_node.type === 'Literal' ||
|
|
||||||
_node.type === 'TagDeclarator') &&
|
|
||||||
isInRange
|
|
||||||
) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_node.type === 'CallExpression' && isInRange) {
|
|
||||||
const { callee, arguments: args } = _node
|
|
||||||
if (
|
|
||||||
callee.type === 'Identifier' &&
|
|
||||||
callee.start <= start &&
|
|
||||||
callee.end >= end
|
|
||||||
) {
|
|
||||||
path.push(['callee', 'CallExpression'])
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (args.length > 0) {
|
|
||||||
for (let argIndex = 0; argIndex < args.length; argIndex++) {
|
|
||||||
const arg = args[argIndex]
|
|
||||||
if (arg.start <= start && arg.end >= end) {
|
|
||||||
path.push(['arguments', 'CallExpression'])
|
|
||||||
path.push([argIndex, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(arg, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_node.type === 'CallExpressionKw' && isInRange) {
|
|
||||||
const { callee, arguments: args } = _node
|
|
||||||
if (
|
|
||||||
callee.type === 'Identifier' &&
|
|
||||||
callee.start <= start &&
|
|
||||||
callee.end >= end
|
|
||||||
) {
|
|
||||||
path.push(['callee', 'CallExpressionKw'])
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (args.length > 0) {
|
|
||||||
for (let argIndex = 0; argIndex < args.length; argIndex++) {
|
|
||||||
const arg = args[argIndex].arg
|
|
||||||
if (arg.start <= start && arg.end >= end) {
|
|
||||||
path.push(['arguments', 'CallExpressionKw'])
|
|
||||||
path.push([argIndex, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(arg, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_node.type === 'BinaryExpression' && isInRange) {
|
|
||||||
const { left, right } = _node
|
|
||||||
if (left.start <= start && left.end >= end) {
|
|
||||||
path.push(['left', 'BinaryExpression'])
|
|
||||||
return moreNodePathFromSourceRange(left, sourceRange, path)
|
|
||||||
}
|
|
||||||
if (right.start <= start && right.end >= end) {
|
|
||||||
path.push(['right', 'BinaryExpression'])
|
|
||||||
return moreNodePathFromSourceRange(right, sourceRange, path)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (_node.type === 'PipeExpression' && isInRange) {
|
|
||||||
const { body } = _node
|
|
||||||
for (let i = 0; i < body.length; i++) {
|
|
||||||
const pipe = body[i]
|
|
||||||
if (pipe.start <= start && pipe.end >= end) {
|
|
||||||
path.push(['body', 'PipeExpression'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(pipe, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (_node.type === 'ArrayExpression' && isInRange) {
|
|
||||||
const { elements } = _node
|
|
||||||
for (let elIndex = 0; elIndex < elements.length; elIndex++) {
|
|
||||||
const element = elements[elIndex]
|
|
||||||
if (element.start <= start && element.end >= end) {
|
|
||||||
path.push(['elements', 'ArrayExpression'])
|
|
||||||
path.push([elIndex, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(element, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (_node.type === 'ObjectExpression' && isInRange) {
|
|
||||||
const { properties } = _node
|
|
||||||
for (let propIndex = 0; propIndex < properties.length; propIndex++) {
|
|
||||||
const property = properties[propIndex]
|
|
||||||
if (property.start <= start && property.end >= end) {
|
|
||||||
path.push(['properties', 'ObjectExpression'])
|
|
||||||
path.push([propIndex, 'index'])
|
|
||||||
if (property.key.start <= start && property.key.end >= end) {
|
|
||||||
path.push(['key', 'Property'])
|
|
||||||
return moreNodePathFromSourceRange(property.key, sourceRange, path)
|
|
||||||
}
|
|
||||||
if (property.value.start <= start && property.value.end >= end) {
|
|
||||||
path.push(['value', 'Property'])
|
|
||||||
return moreNodePathFromSourceRange(property.value, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (_node.type === 'ExpressionStatement' && isInRange) {
|
|
||||||
const { expression } = _node
|
|
||||||
path.push(['expression', 'ExpressionStatement'])
|
|
||||||
return moreNodePathFromSourceRange(expression, sourceRange, path)
|
|
||||||
}
|
|
||||||
if (_node.type === 'VariableDeclaration' && isInRange) {
|
|
||||||
const declaration = _node.declaration
|
|
||||||
|
|
||||||
if (declaration.start <= start && declaration.end >= end) {
|
|
||||||
path.push(['declaration', 'VariableDeclaration'])
|
|
||||||
const init = declaration.init
|
|
||||||
if (init.start <= start && init.end >= end) {
|
|
||||||
path.push(['init', ''])
|
|
||||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_node.type === 'VariableDeclaration' && isInRange) {
|
|
||||||
const declaration = _node.declaration
|
|
||||||
|
|
||||||
if (declaration.start <= start && declaration.end >= end) {
|
|
||||||
const init = declaration.init
|
|
||||||
if (init.start <= start && init.end >= end) {
|
|
||||||
path.push(['declaration', 'VariableDeclaration'])
|
|
||||||
path.push(['init', ''])
|
|
||||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (_node.type === 'UnaryExpression' && isInRange) {
|
|
||||||
const { argument } = _node
|
|
||||||
if (argument.start <= start && argument.end >= end) {
|
|
||||||
path.push(['argument', 'UnaryExpression'])
|
|
||||||
return moreNodePathFromSourceRange(argument, sourceRange, path)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (_node.type === 'FunctionExpression' && isInRange) {
|
|
||||||
for (let i = 0; i < _node.params.length; i++) {
|
|
||||||
const param = _node.params[i]
|
|
||||||
if (param.identifier.start <= start && param.identifier.end >= end) {
|
|
||||||
path.push(['params', 'FunctionExpression'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(param.identifier, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_node.body.start <= start && _node.body.end >= end) {
|
|
||||||
path.push(['body', 'FunctionExpression'])
|
|
||||||
const fnBody = _node.body.body
|
|
||||||
for (let i = 0; i < fnBody.length; i++) {
|
|
||||||
const statement = fnBody[i]
|
|
||||||
if (statement.start <= start && statement.end >= end) {
|
|
||||||
path.push(['body', 'FunctionExpression'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(statement, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (_node.type === 'ReturnStatement' && isInRange) {
|
|
||||||
const { argument } = _node
|
|
||||||
if (argument.start <= start && argument.end >= end) {
|
|
||||||
path.push(['argument', 'ReturnStatement'])
|
|
||||||
return moreNodePathFromSourceRange(argument, sourceRange, path)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (_node.type === 'MemberExpression' && isInRange) {
|
|
||||||
const { object, property } = _node
|
|
||||||
if (object.start <= start && object.end >= end) {
|
|
||||||
path.push(['object', 'MemberExpression'])
|
|
||||||
return moreNodePathFromSourceRange(object, sourceRange, path)
|
|
||||||
}
|
|
||||||
if (property.start <= start && property.end >= end) {
|
|
||||||
path.push(['property', 'MemberExpression'])
|
|
||||||
return moreNodePathFromSourceRange(property, sourceRange, path)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_node.type === 'PipeSubstitution' && isInRange) return path
|
|
||||||
|
|
||||||
if (_node.type === 'IfExpression' && isInRange) {
|
|
||||||
const { cond, then_val, else_ifs, final_else } = _node
|
|
||||||
if (cond.start <= start && cond.end >= end) {
|
|
||||||
path.push(['cond', 'IfExpression'])
|
|
||||||
return moreNodePathFromSourceRange(cond, sourceRange, path)
|
|
||||||
}
|
|
||||||
if (then_val.start <= start && then_val.end >= end) {
|
|
||||||
path.push(['then_val', 'IfExpression'])
|
|
||||||
path.push(['body', 'IfExpression'])
|
|
||||||
return getNodePathFromSourceRange(then_val, sourceRange, path)
|
|
||||||
}
|
|
||||||
for (let i = 0; i < else_ifs.length; i++) {
|
|
||||||
const else_if = else_ifs[i]
|
|
||||||
if (else_if.start <= start && else_if.end >= end) {
|
|
||||||
path.push(['else_ifs', 'IfExpression'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
const { cond, then_val } = else_if
|
|
||||||
if (cond.start <= start && cond.end >= end) {
|
|
||||||
path.push(['cond', 'IfExpression'])
|
|
||||||
return moreNodePathFromSourceRange(cond, sourceRange, path)
|
|
||||||
}
|
|
||||||
path.push(['then_val', 'IfExpression'])
|
|
||||||
path.push(['body', 'IfExpression'])
|
|
||||||
return getNodePathFromSourceRange(then_val, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (final_else.start <= start && final_else.end >= end) {
|
|
||||||
path.push(['final_else', 'IfExpression'])
|
|
||||||
path.push(['body', 'IfExpression'])
|
|
||||||
return getNodePathFromSourceRange(final_else, sourceRange, path)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_node.type === 'ImportStatement' && isInRange) {
|
|
||||||
if (_node.selector && _node.selector.type === 'List') {
|
|
||||||
path.push(['selector', 'ImportStatement'])
|
|
||||||
const { items } = _node.selector
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i]
|
|
||||||
if (item.start <= start && item.end >= end) {
|
|
||||||
path.push(['items', 'ImportSelector'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
if (item.name.start <= start && item.name.end >= end) {
|
|
||||||
path.push(['name', 'ImportItem'])
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
item.alias &&
|
|
||||||
item.alias.start <= start &&
|
|
||||||
item.alias.end >= end
|
|
||||||
) {
|
|
||||||
path.push(['alias', 'ImportItem'])
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('not implemented: ' + node.type)
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getNodePathFromSourceRange(
|
|
||||||
node: Program,
|
|
||||||
sourceRange: SourceRange,
|
|
||||||
previousPath: PathToNode = [['body', '']]
|
|
||||||
): PathToNode {
|
|
||||||
const [start, end] = sourceRange || []
|
|
||||||
let path: PathToNode = [...previousPath]
|
|
||||||
const _node = { ...node }
|
|
||||||
|
|
||||||
// loop over each statement in body getting the index with a for loop
|
|
||||||
for (
|
|
||||||
let statementIndex = 0;
|
|
||||||
statementIndex < _node.body.length;
|
|
||||||
statementIndex++
|
|
||||||
) {
|
|
||||||
const statement = _node.body[statementIndex]
|
|
||||||
if (statement.start <= start && statement.end >= end) {
|
|
||||||
path.push([statementIndex, 'index'])
|
|
||||||
return moreNodePathFromSourceRange(statement, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
type KCLNode = Node<
|
type KCLNode = Node<
|
||||||
| Expr
|
| Expr
|
||||||
| ExpressionStatement
|
| ExpressionStatement
|
||||||
|
|||||||
316
src/lang/queryAstNodePathUtils.ts
Normal file
316
src/lang/queryAstNodePathUtils.ts
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
import {
|
||||||
|
Expr,
|
||||||
|
ExpressionStatement,
|
||||||
|
VariableDeclaration,
|
||||||
|
ReturnStatement,
|
||||||
|
SourceRange,
|
||||||
|
PathToNode,
|
||||||
|
Program,
|
||||||
|
} from './wasm'
|
||||||
|
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
|
||||||
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
|
||||||
|
function moreNodePathFromSourceRange(
|
||||||
|
node: Node<
|
||||||
|
| Expr
|
||||||
|
| ImportStatement
|
||||||
|
| ExpressionStatement
|
||||||
|
| VariableDeclaration
|
||||||
|
| ReturnStatement
|
||||||
|
>,
|
||||||
|
sourceRange: SourceRange,
|
||||||
|
previousPath: PathToNode = [['body', '']]
|
||||||
|
): PathToNode {
|
||||||
|
const [start, end] = sourceRange
|
||||||
|
let path: PathToNode = [...previousPath]
|
||||||
|
const _node = { ...node }
|
||||||
|
|
||||||
|
if (start < _node.start || end > _node.end) return path
|
||||||
|
|
||||||
|
const isInRange = _node.start <= start && _node.end >= end
|
||||||
|
|
||||||
|
if (
|
||||||
|
(_node.type === 'Identifier' ||
|
||||||
|
_node.type === 'Literal' ||
|
||||||
|
_node.type === 'TagDeclarator') &&
|
||||||
|
isInRange
|
||||||
|
) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_node.type === 'CallExpression' && isInRange) {
|
||||||
|
const { callee, arguments: args } = _node
|
||||||
|
if (
|
||||||
|
callee.type === 'Identifier' &&
|
||||||
|
callee.start <= start &&
|
||||||
|
callee.end >= end
|
||||||
|
) {
|
||||||
|
path.push(['callee', 'CallExpression'])
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (args.length > 0) {
|
||||||
|
for (let argIndex = 0; argIndex < args.length; argIndex++) {
|
||||||
|
const arg = args[argIndex]
|
||||||
|
if (arg.start <= start && arg.end >= end) {
|
||||||
|
path.push(['arguments', 'CallExpression'])
|
||||||
|
path.push([argIndex, 'index'])
|
||||||
|
return moreNodePathFromSourceRange(arg, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_node.type === 'CallExpressionKw' && isInRange) {
|
||||||
|
const { callee, arguments: args } = _node
|
||||||
|
if (
|
||||||
|
callee.type === 'Identifier' &&
|
||||||
|
callee.start <= start &&
|
||||||
|
callee.end >= end
|
||||||
|
) {
|
||||||
|
path.push(['callee', 'CallExpressionKw'])
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (args.length > 0) {
|
||||||
|
for (let argIndex = 0; argIndex < args.length; argIndex++) {
|
||||||
|
const arg = args[argIndex].arg
|
||||||
|
if (arg.start <= start && arg.end >= end) {
|
||||||
|
path.push(['arguments', 'CallExpressionKw'])
|
||||||
|
path.push([argIndex, 'index'])
|
||||||
|
return moreNodePathFromSourceRange(arg, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_node.type === 'BinaryExpression' && isInRange) {
|
||||||
|
const { left, right } = _node
|
||||||
|
if (left.start <= start && left.end >= end) {
|
||||||
|
path.push(['left', 'BinaryExpression'])
|
||||||
|
return moreNodePathFromSourceRange(left, sourceRange, path)
|
||||||
|
}
|
||||||
|
if (right.start <= start && right.end >= end) {
|
||||||
|
path.push(['right', 'BinaryExpression'])
|
||||||
|
return moreNodePathFromSourceRange(right, sourceRange, path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (_node.type === 'PipeExpression' && isInRange) {
|
||||||
|
const { body } = _node
|
||||||
|
for (let i = 0; i < body.length; i++) {
|
||||||
|
const pipe = body[i]
|
||||||
|
if (pipe.start <= start && pipe.end >= end) {
|
||||||
|
path.push(['body', 'PipeExpression'])
|
||||||
|
path.push([i, 'index'])
|
||||||
|
return moreNodePathFromSourceRange(pipe, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (_node.type === 'ArrayExpression' && isInRange) {
|
||||||
|
const { elements } = _node
|
||||||
|
for (let elIndex = 0; elIndex < elements.length; elIndex++) {
|
||||||
|
const element = elements[elIndex]
|
||||||
|
if (element.start <= start && element.end >= end) {
|
||||||
|
path.push(['elements', 'ArrayExpression'])
|
||||||
|
path.push([elIndex, 'index'])
|
||||||
|
return moreNodePathFromSourceRange(element, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (_node.type === 'ObjectExpression' && isInRange) {
|
||||||
|
const { properties } = _node
|
||||||
|
for (let propIndex = 0; propIndex < properties.length; propIndex++) {
|
||||||
|
const property = properties[propIndex]
|
||||||
|
if (property.start <= start && property.end >= end) {
|
||||||
|
path.push(['properties', 'ObjectExpression'])
|
||||||
|
path.push([propIndex, 'index'])
|
||||||
|
if (property.key.start <= start && property.key.end >= end) {
|
||||||
|
path.push(['key', 'Property'])
|
||||||
|
return moreNodePathFromSourceRange(property.key, sourceRange, path)
|
||||||
|
}
|
||||||
|
if (property.value.start <= start && property.value.end >= end) {
|
||||||
|
path.push(['value', 'Property'])
|
||||||
|
return moreNodePathFromSourceRange(property.value, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (_node.type === 'ExpressionStatement' && isInRange) {
|
||||||
|
const { expression } = _node
|
||||||
|
path.push(['expression', 'ExpressionStatement'])
|
||||||
|
return moreNodePathFromSourceRange(expression, sourceRange, path)
|
||||||
|
}
|
||||||
|
if (_node.type === 'VariableDeclaration' && isInRange) {
|
||||||
|
const declaration = _node.declaration
|
||||||
|
|
||||||
|
if (declaration.start <= start && declaration.end >= end) {
|
||||||
|
path.push(['declaration', 'VariableDeclaration'])
|
||||||
|
const init = declaration.init
|
||||||
|
if (init.start <= start && init.end >= end) {
|
||||||
|
path.push(['init', ''])
|
||||||
|
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_node.type === 'VariableDeclaration' && isInRange) {
|
||||||
|
const declaration = _node.declaration
|
||||||
|
|
||||||
|
if (declaration.start <= start && declaration.end >= end) {
|
||||||
|
const init = declaration.init
|
||||||
|
if (init.start <= start && init.end >= end) {
|
||||||
|
path.push(['declaration', 'VariableDeclaration'])
|
||||||
|
path.push(['init', ''])
|
||||||
|
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (_node.type === 'UnaryExpression' && isInRange) {
|
||||||
|
const { argument } = _node
|
||||||
|
if (argument.start <= start && argument.end >= end) {
|
||||||
|
path.push(['argument', 'UnaryExpression'])
|
||||||
|
return moreNodePathFromSourceRange(argument, sourceRange, path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (_node.type === 'FunctionExpression' && isInRange) {
|
||||||
|
for (let i = 0; i < _node.params.length; i++) {
|
||||||
|
const param = _node.params[i]
|
||||||
|
if (param.identifier.start <= start && param.identifier.end >= end) {
|
||||||
|
path.push(['params', 'FunctionExpression'])
|
||||||
|
path.push([i, 'index'])
|
||||||
|
return moreNodePathFromSourceRange(param.identifier, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_node.body.start <= start && _node.body.end >= end) {
|
||||||
|
path.push(['body', 'FunctionExpression'])
|
||||||
|
const fnBody = _node.body.body
|
||||||
|
for (let i = 0; i < fnBody.length; i++) {
|
||||||
|
const statement = fnBody[i]
|
||||||
|
if (statement.start <= start && statement.end >= end) {
|
||||||
|
path.push(['body', 'FunctionExpression'])
|
||||||
|
path.push([i, 'index'])
|
||||||
|
return moreNodePathFromSourceRange(statement, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (_node.type === 'ReturnStatement' && isInRange) {
|
||||||
|
const { argument } = _node
|
||||||
|
if (argument.start <= start && argument.end >= end) {
|
||||||
|
path.push(['argument', 'ReturnStatement'])
|
||||||
|
return moreNodePathFromSourceRange(argument, sourceRange, path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (_node.type === 'MemberExpression' && isInRange) {
|
||||||
|
const { object, property } = _node
|
||||||
|
if (object.start <= start && object.end >= end) {
|
||||||
|
path.push(['object', 'MemberExpression'])
|
||||||
|
return moreNodePathFromSourceRange(object, sourceRange, path)
|
||||||
|
}
|
||||||
|
if (property.start <= start && property.end >= end) {
|
||||||
|
path.push(['property', 'MemberExpression'])
|
||||||
|
return moreNodePathFromSourceRange(property, sourceRange, path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_node.type === 'PipeSubstitution' && isInRange) return path
|
||||||
|
|
||||||
|
if (_node.type === 'IfExpression' && isInRange) {
|
||||||
|
const { cond, then_val, else_ifs, final_else } = _node
|
||||||
|
if (cond.start <= start && cond.end >= end) {
|
||||||
|
path.push(['cond', 'IfExpression'])
|
||||||
|
return moreNodePathFromSourceRange(cond, sourceRange, path)
|
||||||
|
}
|
||||||
|
if (then_val.start <= start && then_val.end >= end) {
|
||||||
|
path.push(['then_val', 'IfExpression'])
|
||||||
|
path.push(['body', 'IfExpression'])
|
||||||
|
return getNodePathFromSourceRange(then_val, sourceRange, path)
|
||||||
|
}
|
||||||
|
for (let i = 0; i < else_ifs.length; i++) {
|
||||||
|
const else_if = else_ifs[i]
|
||||||
|
if (else_if.start <= start && else_if.end >= end) {
|
||||||
|
path.push(['else_ifs', 'IfExpression'])
|
||||||
|
path.push([i, 'index'])
|
||||||
|
const { cond, then_val } = else_if
|
||||||
|
if (cond.start <= start && cond.end >= end) {
|
||||||
|
path.push(['cond', 'IfExpression'])
|
||||||
|
return moreNodePathFromSourceRange(cond, sourceRange, path)
|
||||||
|
}
|
||||||
|
path.push(['then_val', 'IfExpression'])
|
||||||
|
path.push(['body', 'IfExpression'])
|
||||||
|
return getNodePathFromSourceRange(then_val, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (final_else.start <= start && final_else.end >= end) {
|
||||||
|
path.push(['final_else', 'IfExpression'])
|
||||||
|
path.push(['body', 'IfExpression'])
|
||||||
|
return getNodePathFromSourceRange(final_else, sourceRange, path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_node.type === 'ImportStatement' && isInRange) {
|
||||||
|
if (_node.selector && _node.selector.type === 'List') {
|
||||||
|
path.push(['selector', 'ImportStatement'])
|
||||||
|
const { items } = _node.selector
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i]
|
||||||
|
if (item.start <= start && item.end >= end) {
|
||||||
|
path.push(['items', 'ImportSelector'])
|
||||||
|
path.push([i, 'index'])
|
||||||
|
if (item.name.start <= start && item.name.end >= end) {
|
||||||
|
path.push(['name', 'ImportItem'])
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
item.alias &&
|
||||||
|
item.alias.start <= start &&
|
||||||
|
item.alias.end >= end
|
||||||
|
) {
|
||||||
|
path.push(['alias', 'ImportItem'])
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('not implemented: ' + node.type)
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNodePathFromSourceRange(
|
||||||
|
node: Program,
|
||||||
|
sourceRange: SourceRange,
|
||||||
|
previousPath: PathToNode = [['body', '']]
|
||||||
|
): PathToNode {
|
||||||
|
const [start, end] = sourceRange || []
|
||||||
|
let path: PathToNode = [...previousPath]
|
||||||
|
const _node = { ...node }
|
||||||
|
|
||||||
|
// loop over each statement in body getting the index with a for loop
|
||||||
|
for (
|
||||||
|
let statementIndex = 0;
|
||||||
|
statementIndex < _node.body.length;
|
||||||
|
statementIndex++
|
||||||
|
) {
|
||||||
|
const statement = _node.body[statementIndex]
|
||||||
|
if (statement.start <= start && statement.end >= end) {
|
||||||
|
path.push([statementIndex, 'index'])
|
||||||
|
return moreNodePathFromSourceRange(statement, sourceRange, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
EdgeCut,
|
EdgeCut,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
|
||||||
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
|
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
|
||||||
|
|||||||
@ -31,6 +31,9 @@ class FileSystemManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async join(dir: string, path: string): Promise<string> {
|
async join(dir: string, path: string): Promise<string> {
|
||||||
|
if (path.startsWith(dir)) {
|
||||||
|
path = path.slice(dir.length)
|
||||||
|
}
|
||||||
return Promise.resolve(window.electron.path.join(dir, path))
|
return Promise.resolve(window.electron.path.join(dir, path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,8 @@ import {
|
|||||||
CallExpression,
|
CallExpression,
|
||||||
topLevelRange,
|
topLevelRange,
|
||||||
} from '../wasm'
|
} from '../wasm'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
import { getNodeFromPath } from '../queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
|||||||
@ -17,9 +17,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
getNodeFromPathCurry,
|
getNodeFromPathCurry,
|
||||||
getNodePathFromSourceRange,
|
|
||||||
getObjExprProperty,
|
getObjExprProperty,
|
||||||
} from 'lang/queryAst'
|
} from 'lang/queryAst'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
import {
|
import {
|
||||||
isLiteralArrayOrStatic,
|
isLiteralArrayOrStatic,
|
||||||
isNotLiteralArrayOrStatic,
|
isNotLiteralArrayOrStatic,
|
||||||
|
|||||||
@ -22,11 +22,8 @@ import {
|
|||||||
SourceRange,
|
SourceRange,
|
||||||
LiteralValue,
|
LiteralValue,
|
||||||
} from '../wasm'
|
} from '../wasm'
|
||||||
import {
|
import { getNodeFromPath, getNodeFromPathCurry } from '../queryAst'
|
||||||
getNodeFromPath,
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
getNodeFromPathCurry,
|
|
||||||
getNodePathFromSourceRange,
|
|
||||||
} from '../queryAst'
|
|
||||||
import {
|
import {
|
||||||
createArrayExpression,
|
createArrayExpression,
|
||||||
createBinaryExpression,
|
createBinaryExpression,
|
||||||
|
|||||||
@ -53,7 +53,7 @@ import { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
|
|||||||
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
||||||
import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact'
|
import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact'
|
||||||
import { Artifact } from './std/artifactGraph'
|
import { Artifact } from './std/artifactGraph'
|
||||||
import { getNodePathFromSourceRange } from './queryAst'
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
|
|
||||||
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
|
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||||
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
||||||
@ -566,9 +566,19 @@ export function sketchFromKclValue(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a KCL program.
|
||||||
|
* @param node The AST of the program to execute.
|
||||||
|
* @param path The full path of the file being executed. Use `null` for
|
||||||
|
* expressions that don't have a file, like expressions in the command bar.
|
||||||
|
* @param programMemoryOverride If this is not `null`, this will be used as the
|
||||||
|
* initial program memory, and the execution will be engineless (AKA mock
|
||||||
|
* execution).
|
||||||
|
*/
|
||||||
export const executor = async (
|
export const executor = async (
|
||||||
node: Node<Program>,
|
node: Node<Program>,
|
||||||
engineCommandManager: EngineCommandManager,
|
engineCommandManager: EngineCommandManager,
|
||||||
|
path?: string,
|
||||||
programMemoryOverride: ProgramMemory | Error | null = null
|
programMemoryOverride: ProgramMemory | Error | null = null
|
||||||
): Promise<ExecState> => {
|
): Promise<ExecState> => {
|
||||||
if (programMemoryOverride !== null && err(programMemoryOverride))
|
if (programMemoryOverride !== null && err(programMemoryOverride))
|
||||||
@ -590,6 +600,7 @@ export const executor = async (
|
|||||||
}
|
}
|
||||||
const execOutcome: RustExecOutcome = await execute(
|
const execOutcome: RustExecOutcome = await execute(
|
||||||
JSON.stringify(node),
|
JSON.stringify(node),
|
||||||
|
path,
|
||||||
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
JSON.stringify(programMemoryOverride?.toRaw() || null),
|
||||||
JSON.stringify({ settings: jsAppSettings }),
|
JSON.stringify({ settings: jsAppSettings }),
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
|
|||||||
40
src/lib/base64.test.ts
Normal file
40
src/lib/base64.test.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { expect } from 'vitest'
|
||||||
|
import { base64ToString, stringToBase64 } from './base64'
|
||||||
|
|
||||||
|
describe('base64 encoding', () => {
|
||||||
|
test('to base64, simple code', async () => {
|
||||||
|
const code = `extrusionDistance = 12`
|
||||||
|
// Generated by online tool
|
||||||
|
const expectedBase64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
|
||||||
|
|
||||||
|
const base64 = stringToBase64(code)
|
||||||
|
expect(base64).toBe(expectedBase64)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`to base64, code with UTF-8 characters`, async () => {
|
||||||
|
// example adapted from MDN docs: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
||||||
|
const code = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
|
||||||
|
// Generated by online tool
|
||||||
|
const expectedBase64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
|
||||||
|
|
||||||
|
const base64 = stringToBase64(code)
|
||||||
|
expect(base64).toBe(expectedBase64)
|
||||||
|
})
|
||||||
|
|
||||||
|
// The following are simply the reverse of the above tests
|
||||||
|
test('from base64, simple code', async () => {
|
||||||
|
const base64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
|
||||||
|
const expectedCode = `extrusionDistance = 12`
|
||||||
|
|
||||||
|
const code = base64ToString(base64)
|
||||||
|
expect(code).toBe(expectedCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`from base64, code with UTF-8 characters`, async () => {
|
||||||
|
const base64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
|
||||||
|
const expectedCode = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
|
||||||
|
|
||||||
|
const code = base64ToString(base64)
|
||||||
|
expect(code).toBe(expectedCode)
|
||||||
|
})
|
||||||
|
})
|
||||||
29
src/lib/base64.ts
Normal file
29
src/lib/base64.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Converts a string to a base64 string, preserving the UTF-8 encoding
|
||||||
|
*/
|
||||||
|
export function stringToBase64(str: string) {
|
||||||
|
return bytesToBase64(new TextEncoder().encode(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a base64 string to a string, preserving the UTF-8 encoding
|
||||||
|
*/
|
||||||
|
export function base64ToString(base64: string) {
|
||||||
|
return new TextDecoder().decode(base64ToBytes(base64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From the MDN Web Docs
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
||||||
|
*/
|
||||||
|
function base64ToBytes(base64: string) {
|
||||||
|
const binString = atob(base64)
|
||||||
|
return Uint8Array.from(binString, (m) => m.codePointAt(0)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToBase64(bytes: Uint8Array) {
|
||||||
|
const binString = Array.from(bytes, (byte) =>
|
||||||
|
String.fromCodePoint(byte)
|
||||||
|
).join('')
|
||||||
|
return btoa(binString)
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
loftValidator,
|
loftValidator,
|
||||||
revolveAxisValidator,
|
revolveAxisValidator,
|
||||||
shellValidator,
|
shellValidator,
|
||||||
|
sweepValidator,
|
||||||
} from './validators'
|
} from './validators'
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
@ -42,8 +43,8 @@ export type ModelingCommandSchema = {
|
|||||||
distance: KclCommandValue
|
distance: KclCommandValue
|
||||||
}
|
}
|
||||||
Sweep: {
|
Sweep: {
|
||||||
path: Selections
|
target: Selections
|
||||||
profile: Selections
|
trajectory: Selections
|
||||||
}
|
}
|
||||||
Loft: {
|
Loft: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
@ -308,25 +309,24 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
'Create a 3D body by moving a sketch region along an arbitrary path.',
|
'Create a 3D body by moving a sketch region along an arbitrary path.',
|
||||||
icon: 'sweep',
|
icon: 'sweep',
|
||||||
status: 'development',
|
status: 'development',
|
||||||
needsReview: true,
|
needsReview: false,
|
||||||
args: {
|
args: {
|
||||||
profile: {
|
target: {
|
||||||
inputType: 'selection',
|
inputType: 'selection',
|
||||||
selectionTypes: ['solid2d'],
|
selectionTypes: ['solid2d'],
|
||||||
required: true,
|
required: true,
|
||||||
skip: true,
|
skip: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
// TODO: add dry-run validation
|
|
||||||
warningMessage:
|
warningMessage:
|
||||||
'The sweep workflow is new and under tested. Please break it and report issues.',
|
'The sweep workflow is new and under tested. Please break it and report issues.',
|
||||||
},
|
},
|
||||||
path: {
|
trajectory: {
|
||||||
inputType: 'selection',
|
inputType: 'selection',
|
||||||
selectionTypes: ['segment', 'path'],
|
selectionTypes: ['segment', 'path'],
|
||||||
required: true,
|
required: true,
|
||||||
skip: true,
|
skip: false,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
// TODO: add dry-run validation
|
validation: sweepValidator,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
||||||
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
|
||||||
import { projectsMachine } from 'machines/projectsMachine'
|
import { projectsMachine } from 'machines/projectsMachine'
|
||||||
|
|
||||||
export type ProjectsCommandSchema = {
|
export type ProjectsCommandSchema = {
|
||||||
@ -17,6 +20,13 @@ export type ProjectsCommandSchema = {
|
|||||||
oldName: string
|
oldName: string
|
||||||
newName: string
|
newName: string
|
||||||
}
|
}
|
||||||
|
'Import file from URL': {
|
||||||
|
name: string
|
||||||
|
code?: string
|
||||||
|
units: UnitLength_type
|
||||||
|
method: 'newProject' | 'existingProject'
|
||||||
|
projectName?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||||
@ -26,6 +36,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
'Open project': {
|
'Open project': {
|
||||||
icon: 'arrowRight',
|
icon: 'arrowRight',
|
||||||
description: 'Open a project',
|
description: 'Open a project',
|
||||||
|
status: isDesktop() ? 'active' : 'inactive',
|
||||||
args: {
|
args: {
|
||||||
name: {
|
name: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
@ -42,6 +53,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
'Create project': {
|
'Create project': {
|
||||||
icon: 'folderPlus',
|
icon: 'folderPlus',
|
||||||
description: 'Create a project',
|
description: 'Create a project',
|
||||||
|
status: isDesktop() ? 'active' : 'inactive',
|
||||||
args: {
|
args: {
|
||||||
name: {
|
name: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
@ -53,6 +65,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
'Delete project': {
|
'Delete project': {
|
||||||
icon: 'close',
|
icon: 'close',
|
||||||
description: 'Delete a project',
|
description: 'Delete a project',
|
||||||
|
status: isDesktop() ? 'active' : 'inactive',
|
||||||
needsReview: true,
|
needsReview: true,
|
||||||
reviewMessage: ({ argumentsToSubmit }) =>
|
reviewMessage: ({ argumentsToSubmit }) =>
|
||||||
CommandBarOverwriteWarning({
|
CommandBarOverwriteWarning({
|
||||||
@ -75,6 +88,7 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
description: 'Rename a project',
|
description: 'Rename a project',
|
||||||
needsReview: true,
|
needsReview: true,
|
||||||
|
status: isDesktop() ? 'active' : 'inactive',
|
||||||
args: {
|
args: {
|
||||||
oldName: {
|
oldName: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
@ -92,4 +106,80 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'Import file from URL': {
|
||||||
|
icon: 'file',
|
||||||
|
description: 'Create a file',
|
||||||
|
needsReview: true,
|
||||||
|
status: 'active',
|
||||||
|
args: {
|
||||||
|
method: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
options: isDesktop()
|
||||||
|
? [
|
||||||
|
{ name: 'New project', value: 'newProject' },
|
||||||
|
{ name: 'Existing project', value: 'existingProject' },
|
||||||
|
]
|
||||||
|
: [{ name: 'Overwrite', value: 'existingProject' }],
|
||||||
|
valueSummary(value) {
|
||||||
|
return isDesktop()
|
||||||
|
? value === 'newProject'
|
||||||
|
? 'New project'
|
||||||
|
: 'Existing project'
|
||||||
|
: 'Overwrite'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// TODO: We can't get the currently-opened project to auto-populate here because
|
||||||
|
// it's not available on projectMachine, but lower in fileMachine. Unify these.
|
||||||
|
projectName: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: (commandsContext) =>
|
||||||
|
isDesktop() &&
|
||||||
|
commandsContext.argumentsToSubmit.method === 'existingProject',
|
||||||
|
skip: true,
|
||||||
|
options: (_, context) =>
|
||||||
|
context?.projects.map((p) => ({
|
||||||
|
name: p.name!,
|
||||||
|
value: p.name!,
|
||||||
|
})) || [],
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: isDesktop(),
|
||||||
|
skip: true,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
inputType: 'text',
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
valueSummary(value) {
|
||||||
|
const lineCount = value?.trim().split('\n').length
|
||||||
|
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
units: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: false,
|
||||||
|
skip: true,
|
||||||
|
options: baseUnitsUnion.map((unit) => ({
|
||||||
|
name: baseUnitLabels[unit],
|
||||||
|
value: unit,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reviewMessage(commandBarContext) {
|
||||||
|
return isDesktop()
|
||||||
|
? `Will add the contents from URL to a new ${
|
||||||
|
commandBarContext.argumentsToSubmit.method === 'newProject'
|
||||||
|
? 'project with file main.kcl'
|
||||||
|
: `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`
|
||||||
|
} named "${
|
||||||
|
commandBarContext.argumentsToSubmit.name
|
||||||
|
}", and set default units to "${
|
||||||
|
commandBarContext.argumentsToSubmit.units
|
||||||
|
}".`
|
||||||
|
: `Will overwrite the contents of the current file with the contents from the URL.`
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -207,3 +207,64 @@ export const shellValidator = async ({
|
|||||||
|
|
||||||
return 'Unable to shell with the provided selection'
|
return 'Unable to shell with the provided selection'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sweepValidator = async ({
|
||||||
|
context,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
context: CommandBarContext
|
||||||
|
data: { trajectory: Selections }
|
||||||
|
}): Promise<boolean | string> => {
|
||||||
|
if (!isSelections(data.trajectory)) {
|
||||||
|
console.log('Unable to sweep, selections are missing')
|
||||||
|
return 'Unable to sweep, selections are missing'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the parent path from the segment selection directly
|
||||||
|
const trajectoryArtifact = data.trajectory.graphSelections[0].artifact
|
||||||
|
if (!trajectoryArtifact) {
|
||||||
|
return "Unable to sweep, couldn't find the trajectory artifact"
|
||||||
|
}
|
||||||
|
if (trajectoryArtifact.type !== 'segment') {
|
||||||
|
return "Unable to sweep, couldn't find the target from a non-segment selection"
|
||||||
|
}
|
||||||
|
const trajectory = trajectoryArtifact.pathId
|
||||||
|
|
||||||
|
// Get the former arg in the command bar flow, and retrieve the path from the solid2d directly
|
||||||
|
const targetArg = context.argumentsToSubmit['target'] as Selections
|
||||||
|
const targetArtifact = targetArg.graphSelections[0].artifact
|
||||||
|
if (!targetArtifact) {
|
||||||
|
return "Unable to sweep, couldn't find the profile artifact"
|
||||||
|
}
|
||||||
|
if (targetArtifact.type !== 'solid2d') {
|
||||||
|
return "Unable to sweep, couldn't find the target from a non-solid2d selection"
|
||||||
|
}
|
||||||
|
const target = targetArtifact.pathId
|
||||||
|
|
||||||
|
const sweepCommand = async () => {
|
||||||
|
// TODO: second look on defaults here
|
||||||
|
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
|
||||||
|
const DEFAULT_SECTIONAL = false
|
||||||
|
const cmdArgs = {
|
||||||
|
target,
|
||||||
|
trajectory,
|
||||||
|
sectional: DEFAULT_SECTIONAL,
|
||||||
|
tolerance: DEFAULT_TOLERANCE,
|
||||||
|
}
|
||||||
|
return await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'sweep',
|
||||||
|
...cmdArgs,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptSweep = await dryRunWrapper(sweepCommand)
|
||||||
|
if (attemptSweep?.success) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unable to sweep with the provided selection'
|
||||||
|
}
|
||||||
|
|||||||
@ -69,6 +69,7 @@ export const KCL_DEFAULT_DEGREE = `360`
|
|||||||
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
||||||
|
|
||||||
export const DEFAULT_HOST = 'https://api.zoo.dev'
|
export const DEFAULT_HOST = 'https://api.zoo.dev'
|
||||||
|
export const PROD_APP_URL = 'https://app.zoo.dev'
|
||||||
export const SETTINGS_FILE_NAME = 'settings.toml'
|
export const SETTINGS_FILE_NAME = 'settings.toml'
|
||||||
export const TOKEN_FILE_NAME = 'token.txt'
|
export const TOKEN_FILE_NAME = 'token.txt'
|
||||||
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||||
@ -110,6 +111,9 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
|
|||||||
localFallback: '/kcl-samples-manifest-fallback.json',
|
localFallback: '/kcl-samples-manifest-fallback.json',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/** URL parameter to create a file */
|
||||||
|
export const CREATE_FILE_URL_PARAM = 'create-file'
|
||||||
|
|
||||||
/** Toast id for the app auto-updater toast */
|
/** Toast id for the app auto-updater toast */
|
||||||
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||||
|
|
||||||
@ -139,3 +143,12 @@ export const VIEW_NAMES_SEMANTIC = {
|
|||||||
} as const
|
} as const
|
||||||
/** The modeling sidebar buttons' IDs get a suffix to prevent collisions */
|
/** The modeling sidebar buttons' IDs get a suffix to prevent collisions */
|
||||||
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
|
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
|
||||||
|
|
||||||
|
/** Custom URL protocol our desktop registers */
|
||||||
|
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio:'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A query parameter that triggers a modal
|
||||||
|
* to "open in desktop app" when present in the URL
|
||||||
|
*/
|
||||||
|
export const ASK_TO_OPEN_QUERY_PARAM = 'ask-open-desktop'
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||||
import { Command, CommandArgumentOption } from './commandTypes'
|
import { Command, CommandArgumentOption } from './commandTypes'
|
||||||
import { kclManager } from './singletons'
|
import { codeManager, kclManager } from './singletons'
|
||||||
import { isDesktop } from './isDesktop'
|
import { isDesktop } from './isDesktop'
|
||||||
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
|
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
|
||||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
import { parseProjectSettings } from 'lang/wasm'
|
import { parseProjectSettings } from 'lang/wasm'
|
||||||
import { err, reportRejection } from './trap'
|
import { err, reportRejection } from './trap'
|
||||||
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
|
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
|
||||||
|
import { copyFileShareLink } from './links'
|
||||||
|
import { IndexLoaderData } from './types'
|
||||||
|
|
||||||
interface OnSubmitProps {
|
interface OnSubmitProps {
|
||||||
sampleName: string
|
sampleName: string
|
||||||
@ -15,10 +17,21 @@ interface OnSubmitProps {
|
|||||||
method: 'overwrite' | 'newFile'
|
method: 'overwrite' | 'newFile'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function kclCommands(
|
interface KclCommandConfig {
|
||||||
onSubmit: (p: OnSubmitProps) => Promise<void>,
|
// TODO: find a different approach that doesn't require
|
||||||
providedOptions: CommandArgumentOption<string>[]
|
// special props for a single command
|
||||||
): Command[] {
|
specialPropsForSampleCommand: {
|
||||||
|
onSubmit: (p: OnSubmitProps) => Promise<void>
|
||||||
|
providedOptions: CommandArgumentOption<string>[]
|
||||||
|
}
|
||||||
|
projectData: IndexLoaderData
|
||||||
|
authToken: string
|
||||||
|
settings: {
|
||||||
|
defaultUnit: UnitLength_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'format-code',
|
name: 'format-code',
|
||||||
@ -107,7 +120,9 @@ export function kclCommands(
|
|||||||
)
|
)
|
||||||
.then((props) => {
|
.then((props) => {
|
||||||
if (props?.code) {
|
if (props?.code) {
|
||||||
onSubmit(props).catch(reportError)
|
commandProps.specialPropsForSampleCommand
|
||||||
|
.onSubmit(props)
|
||||||
|
.catch(reportError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(reportError)
|
.catch(reportError)
|
||||||
@ -149,9 +164,25 @@ export function kclCommands(
|
|||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
options: providedOptions,
|
options: commandProps.specialPropsForSampleCommand.providedOptions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'share-file-link',
|
||||||
|
// displayName: 'Share file',
|
||||||
|
// description: 'Create a link that contains a copy of the current file.',
|
||||||
|
// groupId: 'code',
|
||||||
|
// needsReview: false,
|
||||||
|
// icon: 'link',
|
||||||
|
// onSubmit: () => {
|
||||||
|
// copyFileShareLink({
|
||||||
|
// token: commandProps.authToken,
|
||||||
|
// code: codeManager.code,
|
||||||
|
// name: commandProps.projectData.project?.name || '',
|
||||||
|
// units: commandProps.settings.defaultUnit,
|
||||||
|
// }).catch(reportRejection)
|
||||||
|
// },
|
||||||
|
// },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/lib/links.test.ts
Normal file
16
src/lib/links.test.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createCreateFileUrl } from './links'
|
||||||
|
|
||||||
|
describe(`link creation tests`, () => {
|
||||||
|
test(`createCreateFileUrl happy path`, async () => {
|
||||||
|
const code = `extrusionDistance = 12`
|
||||||
|
const name = `test`
|
||||||
|
const units = `mm`
|
||||||
|
|
||||||
|
// Converted with external online tools
|
||||||
|
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
|
||||||
|
const expectedLink = `http://localhost:3000/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
|
||||||
|
|
||||||
|
const result = createCreateFileUrl({ code, name, units })
|
||||||
|
expect(result.toString()).toBe(expectedLink)
|
||||||
|
})
|
||||||
|
})
|
||||||
100
src/lib/links.ts
Normal file
100
src/lib/links.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
import {
|
||||||
|
ASK_TO_OPEN_QUERY_PARAM,
|
||||||
|
CREATE_FILE_URL_PARAM,
|
||||||
|
PROD_APP_URL,
|
||||||
|
} from './constants'
|
||||||
|
import { stringToBase64 } from './base64'
|
||||||
|
import { DEV, VITE_KC_API_BASE_URL } from 'env'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { err } from './trap'
|
||||||
|
export interface FileLinkParams {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
units: UnitLength_type
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyFileShareLink(
|
||||||
|
args: FileLinkParams & { token: string }
|
||||||
|
) {
|
||||||
|
const token = args.token
|
||||||
|
if (!token) {
|
||||||
|
toast.error('You need to be signed in to share a file.', {
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const shareUrl = createCreateFileUrl(args)
|
||||||
|
const shortlink = await createShortlink(token, shareUrl.toString())
|
||||||
|
|
||||||
|
if (err(shortlink)) {
|
||||||
|
toast.error(shortlink.message, {
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await globalThis.navigator.clipboard.writeText(shortlink.url)
|
||||||
|
toast.success(
|
||||||
|
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!',
|
||||||
|
{
|
||||||
|
duration: 5000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a URL with the necessary query parameters to trigger
|
||||||
|
* the "Import file from URL" command in the app.
|
||||||
|
*
|
||||||
|
* With the additional step of asking the user if they want to
|
||||||
|
* open the URL in the desktop app.
|
||||||
|
*/
|
||||||
|
export function createCreateFileUrl({ code, name, units }: FileLinkParams) {
|
||||||
|
// Use the dev server if we are in development mode
|
||||||
|
let origin = DEV ? 'http://localhost:3000' : PROD_APP_URL
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
[CREATE_FILE_URL_PARAM]: String(true),
|
||||||
|
name,
|
||||||
|
units,
|
||||||
|
code: stringToBase64(code),
|
||||||
|
[ASK_TO_OPEN_QUERY_PARAM]: String(true),
|
||||||
|
})
|
||||||
|
const createFileUrl = new URL(`?${searchParams.toString()}`, origin)
|
||||||
|
|
||||||
|
return createFileUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a file's code, name, and units, creates shareable link to the
|
||||||
|
* web app with a query parameter that triggers a modal to "open in desktop app".
|
||||||
|
* That modal is defined in the `OpenInDesktopAppHandler` component.
|
||||||
|
* TODO: update the return type to use TS library after its updated
|
||||||
|
*/
|
||||||
|
export async function createShortlink(
|
||||||
|
token: string,
|
||||||
|
url: string
|
||||||
|
): Promise<Error | { key: string; url: string }> {
|
||||||
|
/**
|
||||||
|
* We don't use our `withBaseURL` function here because
|
||||||
|
* there is no URL shortener service in the dev API.
|
||||||
|
*/
|
||||||
|
const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url,
|
||||||
|
// In future we can support org-scoped and password-protected shortlinks here
|
||||||
|
// https://zoo.dev/docs/api/shortlinks/create-a-shortlink-for-a-user?lang=typescript
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
return new Error(`Failed to create shortlink: ${error.message}`)
|
||||||
|
} else {
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -114,7 +114,7 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
return redirect(
|
return redirect(
|
||||||
`${PATHS.FILE}/${encodeURIComponent(
|
`${PATHS.FILE}/${encodeURIComponent(
|
||||||
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
|
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
|
||||||
)}`
|
)}${new URL(routerData.request.url).search || ''}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,11 +188,14 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
|
|
||||||
// Loads the settings and by extension the projects in the default directory
|
// Loads the settings and by extension the projects in the default directory
|
||||||
// and returns them to the Home route, along with any errors that occurred
|
// and returns them to the Home route, along with any errors that occurred
|
||||||
export const homeLoader: LoaderFunction = async (): Promise<
|
export const homeLoader: LoaderFunction = async ({
|
||||||
HomeLoaderData | Response
|
request,
|
||||||
> => {
|
}): Promise<HomeLoaderData | Response> => {
|
||||||
|
const url = new URL(request.url)
|
||||||
if (!isDesktop()) {
|
if (!isDesktop()) {
|
||||||
return redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
return redirect(
|
||||||
|
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,11 +18,8 @@ import { EditorSelection, SelectionRange } from '@codemirror/state'
|
|||||||
import { getNormalisedCoordinates, isOverlap } from 'lib/utils'
|
import { getNormalisedCoordinates, isOverlap } from 'lib/utils'
|
||||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
import { Program } from 'lang/wasm'
|
import { Program } from 'lang/wasm'
|
||||||
import {
|
import { getNodeFromPath, isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
getNodeFromPath,
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
getNodePathFromSourceRange,
|
|
||||||
isSingleCursorInPipe,
|
|
||||||
} from 'lang/queryAst'
|
|
||||||
import { CommandArgument } from './commandTypes'
|
import { CommandArgument } from './commandTypes'
|
||||||
import {
|
import {
|
||||||
DefaultPlaneStr,
|
DefaultPlaneStr,
|
||||||
|
|||||||
@ -80,7 +80,8 @@ class MockEngineCommandManager {
|
|||||||
|
|
||||||
export async function enginelessExecutor(
|
export async function enginelessExecutor(
|
||||||
ast: Node<Program>,
|
ast: Node<Program>,
|
||||||
pmo: ProgramMemory | Error = ProgramMemory.empty()
|
pmo: ProgramMemory | Error = ProgramMemory.empty(),
|
||||||
|
path?: string
|
||||||
): Promise<ExecState> {
|
): Promise<ExecState> {
|
||||||
if (pmo !== null && err(pmo)) return Promise.reject(pmo)
|
if (pmo !== null && err(pmo)) return Promise.reject(pmo)
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ export async function enginelessExecutor(
|
|||||||
}) as any as EngineCommandManager
|
}) as any as EngineCommandManager
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
mockEngineCommandManager.startNewSession()
|
mockEngineCommandManager.startNewSession()
|
||||||
const execState = await executor(ast, mockEngineCommandManager, pmo)
|
const execState = await executor(ast, mockEngineCommandManager, path, pmo)
|
||||||
await mockEngineCommandManager.waitForAllCommands()
|
await mockEngineCommandManager.waitForAllCommands()
|
||||||
return execState
|
return execState
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,10 +68,6 @@ interface TextToKclProps {
|
|||||||
data?: unknown
|
data?: unknown
|
||||||
) => unknown
|
) => unknown
|
||||||
navigate: NavigateFunction
|
navigate: NavigateFunction
|
||||||
commandBarSend: (
|
|
||||||
type: EventFrom<typeof commandBarMachine>,
|
|
||||||
data?: unknown
|
|
||||||
) => unknown
|
|
||||||
context: ContextFrom<typeof fileMachine>
|
context: ContextFrom<typeof fileMachine>
|
||||||
token?: string
|
token?: string
|
||||||
settings: {
|
settings: {
|
||||||
@ -84,7 +80,6 @@ export async function submitAndAwaitTextToKcl({
|
|||||||
trimmedPrompt,
|
trimmedPrompt,
|
||||||
fileMachineSend,
|
fileMachineSend,
|
||||||
navigate,
|
navigate,
|
||||||
commandBarSend,
|
|
||||||
context,
|
context,
|
||||||
token,
|
token,
|
||||||
settings,
|
settings,
|
||||||
@ -96,7 +91,6 @@ export async function submitAndAwaitTextToKcl({
|
|||||||
ToastTextToCadError({
|
ToastTextToCadError({
|
||||||
toastId,
|
toastId,
|
||||||
message,
|
message,
|
||||||
commandBarSend,
|
|
||||||
prompt: trimmedPrompt,
|
prompt: trimmedPrompt,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { DEV } from 'env'
|
import { DEV } from 'env'
|
||||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
|
||||||
import {
|
import {
|
||||||
canRectangleOrCircleTool,
|
canRectangleOrCircleTool,
|
||||||
isClosedSketch,
|
isClosedSketch,
|
||||||
@ -21,7 +21,6 @@ type ToolbarMode = {
|
|||||||
export interface ToolbarItemCallbackProps {
|
export interface ToolbarItemCallbackProps {
|
||||||
modelingState: StateFrom<typeof modelingMachine>
|
modelingState: StateFrom<typeof modelingMachine>
|
||||||
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
|
modelingSend: (event: EventFrom<typeof modelingMachine>) => void
|
||||||
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
|
||||||
sketchPathId: string | false
|
sketchPathId: string | false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,8 +83,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
'break',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'extrude',
|
id: 'extrude',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Extrude', groupId: 'modeling' },
|
data: { name: 'Extrude', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -98,8 +97,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'revolve',
|
id: 'revolve',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Revolve', groupId: 'modeling' },
|
data: { name: 'Revolve', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -119,8 +118,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sweep',
|
id: 'sweep',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Sweep', groupId: 'modeling' },
|
data: { name: 'Sweep', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -139,8 +138,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'loft',
|
id: 'loft',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Loft', groupId: 'modeling' },
|
data: { name: 'Loft', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -160,8 +159,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
'break',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'fillet3d',
|
id: 'fillet3d',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Fillet', groupId: 'modeling' },
|
data: { name: 'Fillet', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -174,8 +173,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'chamfer3d',
|
id: 'chamfer3d',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Chamfer', groupId: 'modeling' },
|
data: { name: 'Chamfer', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -188,8 +187,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'shell',
|
id: 'shell',
|
||||||
onClick: ({ commandBarSend }) => {
|
onClick: () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Shell', groupId: 'modeling' },
|
data: { name: 'Shell', groupId: 'modeling' },
|
||||||
})
|
})
|
||||||
@ -269,8 +268,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'plane-offset',
|
id: 'plane-offset',
|
||||||
onClick: ({ commandBarSend }) => {
|
onClick: () => {
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Offset plane', groupId: 'modeling' },
|
data: { name: 'Offset plane', groupId: 'modeling' },
|
||||||
})
|
})
|
||||||
@ -301,8 +300,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'text-to-cad',
|
id: 'text-to-cad',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Text-to-CAD', groupId: 'modeling' },
|
data: { name: 'Text-to-CAD', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -319,8 +318,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'prompt-to-edit',
|
id: 'prompt-to-edit',
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Prompt-to-edit', groupId: 'modeling' },
|
data: { name: 'Prompt-to-edit', groupId: 'modeling' },
|
||||||
}),
|
}),
|
||||||
@ -593,8 +592,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
{
|
{
|
||||||
id: 'constraint-length',
|
id: 'constraint-length',
|
||||||
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
|
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
|
||||||
onClick: ({ commandBarSend }) =>
|
onClick: () =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
name: 'Constrain length',
|
name: 'Constrain length',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { assign, fromPromise, setup } from 'xstate'
|
import { assign, createActor, fromPromise, setup, SnapshotFrom } from 'xstate'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandArgument,
|
CommandArgument,
|
||||||
@ -9,6 +9,7 @@ import { Selections__old } from 'lib/selections'
|
|||||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
|
||||||
export type CommandBarContext = {
|
export type CommandBarContext = {
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
@ -247,8 +248,17 @@ export const commandBarMachine = setup({
|
|||||||
guards: {
|
guards: {
|
||||||
'Command needs review': ({ context }) =>
|
'Command needs review': ({ context }) =>
|
||||||
context.selectedCommand?.needsReview || false,
|
context.selectedCommand?.needsReview || false,
|
||||||
'Command has no arguments': () => false,
|
'Command has no arguments': ({ context }) => {
|
||||||
'All arguments are skippable': () => false,
|
return (
|
||||||
|
!context.selectedCommand?.args ||
|
||||||
|
Object.keys(context.selectedCommand?.args).length === 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'All arguments are skippable': ({ context }) => {
|
||||||
|
return Object.values(context.selectedCommand!.args!).every(
|
||||||
|
(argConfig) => argConfig.skip
|
||||||
|
)
|
||||||
|
},
|
||||||
'Has selected command': ({ context }) => !!context.selectedCommand,
|
'Has selected command': ({ context }) => !!context.selectedCommand,
|
||||||
},
|
},
|
||||||
actors: {
|
actors: {
|
||||||
@ -620,3 +630,12 @@ function sortCommands(a: Command, b: Command) {
|
|||||||
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
|
if (a.groupId === 'settings' && !(b.groupId === 'settings')) return 1
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const commandBarActor = createActor(commandBarMachine).start()
|
||||||
|
|
||||||
|
/** Basic state snapshot selector */
|
||||||
|
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
||||||
|
state
|
||||||
|
export const useCommandBarState = () => {
|
||||||
|
return useSelector(commandBarActor, cmdBarStateSelector)
|
||||||
|
}
|
||||||
|
|||||||
@ -16,10 +16,8 @@ import {
|
|||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { assign, fromPromise, fromCallback, setup } from 'xstate'
|
import { assign, fromPromise, fromCallback, setup } from 'xstate'
|
||||||
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
|
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
|
||||||
import {
|
import { isNodeSafeToReplacePath } from 'lang/queryAst'
|
||||||
isNodeSafeToReplacePath,
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
getNodePathFromSourceRange,
|
|
||||||
} from 'lang/queryAst'
|
|
||||||
import {
|
import {
|
||||||
kclManager,
|
kclManager,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
@ -1561,40 +1559,40 @@ export const modelingMachine = setup({
|
|||||||
if (!input) return new Error('No input provided')
|
if (!input) return new Error('No input provided')
|
||||||
// Extract inputs
|
// Extract inputs
|
||||||
const ast = kclManager.ast
|
const ast = kclManager.ast
|
||||||
const { profile, path } = input
|
const { target, trajectory } = input
|
||||||
|
|
||||||
// Find the profile declaration
|
// Find the profile declaration
|
||||||
const profileNodePath = getNodePathFromSourceRange(
|
const targetNodePath = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
profile.graphSelections[0].codeRef.range
|
target.graphSelections[0].codeRef.range
|
||||||
)
|
)
|
||||||
const profileNode = getNodeFromPath<VariableDeclarator>(
|
const targetNode = getNodeFromPath<VariableDeclarator>(
|
||||||
ast,
|
ast,
|
||||||
profileNodePath,
|
targetNodePath,
|
||||||
'VariableDeclarator'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
if (err(profileNode)) {
|
if (err(targetNode)) {
|
||||||
return new Error("Couldn't parse profile selection")
|
return new Error("Couldn't parse profile selection")
|
||||||
}
|
}
|
||||||
const profileDeclarator = profileNode.node
|
const targetDeclarator = targetNode.node
|
||||||
|
|
||||||
// Find the path declaration
|
// Find the path declaration
|
||||||
const pathNodePath = getNodePathFromSourceRange(
|
const trajectoryNodePath = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
path.graphSelections[0].codeRef.range
|
trajectory.graphSelections[0].codeRef.range
|
||||||
)
|
)
|
||||||
const pathNode = getNodeFromPath<VariableDeclarator>(
|
const trajectoryNode = getNodeFromPath<VariableDeclarator>(
|
||||||
ast,
|
ast,
|
||||||
pathNodePath,
|
trajectoryNodePath,
|
||||||
'VariableDeclarator'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
if (err(pathNode)) {
|
if (err(trajectoryNode)) {
|
||||||
return new Error("Couldn't parse path selection")
|
return new Error("Couldn't parse path selection")
|
||||||
}
|
}
|
||||||
const pathDeclarator = pathNode.node
|
const trajectoryDeclarator = trajectoryNode.node
|
||||||
|
|
||||||
// Perform the sweep
|
// Perform the sweep
|
||||||
const sweepRes = addSweep(ast, profileDeclarator, pathDeclarator)
|
const sweepRes = addSweep(ast, targetDeclarator, trajectoryDeclarator)
|
||||||
const updateAstResult = await kclManager.updateAst(
|
const updateAstResult = await kclManager.updateAst(
|
||||||
sweepRes.modifiedAst,
|
sweepRes.modifiedAst,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@ -25,6 +25,10 @@ export const projectsMachine = setup({
|
|||||||
type: 'Delete project'
|
type: 'Delete project'
|
||||||
data: ProjectsCommandSchema['Delete project']
|
data: ProjectsCommandSchema['Delete project']
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'Import file from URL'
|
||||||
|
data: ProjectsCommandSchema['Import file from URL']
|
||||||
|
}
|
||||||
| { type: 'navigate'; data: { name: string } }
|
| { type: 'navigate'; data: { name: string } }
|
||||||
| {
|
| {
|
||||||
type: 'xstate.done.actor.read-projects'
|
type: 'xstate.done.actor.read-projects'
|
||||||
@ -42,6 +46,10 @@ export const projectsMachine = setup({
|
|||||||
type: 'xstate.done.actor.rename-project'
|
type: 'xstate.done.actor.rename-project'
|
||||||
output: { message: string; oldName: string; newName: string }
|
output: { message: string; oldName: string; newName: string }
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'xstate.done.actor.create-file'
|
||||||
|
output: { message: string; projectName: string; fileName: string }
|
||||||
|
}
|
||||||
| { type: 'assign'; data: { [key: string]: any } },
|
| { type: 'assign'; data: { [key: string]: any } },
|
||||||
input: {} as {
|
input: {} as {
|
||||||
projects: Project[]
|
projects: Project[]
|
||||||
@ -60,6 +68,7 @@ export const projectsMachine = setup({
|
|||||||
toastError: () => {},
|
toastError: () => {},
|
||||||
navigateToProject: () => {},
|
navigateToProject: () => {},
|
||||||
navigateToProjectIfNeeded: () => {},
|
navigateToProjectIfNeeded: () => {},
|
||||||
|
navigateToFile: () => {},
|
||||||
},
|
},
|
||||||
actors: {
|
actors: {
|
||||||
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
||||||
@ -90,12 +99,22 @@ export const projectsMachine = setup({
|
|||||||
name: '',
|
name: '',
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
createFile: fromPromise(
|
||||||
|
(_: {
|
||||||
|
input: ProjectsCommandSchema['Import file from URL'] & {
|
||||||
|
projects: Project[]
|
||||||
|
}
|
||||||
|
}) => Promise.resolve({ message: '', projectName: '', fileName: '' })
|
||||||
|
),
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
'Has at least 1 project': () => false,
|
'Has at least 1 project': ({ event }) => {
|
||||||
|
if (event.type !== 'xstate.done.actor.read-projects') return false
|
||||||
|
return event.output.length ? event.output.length >= 1 : false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjBIkA2AHQAOCUw0qNkjQE4xYjQF8zMtJhwES5NcmpZSqLBwBOqAFZh8PWAoAJTBcCHcvX39YZjYkEC5efkF40QQFFQBmAHY1Qwz9QwBWU0NMrRl5BGyxBTUVJlVM01rtBXNLEGtsPCIyMEdnVwifPwCKAGEPUJ5sT1H-WKFE4j4BITSMzMzNIsyJDSZMhSYmFWzKxAkTNSYxIuU9zOKT7IsrDB67fsHYEajxiEwv8xjFWMtuKtkhsrpJboZsioincFMdTIjLggVGJ1GImMVMg0JISjEV3l1PrY+g4nH95gDAiFSLgbPSxkt4is1ilQGlrhJ4YjkbU0RoMXJEIYkWoikV8hJinjdOdyd0qfYBrSQdFJtNcLNtTwOZxIdyYQh+YKkSjReKqqYBQoEQpsgiSi9MqrKb0Nb9DYEACJgAA2YANbMW4M5puhqVhAvxQptCnRKkxCleuw0Gm2+2aRVRXpsPp+Woj4wA8hwwKRDcaEjH1nGLXDE9aRSmxWmJVipWocmdsbL8kxC501SWHFMZmQoIaKBABAMyAA3VAAawG+D1swAtOX61zY7zENlEWoMkoatcdPoipjCWIZRIirozsPiYYi19qQNp-rZ3nMAPC8Dw1A4YN9QAM1QDx0DUbcZjAfdInZKMTSSJsT2qc9Lwka8lTvTFTB2WUMiyQwc3yPRv3VH4mRZQDywXJc1FXDcBmmZlMBQhYjXQhtMJ5ERT1wlQr0kQj7kxKiZTxM5s2zbQEVoycBgY9AmNQ-wKGA0DwMgngYLgtQuJZZCDwEo8sJEnD1Dwgjb2kntig0GUkWVfZDEMfFVO+Bwg1DPhSDnZjFwcdjNzUCAQzDCztP4uIMKhGy0jPezxPwySnPvHsTjlPIhyOHRsnwlM-N-NRArDLS+N0kDYIM6DYPgmKgvivjD0bYS+VbBF21RTs7UUUcimfRpXyYRF8LFCrfSBCBaoZFiItINcor1CBeIZLqhPNdKL0yxzs2cqoNDqdpSo0EoSoLOb6NCRaQv9FblzWjjTMe7bQQYBQksElKesUMRjFubRMllCHsm2a5MVldRDBfFpmj0IpxPuhwFqW0F6v0iDmpMzbvuiXbAfNFNQcaI5IaKaH9jETFsUMNRVDxOU5TxBULE6VwYvgeIJ38sAIT25td27KpdzG7yZeho5slHVmMc1IY3HLfnkrNZt2hOW5smRCQM2uhQHkZ6VtkRBFnmu0qFGVv11ZFsnm0kdN8QFJVjlUPZ9co+3-2C0KEqdrXsJMJ8ryc86iUyYiCvxFNzldb3jHtjTsf8EPj1ssQchZlR8jR5EmDRrIGZcuF7maF1aZtslx29IWqtiwPDSz1LxDxepnlOfYDghp03eyOpngzXuYdHNPHozgJ26B9IXR2cTvP0BVkSRXKqgLtyq8OfQcXOwlubMIA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjAHYAbADoArBJVMFTCQA4mTAMwmxAXwsy0mHARLkKASXRcATjywAzYgBtsb3cMLABVACUAGWY2JBAuXn5BONEESRl5BAUFFQM1HQUxJSYATmyFAyUTKxsMbDwiMjA1ZGosUlQsDmCAKzB8HlgKcLBcCC7e-sGYoQTiPgEhVIUy9SKSiQ0NEwkclRyMxGKJNRKlMRVDU23zpRqQW3qHJpa2jonUPoGhgGF3UZ42G6nymMzicwWyVAyzKJzECg0OiYGjERhK+kOWUMSlOGiUlTEJlMxRKBnuj3sjXIr1gHy+g2Go3GwPpsDBnG48ySS0QGj0+Q0JTOGkqBg2JhKmNRJy2OyUEn0Kml5LqlMczVatJZUyGI1IuDs2oG7PinMhPIQfKYAqFShF+PFkrkRwkYjU8OUShKKhMKg0pUs1geqoa6ppdJ1FD+AKBk2NrFmZu5KV5-L9tvtYokEsxelRagUCoMiMumyUdpVdlDL01Ee+FAAImAAoC6zwTRDk9DU9b08LRY7MWd1AZcoS-aoLiYFJWnlSNW0jQyAPIcMCkNsdpOLFOWtOC-sO7NOzLbGWFbY6acmFESWdql7R3B8UhQNsUCACZpkABuqAA1s0+D-M+YAALRLluiQ7t2WRMGIbryjeBgGBIEglNsCi5sY6hMOchQqEoCLFCh97VtST4vm+S4UGA7jBO4agcH4z7eKg7joGowExhBcbtgm4LblCIiKPBiHZiKqHoZhuaFhoBbGMYBhiPBCgStUQYUuRzR6gaZDUXxH5fmov4Ac0-z6pgvEgvGsQctBwnLGJahIZJaEYdOmKEfJuQSoRRKSRcZHPNSunoPp750QxTEsTwbEcWoFkGuBkECfZXIwSJcEIS5Ekoe5MnOggXqIaUqFiiUhIInemkhiFzRNi2EU0Z+1KmYBagQM2YCAtZ9JQRljmiTlrn5dJnlFShOLwcpZjKBs+KBrUVb1WojU9c1hlRexMWsexnFdS2KV8QN5q7laNqHlmOaTcpmjoWc8L6HoYrBfOagjGMm02QyrXfqQf4dSBEB9Tqp1dllebichUkeVhRXTiU+QecUKhCps4pvWGn0QN9rJGW1ANmYlTKg98DAKHZpoORanoyqh8oImU3pKJifJ5Ohdr7CYqjIvKWMvDjeORttjHMXtCXA2T0xpdTg20+W9MSIzgorIRXlpr6ORbPs+jwQLFEgVRPj+JQf0mUTHXcaBYG+AE4OZcsxbuts5hbCK+zFmImJgSc5zPV6BQVD6RQG80lERXblCi7tcX7VxRvgVHDtDQgaE4vK2gXKiTA6ItubmJo8IoqOylERUVhBh0XXwHEWn1YmNO7mBKg+2KpzK2IpIBUwBhqUtwYre9tbvEutfpWdsFFnkPpVJnQqz15Ozur36FoypREbGH4Zj438u7tO1qIu5qK5PBGyYjebpEkS44e9oVTbxHr5tnvk9ZeXajTuW+zaHNuzYRUmoeCEp-RKlUHJbeYVhYDDfhDVIxR5IGGnISQk6E+6EkxMUBQX9c66EMMg3Q5Zt7rWNkuOBjsjilDUAqQsZQzzgOkJNUk7o7SmF-tiXOUCmQwMGBQ1OF4TgVGzFUG8yIxQGDZiYPI4oqh+mPsrDSy05xhmfm+KO-CLT+iRucEUuwFQqWzMWXM2YTAuTtL6ZBylkRcMrkAA */
|
||||||
id: 'Home machine',
|
id: 'Home machine',
|
||||||
|
|
||||||
initial: 'Reading projects',
|
initial: 'Reading projects',
|
||||||
@ -111,6 +130,8 @@ export const projectsMachine = setup({
|
|||||||
})),
|
})),
|
||||||
target: '.Reading projects',
|
target: '.Reading projects',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Import file from URL': '.Creating file',
|
||||||
},
|
},
|
||||||
states: {
|
states: {
|
||||||
'Has no projects': {
|
'Has no projects': {
|
||||||
@ -155,7 +176,10 @@ export const projectsMachine = setup({
|
|||||||
id: 'create-project',
|
id: 'create-project',
|
||||||
src: 'createProject',
|
src: 'createProject',
|
||||||
input: ({ event, context }) => {
|
input: ({ event, context }) => {
|
||||||
if (event.type !== 'Create project') {
|
if (
|
||||||
|
event.type !== 'Create project' &&
|
||||||
|
event.type !== 'Import file from URL'
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
projects: context.projects,
|
projects: context.projects,
|
||||||
@ -272,5 +296,39 @@ export const projectsMachine = setup({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Creating file': {
|
||||||
|
invoke: {
|
||||||
|
id: 'create-file',
|
||||||
|
src: 'createFile',
|
||||||
|
input: ({ event, context }) => {
|
||||||
|
if (event.type !== 'Import file from URL') {
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
units: 'mm',
|
||||||
|
method: 'existingProject',
|
||||||
|
projects: context.projects,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: event.data.code || '',
|
||||||
|
name: event.data.name,
|
||||||
|
units: event.data.units,
|
||||||
|
method: event.data.method,
|
||||||
|
projectName: event.data.projectName,
|
||||||
|
projects: context.projects,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: 'Reading projects',
|
||||||
|
actions: ['navigateToFile', 'toastSuccess'],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'Reading projects',
|
||||||
|
actions: 'toastError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
73
src/main.ts
73
src/main.ts
@ -21,6 +21,7 @@ import minimist from 'minimist'
|
|||||||
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
|
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
|
||||||
import argvFromYargs from './commandLineArgs'
|
import argvFromYargs from './commandLineArgs'
|
||||||
|
|
||||||
import * as packageJSON from '../package.json'
|
import * as packageJSON from '../package.json'
|
||||||
@ -48,9 +49,7 @@ process.env.VITE_KC_SITE_BASE_URL ??= 'https://zoo.dev'
|
|||||||
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
process.env.VITE_KC_SKIP_AUTH ??= 'false'
|
||||||
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= '15000'
|
||||||
|
|
||||||
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
/// Register our application to handle all "zoo-studio:" protocols.
|
||||||
|
|
||||||
/// Register our application to handle all "electron-fiddle://" protocols.
|
|
||||||
if (process.defaultApp) {
|
if (process.defaultApp) {
|
||||||
if (process.argv.length >= 2) {
|
if (process.argv.length >= 2) {
|
||||||
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
|
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
|
||||||
@ -65,7 +64,7 @@ if (process.defaultApp) {
|
|||||||
// Must be done before ready event.
|
// Must be done before ready event.
|
||||||
registerStartupListeners()
|
registerStartupListeners()
|
||||||
|
|
||||||
const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
|
const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
|
||||||
let newWindow
|
let newWindow
|
||||||
|
|
||||||
if (reuse) {
|
if (reuse) {
|
||||||
@ -90,32 +89,54 @@ const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathIsCustomProtocolLink =
|
||||||
|
pathToOpen?.startsWith(ZOO_STUDIO_PROTOCOL) ?? false
|
||||||
|
|
||||||
// and load the index.html of the app.
|
// and load the index.html of the app.
|
||||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||||
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
|
const filteredPath = pathToOpen
|
||||||
|
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
|
||||||
|
: ''
|
||||||
|
const fullHashBasedUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/${filteredPath}`
|
||||||
|
newWindow.loadURL(fullHashBasedUrl).catch(reportRejection)
|
||||||
} else {
|
} else {
|
||||||
getProjectPathAtStartup(filePath)
|
if (pathIsCustomProtocolLink && pathToOpen) {
|
||||||
.then(async (projectPath) => {
|
// We're trying to open a custom protocol link
|
||||||
const startIndex = path.join(
|
const filteredPath = pathToOpen
|
||||||
__dirname,
|
? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, ''))
|
||||||
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
: ''
|
||||||
)
|
const startIndex = path.join(
|
||||||
|
__dirname,
|
||||||
if (projectPath === null) {
|
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
||||||
await newWindow.loadFile(startIndex)
|
)
|
||||||
return
|
newWindow
|
||||||
}
|
.loadFile(startIndex, {
|
||||||
|
hash: filteredPath,
|
||||||
console.log('Loading file', projectPath)
|
|
||||||
|
|
||||||
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
|
|
||||||
console.log('Full URL', fullUrl)
|
|
||||||
|
|
||||||
await newWindow.loadFile(startIndex, {
|
|
||||||
hash: fullUrl,
|
|
||||||
})
|
})
|
||||||
})
|
.catch(reportRejection)
|
||||||
.catch(reportRejection)
|
} else {
|
||||||
|
// otherwise we're trying to open a local file from the command line
|
||||||
|
getProjectPathAtStartup(pathToOpen)
|
||||||
|
.then(async (projectPath) => {
|
||||||
|
const startIndex = path.join(
|
||||||
|
__dirname,
|
||||||
|
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (projectPath === null) {
|
||||||
|
await newWindow.loadFile(startIndex)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
|
||||||
|
console.log('Full URL', fullUrl)
|
||||||
|
|
||||||
|
await newWindow.loadFile(startIndex, {
|
||||||
|
hash: fullUrl,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(reportRejection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
|
|||||||
@ -24,16 +24,28 @@ import { markOnce } from 'lib/performance'
|
|||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
||||||
import { useProjectsContext } from 'hooks/useProjectsContext'
|
import { useProjectsContext } from 'hooks/useProjectsContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||||
|
|
||||||
// This route only opens in the desktop context for now,
|
// This route only opens in the desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { state, send } = useProjectsContext()
|
const { state, send } = useProjectsContext()
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||||
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
|
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
|
||||||
|
|
||||||
|
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||||
|
useCreateFileLinkQuery((argDefaultValues) => {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'projects',
|
||||||
|
name: 'Import file from URL',
|
||||||
|
argDefaultValues,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {
|
const {
|
||||||
@ -128,7 +140,7 @@ const Home = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
commandBarSend({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'projects',
|
groupId: 'projects',
|
||||||
|
|||||||
30
src/wasm-lib/Cargo.lock
generated
30
src/wasm-lib/Cargo.lock
generated
@ -443,9 +443,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.23"
|
version = "4.5.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
|
checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@ -453,9 +453,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.23"
|
version = "4.5.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
|
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@ -467,9 +467,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.18"
|
version = "4.5.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -708,9 +708,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
@ -1710,7 +1710,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.2.30"
|
version = "0.2.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx 0.5.1",
|
"approx 0.5.1",
|
||||||
@ -1844,9 +1844,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad-modeling-cmds"
|
name = "kittycad-modeling-cmds"
|
||||||
version = "0.2.89"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce9e58b34645facea36bc9f4868877bbe6fcac01b92896825e8d4f2f7c71dbd6"
|
checksum = "67a993046541732e3c3ddd8a0364b55b7b138a9258beff353b6e7a043a41dce3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -3266,9 +3266,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.135"
|
version = "1.0.137"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
|
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.7.0",
|
"indexmap 2.7.0",
|
||||||
"itoa",
|
"itoa",
|
||||||
@ -4184,9 +4184,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.11.1"
|
version = "1.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
|
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -76,7 +76,7 @@ members = [
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
http = "1"
|
http = "1"
|
||||||
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
|
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
|
||||||
kittycad-modeling-cmds = { version = "0.2.89", features = [
|
kittycad-modeling-cmds = { version = "0.2.93", features = [
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"websocket",
|
"websocket",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
description = "KittyCAD Language implementation and tools"
|
description = "KittyCAD Language implementation and tools"
|
||||||
version = "0.2.30"
|
version = "0.2.31"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
@ -16,7 +16,7 @@ async-recursion = "1.1.1"
|
|||||||
async-trait = "0.1.85"
|
async-trait = "0.1.85"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
clap = { version = "4.5.23", default-features = false, optional = true, features = [
|
clap = { version = "4.5.27", default-features = false, optional = true, features = [
|
||||||
"std",
|
"std",
|
||||||
"derive",
|
"derive",
|
||||||
] }
|
] }
|
||||||
|
|||||||
294
src/wasm-lib/kcl/src/execution/import.rs
Normal file
294
src/wasm-lib/kcl/src/execution/import.rs
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
use std::{ffi::OsStr, path::Path, str::FromStr};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use kcmc::{
|
||||||
|
coord::{Axis, AxisDirectionPair, Direction, System},
|
||||||
|
each_cmd as mcmd,
|
||||||
|
format::InputFormat,
|
||||||
|
ok_response::OkModelingCmdResponse,
|
||||||
|
shared::FileImportFormat,
|
||||||
|
units::UnitLength,
|
||||||
|
websocket::OkWebSocketResponseData,
|
||||||
|
ImportFile, ModelingCmd,
|
||||||
|
};
|
||||||
|
use kittycad_modeling_cmds as kcmc;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::{KclError, KclErrorDetails},
|
||||||
|
execution::{ExecState, ImportedGeometry},
|
||||||
|
fs::FileSystem,
|
||||||
|
source_range::SourceRange,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ExecutorContext;
|
||||||
|
|
||||||
|
// Zoo co-ordinate system.
|
||||||
|
//
|
||||||
|
// * Forward: -Y
|
||||||
|
// * Up: +Z
|
||||||
|
// * Handedness: Right
|
||||||
|
pub const ZOO_COORD_SYSTEM: System = System {
|
||||||
|
forward: AxisDirectionPair {
|
||||||
|
axis: Axis::Y,
|
||||||
|
direction: Direction::Negative,
|
||||||
|
},
|
||||||
|
up: AxisDirectionPair {
|
||||||
|
axis: Axis::Z,
|
||||||
|
direction: Direction::Positive,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn import_foreign(
|
||||||
|
file_path: &Path,
|
||||||
|
format: Option<InputFormat>,
|
||||||
|
exec_state: &mut ExecState,
|
||||||
|
ctxt: &ExecutorContext,
|
||||||
|
source_range: SourceRange,
|
||||||
|
) -> Result<PreImportedGeometry, KclError> {
|
||||||
|
// Make sure the file exists.
|
||||||
|
if !ctxt.fs.exists(file_path, source_range).await? {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("File `{}` does not exist.", file_path.display()),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("No file extension found for `{}`", file_path.display()),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?)
|
||||||
|
.map_err(|e| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: e.to_string(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Get the format type from the extension of the file.
|
||||||
|
let format = if let Some(format) = format {
|
||||||
|
// Validate the given format with the extension format.
|
||||||
|
validate_extension_format(ext_format, format.clone()).map_err(|e| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: e.to_string(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
format
|
||||||
|
} else {
|
||||||
|
ext_format
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the file contents for each file path.
|
||||||
|
let file_contents = ctxt.fs.read(file_path, source_range).await.map_err(|e| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: e.to_string(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// We want the file_path to be without the parent.
|
||||||
|
let file_name = std::path::Path::new(&file_path)
|
||||||
|
.file_name()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("Could not get the file name from the path `{}`", file_path.display()),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let mut import_files = vec![kcmc::ImportFile {
|
||||||
|
path: file_name.to_string(),
|
||||||
|
data: file_contents.clone(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
// In the case of a gltf importing a bin file we need to handle that! and figure out where the
|
||||||
|
// file is relative to our current file.
|
||||||
|
if let InputFormat::Gltf(..) = format {
|
||||||
|
// Check if the file is a binary gltf file, in that case we don't need to import the bin
|
||||||
|
// file.
|
||||||
|
if !file_contents.starts_with(b"glTF") {
|
||||||
|
let json = gltf_json::Root::from_slice(&file_contents).map_err(|e| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: e.to_string(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Read the gltf file and check if there is a bin file.
|
||||||
|
for buffer in json.buffers.iter() {
|
||||||
|
if let Some(uri) = &buffer.uri {
|
||||||
|
if !uri.starts_with("data:") {
|
||||||
|
// We want this path relative to the file_path given.
|
||||||
|
let bin_path = std::path::Path::new(&file_path)
|
||||||
|
.parent()
|
||||||
|
.map(|p| p.join(uri))
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"Could not get the parent path of the file `{}`",
|
||||||
|
file_path.display()
|
||||||
|
),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let bin_contents = ctxt.fs.read(&bin_path, source_range).await.map_err(|e| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: e.to_string(),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
import_files.push(ImportFile {
|
||||||
|
path: uri.to_string(),
|
||||||
|
data: bin_contents,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(PreImportedGeometry {
|
||||||
|
id: exec_state.next_uuid(),
|
||||||
|
source_range,
|
||||||
|
command: mcmd::ImportFiles {
|
||||||
|
files: import_files.clone(),
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct PreImportedGeometry {
|
||||||
|
id: Uuid,
|
||||||
|
command: mcmd::ImportFiles,
|
||||||
|
source_range: SourceRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> {
|
||||||
|
if ctxt.is_mock() {
|
||||||
|
return Ok(ImportedGeometry {
|
||||||
|
id: pre.id,
|
||||||
|
value: pre.command.files.iter().map(|f| f.path.to_string()).collect(),
|
||||||
|
meta: vec![pre.source_range.into()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = ctxt
|
||||||
|
.engine
|
||||||
|
.send_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let OkWebSocketResponseData::Modeling {
|
||||||
|
modeling_response: OkModelingCmdResponse::ImportFiles(imported_files),
|
||||||
|
} = &resp
|
||||||
|
else {
|
||||||
|
return Err(KclError::Engine(KclErrorDetails {
|
||||||
|
message: format!("ImportFiles response was not as expected: {:?}", resp),
|
||||||
|
source_ranges: vec![pre.source_range],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ImportedGeometry {
|
||||||
|
id: imported_files.object_id,
|
||||||
|
value: pre.command.files.iter().map(|f| f.path.to_string()).collect(),
|
||||||
|
meta: vec![pre.source_range.into()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the source format from the extension.
|
||||||
|
fn get_import_format_from_extension(ext: &OsStr) -> Result<InputFormat> {
|
||||||
|
let ext = ext
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid file extension: `{ext:?}`"))?;
|
||||||
|
let format = match FileImportFormat::from_str(ext) {
|
||||||
|
Ok(format) => format,
|
||||||
|
Err(_) => {
|
||||||
|
if ext == "stp" {
|
||||||
|
FileImportFormat::Step
|
||||||
|
} else if ext == "glb" {
|
||||||
|
FileImportFormat::Gltf
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("unknown source format for file extension: {ext}. Try setting the `--src-format` flag explicitly or use a valid format.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make the default units millimeters.
|
||||||
|
let ul = UnitLength::Millimeters;
|
||||||
|
|
||||||
|
// Zoo co-ordinate system.
|
||||||
|
//
|
||||||
|
// * Forward: -Y
|
||||||
|
// * Up: +Z
|
||||||
|
// * Handedness: Right
|
||||||
|
match format {
|
||||||
|
FileImportFormat::Step => Ok(InputFormat::Step(kcmc::format::step::import::Options {
|
||||||
|
split_closed_faces: false,
|
||||||
|
})),
|
||||||
|
FileImportFormat::Stl => Ok(InputFormat::Stl(kcmc::format::stl::import::Options {
|
||||||
|
coords: ZOO_COORD_SYSTEM,
|
||||||
|
units: ul,
|
||||||
|
})),
|
||||||
|
FileImportFormat::Obj => Ok(InputFormat::Obj(kcmc::format::obj::import::Options {
|
||||||
|
coords: ZOO_COORD_SYSTEM,
|
||||||
|
units: ul,
|
||||||
|
})),
|
||||||
|
FileImportFormat::Gltf => Ok(InputFormat::Gltf(kcmc::format::gltf::import::Options {})),
|
||||||
|
FileImportFormat::Ply => Ok(InputFormat::Ply(kcmc::format::ply::import::Options {
|
||||||
|
coords: ZOO_COORD_SYSTEM,
|
||||||
|
units: ul,
|
||||||
|
})),
|
||||||
|
FileImportFormat::Fbx => Ok(InputFormat::Fbx(kcmc::format::fbx::import::Options {})),
|
||||||
|
FileImportFormat::Sldprt => Ok(InputFormat::Sldprt(kcmc::format::sldprt::import::Options {
|
||||||
|
split_closed_faces: false,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_extension_format(ext: InputFormat, given: InputFormat) -> Result<()> {
|
||||||
|
if let InputFormat::Stl(_) = ext {
|
||||||
|
if let InputFormat::Stl(_) = given {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let InputFormat::Obj(_) = ext {
|
||||||
|
if let InputFormat::Obj(_) = given {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let InputFormat::Ply(_) = ext {
|
||||||
|
if let InputFormat::Ply(_) = given {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext == given {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"The given format does not match the file extension. Expected: `{}`, Given: `{}`",
|
||||||
|
get_name_of_format(ext),
|
||||||
|
get_name_of_format(given)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_name_of_format(type_: InputFormat) -> &'static str {
|
||||||
|
match type_ {
|
||||||
|
InputFormat::Fbx(_) => "fbx",
|
||||||
|
InputFormat::Gltf(_) => "gltf",
|
||||||
|
InputFormat::Obj(_) => "obj",
|
||||||
|
InputFormat::Ply(_) => "ply",
|
||||||
|
InputFormat::Sldprt(_) => "sldprt",
|
||||||
|
InputFormat::Step(_) => "step",
|
||||||
|
InputFormat::Stl(_) => "stl",
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ type Point2D = kcmc::shared::Point2d<f64>;
|
|||||||
type Point3D = kcmc::shared::Point3d<f64>;
|
type Point3D = kcmc::shared::Point3d<f64>;
|
||||||
|
|
||||||
pub use function_param::FunctionParam;
|
pub use function_param::FunctionParam;
|
||||||
|
pub(crate) use import::{import_foreign, send_to_engine as send_import_to_engine, ZOO_COORD_SYSTEM};
|
||||||
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
|
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ pub(crate) mod cache;
|
|||||||
mod cad_op;
|
mod cad_op;
|
||||||
mod exec_ast;
|
mod exec_ast;
|
||||||
mod function_param;
|
mod function_param;
|
||||||
|
mod import;
|
||||||
mod kcl_value;
|
mod kcl_value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -40,7 +42,7 @@ use crate::{
|
|||||||
execution::cache::{CacheInformation, CacheResult},
|
execution::cache::{CacheInformation, CacheResult},
|
||||||
fs::{FileManager, FileSystem},
|
fs::{FileManager, FileSystem},
|
||||||
parsing::ast::types::{
|
parsing::ast::types::{
|
||||||
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, NonCodeValue,
|
BodyItem, Expr, FunctionExpression, ImportPath, ImportSelector, ItemVisibility, Node, NodeRef, NonCodeValue,
|
||||||
Program as AstProgram, TagDeclarator, TagNode,
|
Program as AstProgram, TagDeclarator, TagNode,
|
||||||
},
|
},
|
||||||
settings::types::UnitLength,
|
settings::types::UnitLength,
|
||||||
@ -129,7 +131,7 @@ pub struct ExecOutcome {
|
|||||||
impl ExecState {
|
impl ExecState {
|
||||||
pub fn new(exec_settings: &ExecutorSettings) -> Self {
|
pub fn new(exec_settings: &ExecutorSettings) -> Self {
|
||||||
ExecState {
|
ExecState {
|
||||||
global: GlobalState::new(),
|
global: GlobalState::new(exec_settings),
|
||||||
mod_local: ModuleState::new(exec_settings),
|
mod_local: ModuleState::new(exec_settings),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +142,7 @@ impl ExecState {
|
|||||||
// This is for the front end to keep track of the ids.
|
// This is for the front end to keep track of the ids.
|
||||||
id_generator.next_id = 0;
|
id_generator.next_id = 0;
|
||||||
|
|
||||||
let mut global = GlobalState::new();
|
let mut global = GlobalState::new(exec_settings);
|
||||||
global.id_generator = id_generator;
|
global.id_generator = id_generator;
|
||||||
|
|
||||||
*self = ExecState {
|
*self = ExecState {
|
||||||
@ -181,34 +183,15 @@ impl ExecState {
|
|||||||
self.global.artifacts.insert(id, artifact);
|
self.global.artifacts.insert(id, artifact);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_module(
|
fn add_module(&mut self, id: ModuleId, path: std::path::PathBuf, repr: ModuleRepr) -> ModuleId {
|
||||||
&mut self,
|
debug_assert!(!self.global.path_to_source_id.contains_key(&path));
|
||||||
path: std::path::PathBuf,
|
|
||||||
ctxt: &ExecutorContext,
|
|
||||||
source_range: SourceRange,
|
|
||||||
) -> Result<ModuleId, KclError> {
|
|
||||||
// Need to avoid borrowing self in the closure.
|
|
||||||
let new_module_id = ModuleId::from_usize(self.global.path_to_source_id.len());
|
|
||||||
let mut is_new = false;
|
|
||||||
let id = *self.global.path_to_source_id.entry(path.clone()).or_insert_with(|| {
|
|
||||||
is_new = true;
|
|
||||||
new_module_id
|
|
||||||
});
|
|
||||||
|
|
||||||
if is_new {
|
self.global.path_to_source_id.insert(path.clone(), id);
|
||||||
let source = ctxt.fs.read_to_string(&path, source_range).await?;
|
|
||||||
// TODO handle parsing errors properly
|
|
||||||
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
|
|
||||||
|
|
||||||
let module_info = ModuleInfo {
|
let module_info = ModuleInfo { id, repr, path };
|
||||||
id,
|
self.global.module_infos.insert(id, module_info);
|
||||||
path,
|
|
||||||
parsed: Some(parsed),
|
|
||||||
};
|
|
||||||
self.global.module_infos.insert(id, module_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(id)
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn length_unit(&self) -> UnitLen {
|
pub fn length_unit(&self) -> UnitLen {
|
||||||
@ -221,7 +204,7 @@ impl ExecState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GlobalState {
|
impl GlobalState {
|
||||||
fn new() -> Self {
|
fn new(settings: &ExecutorSettings) -> Self {
|
||||||
let mut global = GlobalState {
|
let mut global = GlobalState {
|
||||||
id_generator: Default::default(),
|
id_generator: Default::default(),
|
||||||
path_to_source_id: Default::default(),
|
path_to_source_id: Default::default(),
|
||||||
@ -232,15 +215,14 @@ impl GlobalState {
|
|||||||
artifact_graph: Default::default(),
|
artifact_graph: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO(#4434): Use the top-level file's path.
|
|
||||||
let root_path = PathBuf::new();
|
|
||||||
let root_id = ModuleId::default();
|
let root_id = ModuleId::default();
|
||||||
|
let root_path = settings.current_file.clone().unwrap_or_default();
|
||||||
global.module_infos.insert(
|
global.module_infos.insert(
|
||||||
root_id,
|
root_id,
|
||||||
ModuleInfo {
|
ModuleInfo {
|
||||||
id: root_id,
|
id: root_id,
|
||||||
path: root_path.clone(),
|
path: root_path.clone(),
|
||||||
parsed: None,
|
repr: ModuleRepr::Root,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
global.path_to_source_id.insert(root_path, root_id);
|
global.path_to_source_id.insert(root_path, root_id);
|
||||||
@ -1253,7 +1235,15 @@ pub struct ModuleInfo {
|
|||||||
id: ModuleId,
|
id: ModuleId,
|
||||||
/// Absolute path of the module's source file.
|
/// Absolute path of the module's source file.
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
parsed: Option<Node<AstProgram>>,
|
repr: ModuleRepr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub enum ModuleRepr {
|
||||||
|
Root,
|
||||||
|
Kcl(Node<AstProgram>),
|
||||||
|
Foreign(import::PreImportedGeometry),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
|
||||||
@ -1761,6 +1751,9 @@ pub struct ExecutorSettings {
|
|||||||
/// The directory of the current project. This is used for resolving import
|
/// The directory of the current project. This is used for resolving import
|
||||||
/// paths. If None is given, the current working directory is used.
|
/// paths. If None is given, the current working directory is used.
|
||||||
pub project_directory: Option<PathBuf>,
|
pub project_directory: Option<PathBuf>,
|
||||||
|
/// This is the path to the current file being executed.
|
||||||
|
/// We use this for preventing cyclic imports.
|
||||||
|
pub current_file: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ExecutorSettings {
|
impl Default for ExecutorSettings {
|
||||||
@ -1772,6 +1765,7 @@ impl Default for ExecutorSettings {
|
|||||||
show_grid: false,
|
show_grid: false,
|
||||||
replay: None,
|
replay: None,
|
||||||
project_directory: None,
|
project_directory: None,
|
||||||
|
current_file: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1785,6 +1779,7 @@ impl From<crate::settings::types::Configuration> for ExecutorSettings {
|
|||||||
show_grid: config.settings.modeling.show_scale_grid,
|
show_grid: config.settings.modeling.show_scale_grid,
|
||||||
replay: None,
|
replay: None,
|
||||||
project_directory: None,
|
project_directory: None,
|
||||||
|
current_file: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1798,6 +1793,7 @@ impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSet
|
|||||||
show_grid: config.settings.modeling.show_scale_grid,
|
show_grid: config.settings.modeling.show_scale_grid,
|
||||||
replay: None,
|
replay: None,
|
||||||
project_directory: None,
|
project_directory: None,
|
||||||
|
current_file: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1811,6 +1807,25 @@ impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
|
|||||||
show_grid: modeling.show_scale_grid,
|
show_grid: modeling.show_scale_grid,
|
||||||
replay: None,
|
replay: None,
|
||||||
project_directory: None,
|
project_directory: None,
|
||||||
|
current_file: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExecutorSettings {
|
||||||
|
/// Add the current file path to the executor settings.
|
||||||
|
pub fn with_current_file(&mut self, current_file: PathBuf) {
|
||||||
|
// We want the parent directory of the file.
|
||||||
|
if current_file.extension() == Some(std::ffi::OsStr::new("kcl")) {
|
||||||
|
self.current_file = Some(current_file.clone());
|
||||||
|
// Get the parent directory.
|
||||||
|
if let Some(parent) = current_file.parent() {
|
||||||
|
self.project_directory = Some(parent.to_path_buf());
|
||||||
|
} else {
|
||||||
|
self.project_directory = Some(std::path::PathBuf::from(""));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.project_directory = Some(current_file.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2028,6 +2043,7 @@ impl ExecutorContext {
|
|||||||
show_grid: false,
|
show_grid: false,
|
||||||
replay: None,
|
replay: None,
|
||||||
project_directory: None,
|
project_directory: None,
|
||||||
|
current_file: None,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
engine_addr,
|
engine_addr,
|
||||||
@ -2511,33 +2527,68 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
async fn open_module(
|
async fn open_module(
|
||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &ImportPath,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
source_range: SourceRange,
|
source_range: SourceRange,
|
||||||
) -> Result<ModuleId, KclError> {
|
) -> Result<ModuleId, KclError> {
|
||||||
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
match path {
|
||||||
project_dir.join(path)
|
ImportPath::Kcl { filename } => {
|
||||||
} else {
|
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
||||||
std::path::PathBuf::from(&path)
|
project_dir.join(filename)
|
||||||
};
|
} else {
|
||||||
|
std::path::PathBuf::from(filename)
|
||||||
|
};
|
||||||
|
|
||||||
if exec_state.mod_local.import_stack.contains(&resolved_path) {
|
if exec_state.mod_local.import_stack.contains(&resolved_path) {
|
||||||
return Err(KclError::ImportCycle(KclErrorDetails {
|
return Err(KclError::ImportCycle(KclErrorDetails {
|
||||||
message: format!(
|
message: format!(
|
||||||
"circular import of modules is not allowed: {} -> {}",
|
"circular import of modules is not allowed: {} -> {}",
|
||||||
exec_state
|
exec_state
|
||||||
.mod_local
|
.mod_local
|
||||||
.import_stack
|
.import_stack
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.as_path().to_string_lossy())
|
.map(|p| p.as_path().to_string_lossy())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" -> "),
|
.join(" -> "),
|
||||||
resolved_path.to_string_lossy()
|
resolved_path.to_string_lossy()
|
||||||
),
|
),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
|
||||||
|
return Ok(*id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
|
||||||
|
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
|
||||||
|
// TODO handle parsing errors properly
|
||||||
|
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
|
||||||
|
let repr = ModuleRepr::Kcl(parsed);
|
||||||
|
|
||||||
|
Ok(exec_state.add_module(id, resolved_path, repr))
|
||||||
|
}
|
||||||
|
ImportPath::Foreign { path } => {
|
||||||
|
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
|
||||||
|
project_dir.join(path)
|
||||||
|
} else {
|
||||||
|
std::path::PathBuf::from(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
|
||||||
|
return Ok(*id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let geom = import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?;
|
||||||
|
let repr = ModuleRepr::Foreign(geom);
|
||||||
|
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
|
||||||
|
Ok(exec_state.add_module(id, resolved_path, repr))
|
||||||
|
}
|
||||||
|
i => Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("Unsupported import: `{i}`"),
|
||||||
source_ranges: vec![source_range],
|
source_ranges: vec![source_range],
|
||||||
}));
|
})),
|
||||||
}
|
}
|
||||||
exec_state.add_module(resolved_path.clone(), self, source_range).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exec_module(
|
async fn exec_module(
|
||||||
@ -2551,43 +2602,64 @@ impl ExecutorContext {
|
|||||||
// TODO It sucks that we have to clone the whole module AST here
|
// TODO It sucks that we have to clone the whole module AST here
|
||||||
let info = exec_state.global.module_infos[&module_id].clone();
|
let info = exec_state.global.module_infos[&module_id].clone();
|
||||||
|
|
||||||
let mut local_state = ModuleState {
|
match &info.repr {
|
||||||
import_stack: exec_state.mod_local.import_stack.clone(),
|
ModuleRepr::Root => Err(KclError::ImportCycle(KclErrorDetails {
|
||||||
..ModuleState::new(&self.settings)
|
message: format!(
|
||||||
};
|
"circular import of modules is not allowed: {} -> {}",
|
||||||
local_state.import_stack.push(info.path.clone());
|
exec_state
|
||||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
.mod_local
|
||||||
let original_execution = self.engine.replace_execution_kind(exec_kind);
|
.import_stack
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.as_path().to_string_lossy())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" -> "),
|
||||||
|
info.path.display()
|
||||||
|
),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
})),
|
||||||
|
ModuleRepr::Kcl(program) => {
|
||||||
|
let mut local_state = ModuleState {
|
||||||
|
import_stack: exec_state.mod_local.import_stack.clone(),
|
||||||
|
..ModuleState::new(&self.settings)
|
||||||
|
};
|
||||||
|
local_state.import_stack.push(info.path.clone());
|
||||||
|
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||||
|
let original_execution = self.engine.replace_execution_kind(exec_kind);
|
||||||
|
|
||||||
// The unwrap here is safe since we only elide the AST for the top module.
|
let result = self
|
||||||
let result = self
|
.inner_execute(program, exec_state, crate::execution::BodyType::Root)
|
||||||
.inner_execute(&info.parsed.unwrap(), exec_state, crate::execution::BodyType::Root)
|
.await;
|
||||||
.await;
|
|
||||||
|
|
||||||
let new_units = exec_state.length_unit();
|
let new_units = exec_state.length_unit();
|
||||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||||
if new_units != old_units {
|
if new_units != old_units {
|
||||||
self.engine.set_units(old_units.into(), Default::default()).await?;
|
self.engine.set_units(old_units.into(), Default::default()).await?;
|
||||||
}
|
}
|
||||||
self.engine.replace_execution_kind(original_execution);
|
self.engine.replace_execution_kind(original_execution);
|
||||||
|
|
||||||
let result = result.map_err(|err| {
|
let result = result.map_err(|err| {
|
||||||
if let KclError::ImportCycle(_) = err {
|
if let KclError::ImportCycle(_) = err {
|
||||||
// It was an import cycle. Keep the original message.
|
// It was an import cycle. Keep the original message.
|
||||||
err.override_source_ranges(vec![source_range])
|
err.override_source_ranges(vec![source_range])
|
||||||
} else {
|
} else {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Error loading imported file. Open it to view more details. {}: {}",
|
"Error loading imported file. Open it to view more details. {}: {}",
|
||||||
info.path.display(),
|
info.path.display(),
|
||||||
err.message()
|
err.message()
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
source_ranges: vec![source_range],
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((result, local_state.memory, local_state.module_exports))
|
||||||
}
|
}
|
||||||
})?;
|
ModuleRepr::Foreign(geom) => {
|
||||||
|
let geom = send_import_to_engine(geom.clone(), self).await?;
|
||||||
Ok((result, local_state.memory, local_state.module_exports))
|
Ok((Some(KclValue::ImportedGeometry(geom)), ProgramMemory::new(), Vec::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
@ -2608,15 +2680,20 @@ impl ExecutorContext {
|
|||||||
let (result, _, _) = self
|
let (result, _, _) = self
|
||||||
.exec_module(module_id, exec_state, ExecutionKind::Normal, metadata.source_range)
|
.exec_module(module_id, exec_state, ExecutionKind::Normal, metadata.source_range)
|
||||||
.await?;
|
.await?;
|
||||||
result.ok_or_else(|| {
|
result.unwrap_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
// The module didn't have a return value. Currently,
|
||||||
message: format!(
|
// the only way to have a return value is with the final
|
||||||
"Evaluating module `{}` as part of an assembly did not produce a result",
|
// statement being an expression statement.
|
||||||
identifier.name
|
//
|
||||||
),
|
// TODO: Make a warning when we support them in the
|
||||||
source_ranges: vec![metadata.source_range, meta[0].source_range],
|
// execution phase.
|
||||||
})
|
let mut new_meta = vec![metadata.to_owned()];
|
||||||
})?
|
new_meta.extend(meta);
|
||||||
|
KclValue::KclNone {
|
||||||
|
value: Default::default(),
|
||||||
|
meta: new_meta,
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
@ -3421,6 +3498,16 @@ const inInches = 1.0 * inch()"#;
|
|||||||
assert_eq!(1.0, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap());
|
assert_eq!(1.0, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_unit_overriden_in() {
|
||||||
|
let ast = r#"@settings(defaultLengthUnit = in)
|
||||||
|
const inMm = 25.4 * mm()
|
||||||
|
const inInches = 2.0 * inch()"#;
|
||||||
|
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||||
|
assert_eq!(1.0, mem_get_json(exec_state.memory(), "inMm").as_f64().unwrap().round());
|
||||||
|
assert_eq!(2.0, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_zero_param_fn() {
|
async fn test_zero_param_fn() {
|
||||||
let ast = r#"const sigmaAllow = 35000 // psi
|
let ast = r#"const sigmaAllow = 35000 // psi
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user