Compare commits
27 Commits
pierremtb/
...
v0.40.0
Author | SHA1 | Date | |
---|---|---|---|
5b207d7d1a | |||
2fac213c58 | |||
2f72a8ef14 | |||
27ce9f8aa4 | |||
b0426e3f94 | |||
d707c66e53 | |||
f8f44743fa | |||
f262eda12a | |||
9e1136195a | |||
4ff07ddaee | |||
1e565379a7 | |||
76e34ac4da | |||
4cd427bf91 | |||
f321ecdff0 | |||
d114ab798c | |||
69fec37107 | |||
8ca8c49cc3 | |||
b25fc302fd | |||
648b37c1dd | |||
18e5da5ca4 | |||
46524cda10 | |||
4585671a5d | |||
e7203b9e7a | |||
ab375f4b92 | |||
04ed6f52ee | |||
2332338ca1 | |||
41b97de3d1 |
56
.github/workflows/e2e-tests.yml
vendored
@ -126,20 +126,20 @@ jobs:
|
|||||||
- name: build electron
|
- name: build electron
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn tron:package
|
run: yarn tron:package
|
||||||
- name: Run ubuntu/chrome snapshots
|
# - name: Run ubuntu/chrome snapshots
|
||||||
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
|
# if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
|
||||||
shell: bash
|
# shell: bash
|
||||||
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
|
# # TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
|
||||||
# but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
|
# # but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
|
||||||
run: |
|
# run: |
|
||||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
|
# PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
|
||||||
env:
|
# env:
|
||||||
CI: true
|
# CI: true
|
||||||
NODE_ENV: development
|
# NODE_ENV: development
|
||||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
# VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
VITE_KC_SKIP_AUTH: true
|
# VITE_KC_SKIP_AUTH: true
|
||||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
# token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
# snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: ${{ !cancelled() && (success() || failure()) }}
|
if: ${{ !cancelled() && (success() || failure()) }}
|
||||||
with:
|
with:
|
||||||
@ -162,20 +162,20 @@ jobs:
|
|||||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
- name: Commit changes, if any
|
# - name: Commit changes, if any
|
||||||
if: steps.git-check.outputs.modified == 'true'
|
# if: steps.git-check.outputs.modified == 'true'
|
||||||
shell: bash
|
# shell: bash
|
||||||
run: |
|
# run: |
|
||||||
git add .
|
# git add .
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
# git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --local user.name "github-actions[bot]"
|
# git config --local user.name "github-actions[bot]"
|
||||||
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
# git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||||
git fetch origin
|
# git fetch origin
|
||||||
echo ${{ github.head_ref }}
|
# echo ${{ github.head_ref }}
|
||||||
git checkout ${{ github.head_ref }}
|
# git checkout ${{ github.head_ref }}
|
||||||
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
|
# git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
|
||||||
git push
|
# git push
|
||||||
git push origin ${{ github.head_ref }}
|
# git push origin ${{ github.head_ref }}
|
||||||
# only upload artifacts if there's actually changes
|
# only upload artifacts if there's actually changes
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: steps.git-check.outputs.modified == 'true'
|
if: steps.git-check.outputs.modified == 'true'
|
||||||
|
2
.gitignore
vendored
@ -44,7 +44,7 @@ e2e/playwright/temp3.png
|
|||||||
e2e/playwright/export-snapshots/*
|
e2e/playwright/export-snapshots/*
|
||||||
!e2e/playwright/export-snapshots/*.png
|
!e2e/playwright/export-snapshots/*.png
|
||||||
|
|
||||||
|
/kcl-samples
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
|
@ -4,14 +4,16 @@ 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.
|
||||||
|
|
||||||
For importing KCL functions using the `import` statement, see the docs on [KCL modules](/docs/kcl/modules).
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import(file_path: String, options?: ImportFormat) -> ImportedGeometry
|
import(file_path: String, options?: ImportFormat) -> ImportedGeometry
|
||||||
```
|
```
|
||||||
|
@ -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)
|
||||||
|
@ -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.",
|
||||||
"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'])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -4,7 +4,6 @@ import { expect } from '@playwright/test'
|
|||||||
type CmdBarSerialised =
|
type CmdBarSerialised =
|
||||||
| {
|
| {
|
||||||
stage: 'commandBarClosed'
|
stage: 'commandBarClosed'
|
||||||
// TODO no more properties needed but needs to be implemented in _serialiseCmdBar
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
stage: 'pickCommand'
|
stage: 'pickCommand'
|
||||||
@ -37,6 +36,9 @@ export class CmdBarFixture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
|
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
|
||||||
|
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
|
||||||
|
return { stage: 'commandBarClosed' }
|
||||||
|
}
|
||||||
const reviewForm = this.page.locator('#review-form')
|
const reviewForm = this.page.locator('#review-form')
|
||||||
const getHeaderArgs = async () => {
|
const getHeaderArgs = async () => {
|
||||||
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
|
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
|
||||||
@ -151,4 +153,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1078,7 +1078,7 @@ sketch002 = startSketchOn('XZ')
|
|||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Unable to sweep with the provided selection')
|
page.getByText('Unable to sweep with the current selection. Reason:')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1846,7 +1846,7 @@ sweep001 = sweep({ path = sketch002 }, sketch001)
|
|||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Unable to shell with the provided selection')
|
page.getByText('Unable to shell with the current selection. Reason:')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
})
|
})
|
||||||
|
@ -1525,7 +1525,7 @@ extrude001 = extrude(200, sketch001)`)
|
|||||||
test(
|
test(
|
||||||
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
|
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ context, page }, testInfo) => {
|
async ({ context, page, cmdBar, homePage }, testInfo) => {
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }),
|
fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }),
|
||||||
@ -1563,11 +1563,38 @@ test(
|
|||||||
|
|
||||||
const pointOnModel = { x: 630, y: 280 }
|
const pointOnModel = { x: 630, y: 280 }
|
||||||
|
|
||||||
await test.step('Opening the bracket project should load the stream', async () => {
|
await test.step('Opening the bracket project via command palette should load the stream', async () => {
|
||||||
// expect to see the text bracket
|
await homePage.expectState({
|
||||||
await expect(page.getByText('bracket')).toBeVisible()
|
projectCards: [
|
||||||
|
{
|
||||||
|
title: 'bracket',
|
||||||
|
fileCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'router-template-slate',
|
||||||
|
fileCount: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sortBy: 'last-modified-desc',
|
||||||
|
})
|
||||||
|
|
||||||
await page.getByText('bracket').click()
|
await cmdBar.openCmdBar()
|
||||||
|
await cmdBar.chooseCommand('open project')
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Open project',
|
||||||
|
currentArgKey: 'name',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Name: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'name',
|
||||||
|
})
|
||||||
|
await cmdBar.argumentInput.fill('brac')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'commandBarClosed',
|
||||||
|
})
|
||||||
|
|
||||||
await u.waitForPageLoad()
|
await u.waitForPageLoad()
|
||||||
|
|
||||||
@ -1588,7 +1615,7 @@ test(
|
|||||||
await expect(page.getByText('Create project')).toBeVisible()
|
await expect(page.getByText('Create project')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Opening the router-template project should load the stream', async () => {
|
await test.step('Opening the router-template project via link should load the stream', async () => {
|
||||||
// expect to see the text bracket
|
// expect to see the text bracket
|
||||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||||
|
|
||||||
@ -1605,16 +1632,26 @@ test(
|
|||||||
.toBeLessThan(15)
|
.toBeLessThan(15)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Opening the router-template project should load the stream', async () => {
|
await test.step('The projects on the home page should still be normal', async () => {
|
||||||
await page.getByTestId('project-sidebar-toggle').click()
|
await page.getByTestId('project-sidebar-toggle').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Go to Home' })
|
page.getByRole('button', { name: 'Go to Home' })
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Go to Home' }).click()
|
await page.getByRole('button', { name: 'Go to Home' }).click()
|
||||||
|
|
||||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
await homePage.expectState({
|
||||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
projectCards: [
|
||||||
await expect(page.getByText('Create project')).toBeVisible()
|
{
|
||||||
|
title: 'bracket',
|
||||||
|
fileCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'router-template-slate',
|
||||||
|
fileCount: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sortBy: 'last-modified-desc',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -99,6 +99,7 @@ test.describe('Sketch tests', () => {
|
|||||||
test('Can delete most of a sketch and the line tool will still work', async ({
|
test('Can delete most of a sketch and the line tool will still work', async ({
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
@ -112,12 +113,13 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
|
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit Sketch' })
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
).toBeEnabled({ timeout: 1000 })
|
).toBeEnabled({ timeout: 2000 })
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||||
|
|
||||||
@ -1405,3 +1407,46 @@ test.describe(`Click based selection don't brick the app when clicked out of ran
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4372
|
||||||
|
test.describe('Redirecting to home page and back to the original file should clear sketch DOM elements', () => {
|
||||||
|
test('Can redirect to home page and back to original file and have a cleared DOM', async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
scene,
|
||||||
|
toolbar,
|
||||||
|
editor,
|
||||||
|
homePage,
|
||||||
|
}) => {
|
||||||
|
// We seed the scene with a single offset plane
|
||||||
|
await context.addInitScript(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
` sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([256.85, 14.41], %)
|
||||||
|
|> lineTo([0, 211.07], %)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.waitForExecutionDone()
|
||||||
|
|
||||||
|
const [objClick] = scene.makeMouseHelpers(634, 274)
|
||||||
|
await objClick()
|
||||||
|
|
||||||
|
// Enter sketch mode
|
||||||
|
await toolbar.editSketch()
|
||||||
|
|
||||||
|
await expect(page.getByText('323.49')).toBeVisible()
|
||||||
|
|
||||||
|
// Open navigation side bar
|
||||||
|
await page.getByTestId('project-sidebar-toggle').click()
|
||||||
|
const goToHome = page.getByRole('button', {
|
||||||
|
name: 'Go to Home',
|
||||||
|
})
|
||||||
|
|
||||||
|
await goToHome.click()
|
||||||
|
await homePage.openProject('testDefault')
|
||||||
|
await expect(page.getByText('323.49')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@ -69,7 +69,6 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
await confirmButton.click()
|
await confirmButton.click()
|
||||||
|
|
||||||
await editor.expectEditor.toContain('// ' + newSample.title)
|
await editor.expectEditor.toContain('// ' + newSample.title)
|
||||||
await expect(unitsToast('in')).toBeVisible()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -158,7 +157,6 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
||||||
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
|
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
|
||||||
await expect(projectMenuButton).toContainText(sampleOne.file)
|
await expect(projectMenuButton).toContainText(sampleOne.file)
|
||||||
await expect(unitsToast('in')).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Now overwrite the current file`, async () => {
|
await test.step(`Now overwrite the current file`, async () => {
|
||||||
@ -188,7 +186,6 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
|
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
|
||||||
await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
|
await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
|
||||||
await expect(projectMenuButton).toContainText(sampleOne.file)
|
await expect(projectMenuButton).toContainText(sampleOne.file)
|
||||||
await expect(unitsToast('mm')).toBeVisible()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
|
20
src/App.tsx
@ -22,13 +22,29 @@ 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'
|
||||||
|
import { useToken } from 'machines/appMachine'
|
||||||
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()
|
||||||
@ -45,8 +61,8 @@ export function App() {
|
|||||||
|
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
|
|
||||||
const { auth, settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const token = auth?.context?.token
|
const token = useToken()
|
||||||
|
|
||||||
const coreDumpManager = useMemo(
|
const coreDumpManager = useMemo(
|
||||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
import { useAuthState } from 'machines/appMachine'
|
||||||
import Loading from './components/Loading'
|
import Loading from './components/Loading'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
|
|
||||||
// Wrapper around protected routes, used in src/Router.tsx
|
// Wrapper around protected routes, used in src/Router.tsx
|
||||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||||
const { auth } = useSettingsAuthContext()
|
const authState = useAuthState()
|
||||||
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
|
const isLoggingIn = authState.matches('checkIfLoggedIn')
|
||||||
|
|
||||||
return isLoggingIn ? (
|
return isLoggingIn ? (
|
||||||
<Loading>
|
<Loading>
|
||||||
|
@ -34,10 +34,9 @@ import {
|
|||||||
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 useHotkeyWrapper from 'lib/hotkeyWrapper'
|
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { coreDump } from 'lang/wasm'
|
import { coreDump } from 'lang/wasm'
|
||||||
@ -46,6 +45,8 @@ 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'
|
||||||
|
import { useToken } from 'machines/appMachine'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
|
|
||||||
@ -57,31 +58,42 @@ 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: (
|
||||||
<RouteProvider>
|
<OpenInDesktopAppHandler>
|
||||||
<SettingsAuthProvider>
|
<RouteProvider>
|
||||||
<LspProvider>
|
<SettingsAuthProvider>
|
||||||
<ProjectsContextProvider>
|
<LspProvider>
|
||||||
<KclContextProvider>
|
<ProjectsContextProvider>
|
||||||
<AppStateProvider>
|
<KclContextProvider>
|
||||||
<MachineManagerProvider>
|
<AppStateProvider>
|
||||||
<Outlet />
|
<MachineManagerProvider>
|
||||||
</MachineManagerProvider>
|
<Outlet />
|
||||||
</AppStateProvider>
|
</MachineManagerProvider>
|
||||||
</KclContextProvider>
|
</AppStateProvider>
|
||||||
</ProjectsContextProvider>
|
</KclContextProvider>
|
||||||
</LspProvider>
|
</ProjectsContextProvider>
|
||||||
</SettingsAuthProvider>
|
</LspProvider>
|
||||||
</RouteProvider>
|
</SettingsAuthProvider>
|
||||||
|
</RouteProvider>
|
||||||
|
</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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -191,8 +203,7 @@ export const Router = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CoreDump() {
|
function CoreDump() {
|
||||||
const { auth } = useSettingsAuthContext()
|
const token = useToken()
|
||||||
const token = auth?.context?.token
|
|
||||||
const coreDumpManager = useMemo(
|
const coreDumpManager = useMemo(
|
||||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||||
[]
|
[]
|
||||||
|
@ -124,6 +124,14 @@ export const ClientSideScene = ({
|
|||||||
'mouseup',
|
'mouseup',
|
||||||
toSync(sceneInfra.onMouseUp, reportRejection)
|
toSync(sceneInfra.onMouseUp, reportRejection)
|
||||||
)
|
)
|
||||||
|
sceneEntitiesManager
|
||||||
|
.tearDownSketch()
|
||||||
|
.then(() => {
|
||||||
|
// no op
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -2,11 +2,11 @@ import { Toolbar } from '../Toolbar'
|
|||||||
import UserSidebarMenu from 'components/UserSidebarMenu'
|
import UserSidebarMenu from 'components/UserSidebarMenu'
|
||||||
import { type IndexLoaderData } from 'lib/types'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import styles from './AppHeader.module.css'
|
import styles from './AppHeader.module.css'
|
||||||
import { RefreshButton } from 'components/RefreshButton'
|
import { RefreshButton } from 'components/RefreshButton'
|
||||||
import { CommandBarOpenButton } from './CommandBarOpenButton'
|
import { CommandBarOpenButton } from './CommandBarOpenButton'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
import { useUser } from 'machines/appMachine'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
@ -24,8 +24,7 @@ export const AppHeader = ({
|
|||||||
style,
|
style,
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const { auth } = useSettingsAuthContext()
|
const user = useUser()
|
||||||
const user = auth?.context?.user
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -98,6 +98,7 @@ export const CommandBar = () => {
|
|||||||
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
||||||
(isSelectionArgument ? 'pointer-events-none' : '')
|
(isSelectionArgument ? 'pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
|
data-testid="command-bar-wrapper"
|
||||||
>
|
>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
enter="duration-100 ease-out"
|
enter="duration-100 ease-out"
|
||||||
|
@ -75,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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
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'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useToken } from 'machines/appMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -48,7 +49,9 @@ export const FileMachineProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { project, file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const token = useToken()
|
||||||
|
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
|
const { project, file } = projectData
|
||||||
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@ -295,40 +298,47 @@ export const FileMachineProvider = ({
|
|||||||
|
|
||||||
const kclCommandMemo = useMemo(
|
const kclCommandMemo = useMemo(
|
||||||
() =>
|
() =>
|
||||||
kclCommands(
|
kclCommands({
|
||||||
async (data) => {
|
authToken: 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]
|
||||||
|
@ -27,6 +27,7 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
|||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { codeManager } from 'lib/singletons'
|
import { codeManager } from 'lib/singletons'
|
||||||
|
import { useToken } from 'machines/appMachine'
|
||||||
|
|
||||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||||
return []
|
return []
|
||||||
@ -69,8 +70,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||||
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||||
|
|
||||||
const { auth } = useSettingsAuthContext()
|
const token = useToken()
|
||||||
const token = auth?.context.token
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||||
|
@ -68,11 +68,8 @@ 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'
|
||||||
@ -92,6 +89,7 @@ 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'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
|
import { useToken } from 'machines/appMachine'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -113,7 +111,6 @@ export const ModelingMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
auth,
|
|
||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme, enableSSAO, allowOrbitInSketchMode },
|
app: { theme, enableSSAO, allowOrbitInSketchMode },
|
||||||
@ -130,7 +127,7 @@ export const ModelingMachineProvider = ({
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { context, send: fileMachineSend } = useFileContext()
|
const { context, send: fileMachineSend } = useFileContext()
|
||||||
const { file } = useLoaderData() as IndexLoaderData
|
const { file } = useLoaderData() as IndexLoaderData
|
||||||
const token = auth?.context?.token
|
const token = useToken()
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
const persistedContext = useMemo(() => getPersistedContext(), [])
|
const persistedContext = useMemo(() => getPersistedContext(), [])
|
||||||
|
|
||||||
|
@ -297,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}
|
||||||
@ -339,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${
|
||||||
|
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
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -9,7 +9,7 @@ import { Logo } from './Logo'
|
|||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
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'
|
||||||
@ -17,6 +17,10 @@ import Tooltip from './Tooltip'
|
|||||||
import { SnapshotFrom } from 'xstate'
|
import { SnapshotFrom } from 'xstate'
|
||||||
import { commandBarActor } from 'machines/commandBarMachine'
|
import { commandBarActor } from 'machines/commandBarMachine'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { copyFileShareLink } from 'lib/links'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { DEV } from 'env'
|
||||||
|
import { useToken } from 'machines/appMachine'
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
project,
|
project,
|
||||||
@ -100,6 +104,8 @@ function ProjectMenuPopover({
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
|
const { settings } = useSettingsAuthContext()
|
||||||
|
const token = useToken()
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const commands = useSelector(commandBarActor, commandsSelector)
|
const commands = useSelector(commandBarActor, commandsSelector)
|
||||||
|
|
||||||
@ -158,7 +164,6 @@ function ProjectMenuPopover({
|
|||||||
data: exportCommandInfo,
|
data: exportCommandInfo,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'break',
|
|
||||||
{
|
{
|
||||||
id: 'make',
|
id: 'make',
|
||||||
Element: 'button',
|
Element: 'button',
|
||||||
@ -184,6 +189,20 @@ function ProjectMenuPopover({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'share-link',
|
||||||
|
Element: 'button',
|
||||||
|
children: 'Share link to file',
|
||||||
|
disabled: !DEV,
|
||||||
|
onClick: async () => {
|
||||||
|
await copyFileShareLink({
|
||||||
|
token: token ?? '',
|
||||||
|
code: codeManager.code,
|
||||||
|
name: project?.name || '',
|
||||||
|
units: settings.context.modeling.defaultUnit.current,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
'break',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'go-home',
|
id: 'go-home',
|
||||||
|
@ -2,11 +2,11 @@ import { useMachine } from '@xstate/react'
|
|||||||
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,
|
||||||
@ -18,12 +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 { 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,18 +187,21 @@ const ProjectsContextDesktop = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
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,
|
||||||
@ -168,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) ||
|
||||||
@ -217,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
|
||||||
@ -241,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,
|
||||||
|
}
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@ -270,6 +479,7 @@ const ProjectsContextDesktop = ({
|
|||||||
state,
|
state,
|
||||||
commandBarConfig: projectsCommandBarConfig,
|
commandBarConfig: projectsCommandBarConfig,
|
||||||
actor,
|
actor,
|
||||||
|
onCancel: clearImportSearchParams,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -8,10 +8,10 @@ import Tooltip from './Tooltip'
|
|||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
|
import { useToken } from 'machines/appMachine'
|
||||||
|
|
||||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||||
const { auth } = useSettingsAuthContext()
|
const token = useToken()
|
||||||
const token = auth?.context?.token
|
|
||||||
const coreDumpManager = useMemo(
|
const coreDumpManager = useMemo(
|
||||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||||
[]
|
[]
|
||||||
|
@ -2,13 +2,16 @@ import { useEffect, useState, createContext, ReactNode } from 'react'
|
|||||||
import { useNavigation, useLocation } from 'react-router-dom'
|
import { useNavigation, useLocation } from 'react-router-dom'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { markOnce } from 'lib/performance'
|
import { markOnce } from 'lib/performance'
|
||||||
|
import { useAuthNavigation } from 'hooks/useAuthNavigation'
|
||||||
|
|
||||||
export const RouteProviderContext = createContext({})
|
export const RouteProviderContext = createContext({})
|
||||||
|
|
||||||
export function RouteProvider({ children }: { children: ReactNode }) {
|
export function RouteProvider({ children }: { children: ReactNode }) {
|
||||||
|
useAuthNavigation()
|
||||||
const [first, setFirstState] = useState(true)
|
const [first, setFirstState] = useState(true)
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// On initialization, the react-router-dom does not send a 'loading' state event.
|
// On initialization, the react-router-dom does not send a 'loading' state event.
|
||||||
// it sends an idle event first.
|
// it sends an idle event first.
|
||||||
|
@ -2,10 +2,7 @@ import { trap } from 'lib/trap'
|
|||||||
import { useMachine, useSelector } from '@xstate/react'
|
import { useMachine, useSelector } from '@xstate/react'
|
||||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
|
||||||
import withBaseUrl from '../lib/withBaseURL'
|
|
||||||
import React, { createContext, useEffect, useState } from 'react'
|
import React, { createContext, useEffect, useState } from 'react'
|
||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import {
|
import {
|
||||||
@ -16,7 +13,6 @@ import {
|
|||||||
} from 'lib/theme'
|
} from 'lib/theme'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
|
||||||
import {
|
import {
|
||||||
kclManager,
|
kclManager,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
@ -50,7 +46,6 @@ type MachineContext<T extends AnyStateMachine> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SettingsAuthContextType = {
|
type SettingsAuthContextType = {
|
||||||
auth: MachineContext<typeof authMachine>
|
|
||||||
settings: MachineContext<typeof settingsMachine>
|
settings: MachineContext<typeof settingsMachine>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,40 +365,9 @@ export const SettingsAuthProviderBase = ({
|
|||||||
)
|
)
|
||||||
}, [settingsState.context.textEditor.blinkingCursor.current])
|
}, [settingsState.context.textEditor.blinkingCursor.current])
|
||||||
|
|
||||||
// Auth machine setup
|
|
||||||
const [authState, authSend, authActor] = useMachine(
|
|
||||||
authMachine.provide({
|
|
||||||
actions: {
|
|
||||||
goToSignInPage: () => {
|
|
||||||
navigate(PATHS.SIGN_IN)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
logout()
|
|
||||||
},
|
|
||||||
goToIndexPage: () => {
|
|
||||||
if (location.pathname.includes(PATHS.SIGN_IN)) {
|
|
||||||
navigate(PATHS.INDEX)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
useStateMachineCommands({
|
|
||||||
machineId: 'auth',
|
|
||||||
state: authState,
|
|
||||||
send: authSend,
|
|
||||||
commandBarConfig: authCommandBarConfig,
|
|
||||||
actor: authActor,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsAuthContext.Provider
|
<SettingsAuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
auth: {
|
|
||||||
state: authState,
|
|
||||||
context: authState.context,
|
|
||||||
send: authSend,
|
|
||||||
},
|
|
||||||
settings: {
|
settings: {
|
||||||
state: settingsState,
|
state: settingsState,
|
||||||
context: settingsState.context,
|
context: settingsState.context,
|
||||||
@ -417,12 +381,3 @@ export const SettingsAuthProviderBase = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsAuthProvider
|
export default SettingsAuthProvider
|
||||||
|
|
||||||
export async function logout() {
|
|
||||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
|
||||||
if (isDesktop()) return Promise.resolve(null)
|
|
||||||
return fetch(withBaseUrl('/logout'), {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
@ -4,12 +4,12 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import { Fragment, useMemo, useState } from 'react'
|
import { Fragment, useMemo, useState } from 'react'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
import { authActor } from 'machines/appMachine'
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
const displayedName = getDisplayName(user)
|
const displayedName = getDisplayName(user)
|
||||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const send = useSettingsAuthContext()?.auth?.send
|
const send = authActor.send
|
||||||
|
|
||||||
// We filter this memoized list so that no orphan "break" elements are rendered.
|
// We filter this memoized list so that no orphan "break" elements are rendered.
|
||||||
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
|
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
|
||||||
|
29
src/hooks/useAuthNavigation.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { PATHS } from 'lib/paths'
|
||||||
|
import { useAuthState } from 'machines/appMachine'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple hook that listens to the auth state of the app and navigates
|
||||||
|
* accordingly.
|
||||||
|
*/
|
||||||
|
export function useAuthNavigation() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
const authState = useAuthState()
|
||||||
|
|
||||||
|
// Subscribe to the auth state of the app and navigate accordingly.
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
authState.matches('loggedIn') &&
|
||||||
|
location.pathname.includes(PATHS.SIGN_IN)
|
||||||
|
) {
|
||||||
|
navigate(PATHS.INDEX)
|
||||||
|
} else if (
|
||||||
|
authState.matches('loggedOut') &&
|
||||||
|
!location.pathname.includes(PATHS.SIGN_IN)
|
||||||
|
) {
|
||||||
|
navigate(PATHS.SIGN_IN)
|
||||||
|
}
|
||||||
|
}, [authState])
|
||||||
|
}
|
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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import {
|
|||||||
Identifier,
|
Identifier,
|
||||||
SourceRange,
|
SourceRange,
|
||||||
topLevelRange,
|
topLevelRange,
|
||||||
|
LiteralValue,
|
||||||
|
Literal,
|
||||||
} from './wasm'
|
} from './wasm'
|
||||||
import {
|
import {
|
||||||
createLiteral,
|
createLiteral,
|
||||||
@ -25,7 +27,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'
|
||||||
@ -36,10 +39,26 @@ beforeAll(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Testing createLiteral', () => {
|
describe('Testing createLiteral', () => {
|
||||||
it('should create a literal', () => {
|
it('should create a literal number without units', () => {
|
||||||
const result = createLiteral(5)
|
const result = createLiteral(5)
|
||||||
expect(result.type).toBe('Literal')
|
expect(result.type).toBe('Literal')
|
||||||
expect((result as any).value.value).toBe(5)
|
expect((result as any).value.value).toBe(5)
|
||||||
|
expect((result as any).value.suffix).toBe('None')
|
||||||
|
expect((result as Literal).raw).toBe('5')
|
||||||
|
})
|
||||||
|
it('should create a literal number with units', () => {
|
||||||
|
const lit: LiteralValue = { value: 5, suffix: 'Mm' }
|
||||||
|
const result = createLiteral(lit)
|
||||||
|
expect(result.type).toBe('Literal')
|
||||||
|
expect((result as any).value.value).toBe(5)
|
||||||
|
expect((result as any).value.suffix).toBe('Mm')
|
||||||
|
expect((result as Literal).raw).toBe('5mm')
|
||||||
|
})
|
||||||
|
it('should create a literal boolean', () => {
|
||||||
|
const result = createLiteral(false)
|
||||||
|
expect(result.type).toBe('Literal')
|
||||||
|
expect((result as Literal).value).toBe(false)
|
||||||
|
expect((result as Literal).raw).toBe('false')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('Testing createIdentifier', () => {
|
describe('Testing createIdentifier', () => {
|
||||||
|
@ -20,16 +20,17 @@ import {
|
|||||||
SourceRange,
|
SourceRange,
|
||||||
sketchFromKclValue,
|
sketchFromKclValue,
|
||||||
isPathToNodeNumber,
|
isPathToNodeNumber,
|
||||||
|
formatNumber,
|
||||||
} from './wasm'
|
} from './wasm'
|
||||||
import {
|
import {
|
||||||
isNodeSafeToReplacePath,
|
isNodeSafeToReplacePath,
|
||||||
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,
|
||||||
@ -743,11 +744,26 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
|
|||||||
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: This depends on WASM, but it's not async. Callers are responsible for
|
||||||
|
* awaiting init of the WASM module.
|
||||||
|
*/
|
||||||
export function createLiteral(value: LiteralValue | number): Node<Literal> {
|
export function createLiteral(value: LiteralValue | number): Node<Literal> {
|
||||||
const raw = `${value}`
|
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
value = { value, suffix: 'None' }
|
value = { value, suffix: 'None' }
|
||||||
}
|
}
|
||||||
|
let raw: string
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// TODO: Should we handle escape sequences?
|
||||||
|
raw = `${value}`
|
||||||
|
} else if (typeof value === 'boolean') {
|
||||||
|
raw = `${value}`
|
||||||
|
} else if (typeof value.value === 'number' && value.suffix === 'None') {
|
||||||
|
// Fast path for numbers when there are no units.
|
||||||
|
raw = `${value.value}`
|
||||||
|
} else {
|
||||||
|
raw = formatNumber(value.value, value.suffix)
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
type: 'Literal',
|
type: 'Literal',
|
||||||
start: 0,
|
start: 0,
|
||||||
|
@ -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,
|
||||||
|
@ -5,12 +5,15 @@ import {
|
|||||||
PathToNode,
|
PathToNode,
|
||||||
Identifier,
|
Identifier,
|
||||||
topLevelRange,
|
topLevelRange,
|
||||||
|
PipeExpression,
|
||||||
|
CallExpression,
|
||||||
|
VariableDeclarator,
|
||||||
} from './wasm'
|
} from './wasm'
|
||||||
|
import { ProgramMemory } from 'lang/wasm'
|
||||||
import {
|
import {
|
||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
isTypeInValue,
|
isTypeInValue,
|
||||||
getNodePathFromSourceRange,
|
|
||||||
hasExtrudeSketch,
|
hasExtrudeSketch,
|
||||||
findUsesOfTagInPipe,
|
findUsesOfTagInPipe,
|
||||||
hasSketchPipeBeenExtruded,
|
hasSketchPipeBeenExtruded,
|
||||||
@ -19,15 +22,18 @@ 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,
|
||||||
createCallExpression,
|
createCallExpression,
|
||||||
createLiteral,
|
createLiteral,
|
||||||
createPipeSubstitution,
|
createPipeSubstitution,
|
||||||
|
createCallExpressionStdLib,
|
||||||
} from './modifyAst'
|
} from './modifyAst'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { codeRefFromRange } from './std/artifactGraph'
|
import { codeRefFromRange } from './std/artifactGraph'
|
||||||
|
import { addCallExpressionsToPipe, addCloseToPipe } from 'lang/std/sketch'
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initPromise
|
await initPromise
|
||||||
@ -680,3 +686,115 @@ myNestedVar = [
|
|||||||
expect(pathToNode).toEqual(pathToNode2)
|
expect(pathToNode).toEqual(pathToNode2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Testing specific sketch getNodeFromPath workflow', () => {
|
||||||
|
it('should parse the code', () => {
|
||||||
|
const openSketch = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.02, 0.22], %)
|
||||||
|
|> xLine(0.39, %)
|
||||||
|
|> line([0.02, -0.17], %)
|
||||||
|
|> yLine(-0.15, %)
|
||||||
|
|> line([-0.21, -0.02], %)
|
||||||
|
|> xLine(-0.15, %)
|
||||||
|
|> line([-0.02, 0.21], %)
|
||||||
|
|> line([-0.08, 0.05], %)`
|
||||||
|
const ast = assertParse(openSketch)
|
||||||
|
expect(ast.start).toEqual(0)
|
||||||
|
expect(ast.end).toEqual(227)
|
||||||
|
})
|
||||||
|
it('should find the location to add new lineTo', () => {
|
||||||
|
const openSketch = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.02, 0.22], %)
|
||||||
|
|> xLine(0.39, %)
|
||||||
|
|> line([0.02, -0.17], %)
|
||||||
|
|> yLine(-0.15, %)
|
||||||
|
|> line([-0.21, -0.02], %)
|
||||||
|
|> xLine(-0.15, %)
|
||||||
|
|> line([-0.02, 0.21], %)
|
||||||
|
|> line([-0.08, 0.05], %)`
|
||||||
|
const ast = assertParse(openSketch)
|
||||||
|
|
||||||
|
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
|
||||||
|
const sketchRange = topLevelRange(
|
||||||
|
openSketch.indexOf(sketchSnippet),
|
||||||
|
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
|
||||||
|
)
|
||||||
|
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
|
||||||
|
const modifiedAst = addCallExpressionsToPipe({
|
||||||
|
node: ast,
|
||||||
|
programMemory: ProgramMemory.empty(),
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
expressions: [
|
||||||
|
createCallExpressionStdLib(
|
||||||
|
'lineTo', // We are forcing lineTo!
|
||||||
|
[
|
||||||
|
createArrayExpression([
|
||||||
|
createCallExpressionStdLib('profileStartX', [
|
||||||
|
createPipeSubstitution(),
|
||||||
|
]),
|
||||||
|
createCallExpressionStdLib('profileStartY', [
|
||||||
|
createPipeSubstitution(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
createPipeSubstitution(),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
if (err(modifiedAst)) throw modifiedAst
|
||||||
|
const recasted = recast(modifiedAst)
|
||||||
|
const expectedCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.02, 0.22], %)
|
||||||
|
|> xLine(0.39, %)
|
||||||
|
|> line([0.02, -0.17], %)
|
||||||
|
|> yLine(-0.15, %)
|
||||||
|
|> line([-0.21, -0.02], %)
|
||||||
|
|> xLine(-0.15, %)
|
||||||
|
|> line([-0.02, 0.21], %)
|
||||||
|
|> line([-0.08, 0.05], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
`
|
||||||
|
expect(recasted).toEqual(expectedCode)
|
||||||
|
})
|
||||||
|
it('it should find the location to add close', () => {
|
||||||
|
const openSketch = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.02, 0.22], %)
|
||||||
|
|> xLine(0.39, %)
|
||||||
|
|> line([0.02, -0.17], %)
|
||||||
|
|> yLine(-0.15, %)
|
||||||
|
|> line([-0.21, -0.02], %)
|
||||||
|
|> xLine(-0.15, %)
|
||||||
|
|> line([-0.02, 0.21], %)
|
||||||
|
|> line([-0.08, 0.05], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
`
|
||||||
|
const ast = assertParse(openSketch)
|
||||||
|
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
|
||||||
|
const sketchRange = topLevelRange(
|
||||||
|
openSketch.indexOf(sketchSnippet),
|
||||||
|
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
|
||||||
|
)
|
||||||
|
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
|
||||||
|
const modifiedAst = addCloseToPipe({
|
||||||
|
node: ast,
|
||||||
|
programMemory: ProgramMemory.empty(),
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (err(modifiedAst)) throw modifiedAst
|
||||||
|
const recasted = recast(modifiedAst)
|
||||||
|
const expectedCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0.02, 0.22], %)
|
||||||
|
|> xLine(0.39, %)
|
||||||
|
|> line([0.02, -0.17], %)
|
||||||
|
|> yLine(-0.15, %)
|
||||||
|
|> line([-0.21, -0.02], %)
|
||||||
|
|> xLine(-0.15, %)
|
||||||
|
|> line([-0.02, 0.21], %)
|
||||||
|
|> line([-0.08, 0.05], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
`
|
||||||
|
expect(recasted).toEqual(expectedCode)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -21,7 +21,9 @@ import {
|
|||||||
topLevelRange,
|
topLevelRange,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
|
recast,
|
||||||
} 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'
|
||||||
@ -67,7 +69,28 @@ export function getNodeFromPath<T>(
|
|||||||
deepPath: successfulPaths,
|
deepPath: successfulPaths,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Error('not an object')
|
const stackTraceError = new Error()
|
||||||
|
const sourceCode = recast(node)
|
||||||
|
const levels = stackTraceError.stack?.split('\n')
|
||||||
|
const aFewFunctionNames: string[] = []
|
||||||
|
let tree = ''
|
||||||
|
levels?.forEach((val, index) => {
|
||||||
|
const fnName = val.trim().split(' ')[1]
|
||||||
|
const ending = index === levels.length - 1 ? ' ' : ' > '
|
||||||
|
tree += fnName + ending
|
||||||
|
if (index < 3) {
|
||||||
|
aFewFunctionNames.push(fnName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const error = new Error(
|
||||||
|
`Failed to stopAt ${stopAt}, ${aFewFunctionNames
|
||||||
|
.filter((a) => a)
|
||||||
|
.join(' > ')}`
|
||||||
|
)
|
||||||
|
console.error(tree)
|
||||||
|
console.error(sourceCode)
|
||||||
|
console.error(error.stack)
|
||||||
|
return error
|
||||||
}
|
}
|
||||||
currentNode = currentNode?.[pathItem[0]]
|
currentNode = currentNode?.[pathItem[0]]
|
||||||
successfulPaths.push(pathItem)
|
successfulPaths.push(pathItem)
|
||||||
@ -125,311 +148,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
@ -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'
|
||||||
|
@ -1999,7 +1999,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
|
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
|
||||||
/*noop*/
|
/*noop*/
|
||||||
return null
|
return e
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -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,
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
ArrayExpression,
|
ArrayExpression,
|
||||||
BinaryExpression,
|
BinaryExpression,
|
||||||
ArtifactGraph,
|
ArtifactGraph,
|
||||||
|
LiteralValue,
|
||||||
|
NumericSuffix,
|
||||||
} from './wasm'
|
} from './wasm'
|
||||||
import { filterArtifacts } from 'lang/std/artifactGraph'
|
import { filterArtifacts } from 'lang/std/artifactGraph'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
@ -69,3 +71,15 @@ export function isLiteral(e: any): e is Literal {
|
|||||||
export function isBinaryExpression(e: any): e is BinaryExpression {
|
export function isBinaryExpression(e: any): e is BinaryExpression {
|
||||||
return e && e.type === 'BinaryExpression'
|
return e && e.type === 'BinaryExpression'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLiteralValueNumber(
|
||||||
|
e: LiteralValue
|
||||||
|
): e is { value: number; suffix: NumericSuffix } {
|
||||||
|
return (
|
||||||
|
typeof e === 'object' &&
|
||||||
|
'value' in e &&
|
||||||
|
typeof e.value === 'number' &&
|
||||||
|
'suffix' in e &&
|
||||||
|
typeof e.suffix === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { initPromise, parse, ParseResult } from './wasm'
|
import { formatNumber, initPromise, parse, ParseResult } from './wasm'
|
||||||
import { enginelessExecutor } from 'lib/testHelpers'
|
import { enginelessExecutor } from 'lib/testHelpers'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { Program } from '../wasm-lib/kcl/bindings/Program'
|
import { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
@ -20,3 +20,12 @@ it('can execute parsed AST', async () => {
|
|||||||
expect(err(execState)).toEqual(false)
|
expect(err(execState)).toEqual(false)
|
||||||
expect(execState.memory.get('x')?.value).toEqual(1)
|
expect(execState.memory.get('x')?.value).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('formats numbers with units', () => {
|
||||||
|
expect(formatNumber(1, 'None')).toEqual('1')
|
||||||
|
expect(formatNumber(1, 'Count')).toEqual('1_')
|
||||||
|
expect(formatNumber(1, 'Mm')).toEqual('1mm')
|
||||||
|
expect(formatNumber(1, 'Inch')).toEqual('1in')
|
||||||
|
expect(formatNumber(0.5, 'Mm')).toEqual('0.5mm')
|
||||||
|
expect(formatNumber(-0.5, 'Mm')).toEqual('-0.5mm')
|
||||||
|
})
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
init,
|
init,
|
||||||
parse_wasm,
|
parse_wasm,
|
||||||
recast_wasm,
|
recast_wasm,
|
||||||
|
format_number,
|
||||||
execute,
|
execute,
|
||||||
kcl_lint,
|
kcl_lint,
|
||||||
modify_ast_for_sketch_wasm,
|
modify_ast_for_sketch_wasm,
|
||||||
@ -17,6 +18,7 @@ import {
|
|||||||
default_project_settings,
|
default_project_settings,
|
||||||
base64_decode,
|
base64_decode,
|
||||||
clear_scene_and_bust_cache,
|
clear_scene_and_bust_cache,
|
||||||
|
change_kcl_settings,
|
||||||
reloadModule,
|
reloadModule,
|
||||||
} from 'lib/wasm_lib_wrapper'
|
} from 'lib/wasm_lib_wrapper'
|
||||||
|
|
||||||
@ -53,7 +55,9 @@ 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'
|
||||||
|
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
|
||||||
|
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
|
||||||
|
|
||||||
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'
|
||||||
@ -90,6 +94,7 @@ export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
|
|||||||
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
|
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
|
||||||
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
||||||
export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
|
||||||
|
export type { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
|
||||||
|
|
||||||
export type SyntaxType =
|
export type SyntaxType =
|
||||||
| 'Program'
|
| 'Program'
|
||||||
@ -566,9 +571,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 +605,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,
|
||||||
@ -627,6 +643,13 @@ export const recast = (ast: Program): string | Error => {
|
|||||||
return recast_wasm(JSON.stringify(ast))
|
return recast_wasm(JSON.stringify(ast))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a number with suffix as KCL.
|
||||||
|
*/
|
||||||
|
export function formatNumber(value: number, suffix: NumericSuffix): string {
|
||||||
|
return format_number(value, JSON.stringify(suffix))
|
||||||
|
}
|
||||||
|
|
||||||
export const makeDefaultPlanes = async (
|
export const makeDefaultPlanes = async (
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
): Promise<DefaultPlanes> => {
|
): Promise<DefaultPlanes> => {
|
||||||
@ -823,3 +846,17 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
|
|||||||
return new Error('Caught error decoding base64 string: ' + e)
|
return new Error('Caught error decoding base64 string: ' + e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change the meta settings for the kcl file.
|
||||||
|
/// Returns the new kcl string with the updated settings.
|
||||||
|
export function changeKclSettings(
|
||||||
|
kcl: string,
|
||||||
|
settings: MetaSettings
|
||||||
|
): string | Error {
|
||||||
|
try {
|
||||||
|
return change_kcl_settings(kcl, JSON.stringify(settings))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Caught error changing kcl settings: ' + e)
|
||||||
|
return new Error('Caught error changing kcl settings: ' + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
@ -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)
|
||||||
|
}
|
@ -1,17 +1,14 @@
|
|||||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
import { Command } from 'lib/commandTypes'
|
||||||
import { authMachine } from 'machines/authMachine'
|
import { authActor } from 'machines/appMachine'
|
||||||
|
import { ACTOR_IDS } from 'machines/machineConstants'
|
||||||
|
|
||||||
type AuthCommandSchema = {}
|
export const authCommands: Command[] = [
|
||||||
|
{
|
||||||
export const authCommandBarConfig: StateMachineCommandSetConfig<
|
groupId: ACTOR_IDS.AUTH,
|
||||||
typeof authMachine,
|
name: 'log-out',
|
||||||
AuthCommandSchema
|
displayName: 'Log out',
|
||||||
> = {
|
|
||||||
'Log in': {
|
|
||||||
hide: 'both',
|
|
||||||
},
|
|
||||||
'Log out': {
|
|
||||||
args: [],
|
|
||||||
icon: 'arrowLeft',
|
icon: 'arrowLeft',
|
||||||
|
needsReview: false,
|
||||||
|
onSubmit: () => authActor.send({ type: 'Log out' }),
|
||||||
},
|
},
|
||||||
}
|
]
|
||||||
|
@ -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,22 +36,23 @@ 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',
|
||||||
required: true,
|
required: true,
|
||||||
options: [],
|
options: (_, context) =>
|
||||||
optionsFromContext: (context) =>
|
context?.projects.map((p) => ({
|
||||||
context.projects.map((p) => ({
|
name: p.name,
|
||||||
name: p.name!,
|
value: p.name,
|
||||||
value: p.name!,
|
})) || [],
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'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 +64,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 +87,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 +105,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.`
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
19
src/lib/commandBarConfigs/validators.test.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { parseEngineErrorMessage } from './validators'
|
||||||
|
|
||||||
|
describe('parseEngineErrorMessage', () => {
|
||||||
|
it('takes an engine error string and parses its json message', () => {
|
||||||
|
const engineError =
|
||||||
|
'engine error: [{"error_code":"internal_engine","message":"Trajectory curve must be G1 continuous (with continuous tangents)"}]'
|
||||||
|
const message = parseEngineErrorMessage(engineError)
|
||||||
|
expect(message).toEqual(
|
||||||
|
'Trajectory curve must be G1 continuous (with continuous tangents)'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retuns undefined on strings with different formats', () => {
|
||||||
|
const s1 = 'engine error: []'
|
||||||
|
const s2 = 'blabla'
|
||||||
|
expect(parseEngineErrorMessage(s1)).toBeUndefined()
|
||||||
|
expect(parseEngineErrorMessage(s2)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
@ -3,6 +3,7 @@ import { engineCommandManager } from 'lib/singletons'
|
|||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { CommandBarContext } from 'machines/commandBarMachine'
|
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
|
import { ApiError_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
|
||||||
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
|
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
|
||||||
for (let tries = 0; tries < numberOfRetries; tries++) {
|
for (let tries = 0; tries < numberOfRetries; tries++) {
|
||||||
@ -46,6 +47,20 @@ function isSelections(selections: unknown): selections is Selections {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseEngineErrorMessage(engineError: string) {
|
||||||
|
const parts = engineError.split('engine error: ')
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = JSON.parse(parts[1]) as ApiError_type[]
|
||||||
|
if (!errors[0]) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors[0].message
|
||||||
|
}
|
||||||
|
|
||||||
export const revolveAxisValidator = async ({
|
export const revolveAxisValidator = async ({
|
||||||
data,
|
data,
|
||||||
context,
|
context,
|
||||||
@ -83,7 +98,7 @@ export const revolveAxisValidator = async ({
|
|||||||
value: 360,
|
value: 360,
|
||||||
}
|
}
|
||||||
|
|
||||||
const revolveAboutEdgeCommand = async () => {
|
const command = async () => {
|
||||||
return await engineCommandManager.sendSceneCommand({
|
return await engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
@ -96,13 +111,13 @@ export const revolveAxisValidator = async ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
|
const result = await dryRunWrapper(command)
|
||||||
if (attemptRevolve?.success) {
|
if (result?.success) {
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
// return error message for the toast
|
|
||||||
return 'Unable to revolve with selected edge'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||||
|
return `Unable to revolve with the current selection. Reason: ${reason}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loftValidator = async ({
|
export const loftValidator = async ({
|
||||||
@ -128,7 +143,7 @@ export const loftValidator = async ({
|
|||||||
return 'Unable to loft, selection contains less than two solid2ds'
|
return 'Unable to loft, selection contains less than two solid2ds'
|
||||||
}
|
}
|
||||||
|
|
||||||
const loftCommand = async () => {
|
const command = async () => {
|
||||||
// TODO: check what to do with these
|
// TODO: check what to do with these
|
||||||
const DEFAULT_V_DEGREE = 2
|
const DEFAULT_V_DEGREE = 2
|
||||||
const DEFAULT_TOLERANCE = 2
|
const DEFAULT_TOLERANCE = 2
|
||||||
@ -145,13 +160,13 @@ export const loftValidator = async ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const attempt = await dryRunWrapper(loftCommand)
|
const result = await dryRunWrapper(command)
|
||||||
if (attempt?.success) {
|
if (result?.success) {
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
// return error message for the toast
|
|
||||||
return 'Unable to loft with selected sketches'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||||
|
return `Unable to loft with the current selection. Reason: ${reason}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shellValidator = async ({
|
export const shellValidator = async ({
|
||||||
@ -180,7 +195,7 @@ export const shellValidator = async ({
|
|||||||
return "Unable to shell, couldn't find the solid"
|
return "Unable to shell, couldn't find the solid"
|
||||||
}
|
}
|
||||||
|
|
||||||
const shellCommand = async () => {
|
const command = async () => {
|
||||||
// TODO: figure out something better than an arbitrarily small value
|
// TODO: figure out something better than an arbitrarily small value
|
||||||
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
|
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
|
||||||
const DEFAULT_HOLLOW = false
|
const DEFAULT_HOLLOW = false
|
||||||
@ -200,12 +215,13 @@ export const shellValidator = async ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const attemptShell = await dryRunWrapper(shellCommand)
|
const result = await dryRunWrapper(command)
|
||||||
if (attemptShell?.success) {
|
if (result?.success) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Unable to shell with the provided selection'
|
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||||
|
return `Unable to shell with the current selection. Reason: ${reason}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sweepValidator = async ({
|
export const sweepValidator = async ({
|
||||||
@ -241,7 +257,7 @@ export const sweepValidator = async ({
|
|||||||
}
|
}
|
||||||
const target = targetArtifact.pathId
|
const target = targetArtifact.pathId
|
||||||
|
|
||||||
const sweepCommand = async () => {
|
const command = async () => {
|
||||||
// TODO: second look on defaults here
|
// TODO: second look on defaults here
|
||||||
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
|
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
|
||||||
const DEFAULT_SECTIONAL = false
|
const DEFAULT_SECTIONAL = false
|
||||||
@ -261,10 +277,11 @@ export const sweepValidator = async ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const attemptSweep = await dryRunWrapper(sweepCommand)
|
const result = await dryRunWrapper(command)
|
||||||
if (attemptSweep?.success) {
|
if (result?.success) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Unable to sweep with the provided selection'
|
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||||
|
return `Unable to sweep with the current selection. Reason: ${reason}`
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -2,11 +2,10 @@ import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarnin
|
|||||||
import { Command, CommandArgumentOption } from './commandTypes'
|
import { Command, CommandArgumentOption } from './commandTypes'
|
||||||
import { kclManager } from './singletons'
|
import { kclManager } from './singletons'
|
||||||
import { isDesktop } from './isDesktop'
|
import { isDesktop } from './isDesktop'
|
||||||
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
|
import { FILE_EXT } 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 { reportRejection } from './trap'
|
||||||
import { err, reportRejection } from './trap'
|
import { IndexLoaderData } from './types'
|
||||||
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
|
|
||||||
|
|
||||||
interface OnSubmitProps {
|
interface OnSubmitProps {
|
||||||
sampleName: string
|
sampleName: string
|
||||||
@ -15,10 +14,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',
|
||||||
@ -55,59 +65,28 @@ export function kclCommands(
|
|||||||
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
|
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
|
||||||
projectPathPart
|
projectPathPart
|
||||||
)}/${encodeURIComponent(primaryKclFile)}`
|
)}/${encodeURIComponent(primaryKclFile)}`
|
||||||
const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
|
|
||||||
projectPathPart
|
|
||||||
)}/${PROJECT_SETTINGS_FILE_NAME}`
|
|
||||||
|
|
||||||
Promise.allSettled([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)])
|
fetch(sampleCodeUrl)
|
||||||
.then((results) => {
|
.then(async (codeResponse): Promise<OnSubmitProps> => {
|
||||||
const a =
|
if (!codeResponse.ok) {
|
||||||
'value' in results[0] ? results[0].value : results[0].reason
|
console.error(
|
||||||
const b =
|
'Failed to fetch sample code:',
|
||||||
'value' in results[1] ? results[1].value : results[1].reason
|
codeResponse.statusText
|
||||||
return [a, b]
|
)
|
||||||
})
|
return Promise.reject(new Error('Failed to fetch sample code'))
|
||||||
.then(
|
|
||||||
async ([
|
|
||||||
codeResponse,
|
|
||||||
settingsResponse,
|
|
||||||
]): Promise<OnSubmitProps> => {
|
|
||||||
if (!codeResponse.ok) {
|
|
||||||
console.error(
|
|
||||||
'Failed to fetch sample code:',
|
|
||||||
codeResponse.statusText
|
|
||||||
)
|
|
||||||
return Promise.reject(new Error('Failed to fetch sample code'))
|
|
||||||
}
|
|
||||||
const code = await codeResponse.text()
|
|
||||||
|
|
||||||
// It's possible that a sample doesn't have a project.toml
|
|
||||||
// associated with it.
|
|
||||||
let projectSettingsPayload: ReturnType<
|
|
||||||
typeof projectConfigurationToSettingsPayload
|
|
||||||
> = {}
|
|
||||||
if (settingsResponse.ok) {
|
|
||||||
const parsedProjectSettings = parseProjectSettings(
|
|
||||||
await settingsResponse.text()
|
|
||||||
)
|
|
||||||
if (!err(parsedProjectSettings)) {
|
|
||||||
projectSettingsPayload =
|
|
||||||
projectConfigurationToSettingsPayload(parsedProjectSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sampleName: data.sample.split('/')[0] + FILE_EXT,
|
|
||||||
code,
|
|
||||||
method: data.method,
|
|
||||||
sampleUnits:
|
|
||||||
projectSettingsPayload.modeling?.defaultUnit || 'mm',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
const code = await codeResponse.text()
|
||||||
|
return {
|
||||||
|
sampleName: data.sample.split('/')[0] + FILE_EXT,
|
||||||
|
code,
|
||||||
|
method: data.method,
|
||||||
|
}
|
||||||
|
})
|
||||||
.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 +128,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
@ -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
@ -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()
|
||||||
|
}
|
||||||
|
}
|
92
src/lib/rectangleTool.test.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { expect } from 'vitest'
|
||||||
|
import {
|
||||||
|
recast,
|
||||||
|
assertParse,
|
||||||
|
topLevelRange,
|
||||||
|
VariableDeclaration,
|
||||||
|
initPromise,
|
||||||
|
} from 'lang/wasm'
|
||||||
|
import { updateCenterRectangleSketch } from './rectangleTool'
|
||||||
|
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||||
|
import { getNodeFromPath } from 'lang/queryAst'
|
||||||
|
import { findUniqueName } from 'lang/modifyAst'
|
||||||
|
import { err, trap } from './trap'
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initPromise
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('library rectangleTool helper functions', () => {
|
||||||
|
describe('updateCenterRectangleSketch', () => {
|
||||||
|
// regression test for https://github.com/KittyCAD/modeling-app/issues/5157
|
||||||
|
test('should update AST and source code', async () => {
|
||||||
|
// Base source code that will be edited in place
|
||||||
|
const sourceCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([120.37, 162.76], %)
|
||||||
|
|> angledLine([0, 0], %, $rectangleSegmentA001)
|
||||||
|
|> angledLine([segAng(rectangleSegmentA001) + 90, 0], %, $rectangleSegmentB001)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001),
|
||||||
|
-segLen(rectangleSegmentA001)
|
||||||
|
], %, $rectangleSegmentC001)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
`
|
||||||
|
// Create ast
|
||||||
|
const _ast = assertParse(sourceCode)
|
||||||
|
let ast = structuredClone(_ast)
|
||||||
|
|
||||||
|
// Find some nodes and paths to reference
|
||||||
|
const sketchSnippet = `startProfileAt([120.37, 162.76], %)`
|
||||||
|
const sketchRange = topLevelRange(
|
||||||
|
sourceCode.indexOf(sketchSnippet),
|
||||||
|
sourceCode.indexOf(sketchSnippet) + sketchSnippet.length
|
||||||
|
)
|
||||||
|
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
|
||||||
|
const _node = getNodeFromPath<VariableDeclaration>(
|
||||||
|
ast,
|
||||||
|
sketchPathToNode || [],
|
||||||
|
'VariableDeclaration'
|
||||||
|
)
|
||||||
|
if (trap(_node)) return
|
||||||
|
const sketchInit = _node.node?.declaration.init
|
||||||
|
|
||||||
|
// Hard code inputs that a user would have taken with their mouse
|
||||||
|
const x = 40
|
||||||
|
const y = 60
|
||||||
|
const rectangleOrigin = [120, 180]
|
||||||
|
const tags: [string, string, string] = [
|
||||||
|
'rectangleSegmentA001',
|
||||||
|
'rectangleSegmentB001',
|
||||||
|
'rectangleSegmentC001',
|
||||||
|
]
|
||||||
|
|
||||||
|
// Update the ast
|
||||||
|
if (sketchInit.type === 'PipeExpression') {
|
||||||
|
updateCenterRectangleSketch(
|
||||||
|
sketchInit,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
tags[0],
|
||||||
|
rectangleOrigin[0],
|
||||||
|
rectangleOrigin[1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ast is edited in place from the updateCenterRectangleSketch
|
||||||
|
const expectedSourceCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([80, 120], %)
|
||||||
|
|> angledLine([0, 80], %, $rectangleSegmentA001)
|
||||||
|
|> angledLine([segAng(rectangleSegmentA001) + 90, 120], %, $rectangleSegmentB001)
|
||||||
|
|> angledLine([
|
||||||
|
segAng(rectangleSegmentA001),
|
||||||
|
-segLen(rectangleSegmentA001)
|
||||||
|
], %, $rectangleSegmentC001)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
`
|
||||||
|
const recasted = recast(ast)
|
||||||
|
expect(recasted).toEqual(expectedSourceCode)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -8,13 +8,19 @@ import {
|
|||||||
createTagDeclarator,
|
createTagDeclarator,
|
||||||
createUnaryExpression,
|
createUnaryExpression,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
|
import {
|
||||||
|
ArrayExpression,
|
||||||
|
CallExpression,
|
||||||
|
PipeExpression,
|
||||||
|
recast,
|
||||||
|
} from 'lang/wasm'
|
||||||
import { roundOff } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import {
|
import {
|
||||||
isCallExpression,
|
isCallExpression,
|
||||||
isArrayExpression,
|
isArrayExpression,
|
||||||
isLiteral,
|
isLiteral,
|
||||||
isBinaryExpression,
|
isBinaryExpression,
|
||||||
|
isLiteralValueNumber,
|
||||||
} from 'lang/util'
|
} from 'lang/util'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,10 +146,12 @@ export function updateCenterRectangleSketch(
|
|||||||
if (isArrayExpression(arrayExpression)) {
|
if (isArrayExpression(arrayExpression)) {
|
||||||
const literal = arrayExpression.elements[0]
|
const literal = arrayExpression.elements[0]
|
||||||
if (isLiteral(literal)) {
|
if (isLiteral(literal)) {
|
||||||
callExpression.arguments[0] = createArrayExpression([
|
if (isLiteralValueNumber(literal.value)) {
|
||||||
createLiteral(literal.value),
|
callExpression.arguments[0] = createArrayExpression([
|
||||||
createLiteral(Math.abs(twoX)),
|
createLiteral(literal.value),
|
||||||
])
|
createLiteral(Math.abs(twoX)),
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import {
|
import {
|
||||||
parse_wasm as ParseWasm,
|
parse_wasm as ParseWasm,
|
||||||
recast_wasm as RecastWasm,
|
recast_wasm as RecastWasm,
|
||||||
|
format_number as FormatNumber,
|
||||||
execute as Execute,
|
execute as Execute,
|
||||||
kcl_lint as KclLint,
|
kcl_lint as KclLint,
|
||||||
modify_ast_for_sketch_wasm as ModifyAstForSketch,
|
modify_ast_for_sketch_wasm as ModifyAstForSketch,
|
||||||
@ -25,6 +26,7 @@ import {
|
|||||||
default_project_settings as DefaultProjectSettings,
|
default_project_settings as DefaultProjectSettings,
|
||||||
base64_decode as Base64Decode,
|
base64_decode as Base64Decode,
|
||||||
clear_scene_and_bust_cache as ClearSceneAndBustCache,
|
clear_scene_and_bust_cache as ClearSceneAndBustCache,
|
||||||
|
change_kcl_settings as ChangeKclSettings,
|
||||||
} from '../wasm-lib/pkg/wasm_lib'
|
} from '../wasm-lib/pkg/wasm_lib'
|
||||||
|
|
||||||
type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib')
|
type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib')
|
||||||
@ -51,6 +53,9 @@ export const parse_wasm: typeof ParseWasm = (...args) => {
|
|||||||
export const recast_wasm: typeof RecastWasm = (...args) => {
|
export const recast_wasm: typeof RecastWasm = (...args) => {
|
||||||
return getModule().recast_wasm(...args)
|
return getModule().recast_wasm(...args)
|
||||||
}
|
}
|
||||||
|
export const format_number: typeof FormatNumber = (...args) => {
|
||||||
|
return getModule().format_number(...args)
|
||||||
|
}
|
||||||
export const execute: typeof Execute = (...args) => {
|
export const execute: typeof Execute = (...args) => {
|
||||||
return getModule().execute(...args)
|
return getModule().execute(...args)
|
||||||
}
|
}
|
||||||
@ -106,3 +111,6 @@ export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = (
|
|||||||
) => {
|
) => {
|
||||||
return getModule().clear_scene_and_bust_cache(...args)
|
return getModule().clear_scene_and_bust_cache(...args)
|
||||||
}
|
}
|
||||||
|
export const change_kcl_settings: typeof ChangeKclSettings = (...args) => {
|
||||||
|
return getModule().change_kcl_settings(...args)
|
||||||
|
}
|
||||||
|
30
src/machines/appMachine.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { ActorRefFrom, createActor, setup } from 'xstate'
|
||||||
|
import { authMachine } from './authMachine'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { ACTOR_IDS } from './machineConstants'
|
||||||
|
|
||||||
|
const appMachine = setup({
|
||||||
|
actors: {
|
||||||
|
[ACTOR_IDS.AUTH]: authMachine,
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5gF8A0IB2B7CdGgAoBbAQwGMALASwzAEp8QAHLWKgFyqw0YA9EAjACZ0AT0FDkU5EA */
|
||||||
|
id: 'modeling-app',
|
||||||
|
invoke: [
|
||||||
|
{
|
||||||
|
src: ACTOR_IDS.AUTH,
|
||||||
|
systemId: ACTOR_IDS.AUTH,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const appActor = createActor(appMachine).start()
|
||||||
|
|
||||||
|
export const authActor = appActor.system.get(ACTOR_IDS.AUTH) as ActorRefFrom<
|
||||||
|
typeof authMachine
|
||||||
|
>
|
||||||
|
export const useAuthState = () => useSelector(authActor, (state) => state)
|
||||||
|
export const useToken = () =>
|
||||||
|
useSelector(authActor, (state) => state.context.token)
|
||||||
|
export const useUser = () =>
|
||||||
|
useSelector(authActor, (state) => state.context.user)
|
@ -15,6 +15,8 @@ import {
|
|||||||
} from 'lib/desktop'
|
} from 'lib/desktop'
|
||||||
import { COOKIE_NAME } from 'lib/constants'
|
import { COOKIE_NAME } from 'lib/constants'
|
||||||
import { markOnce } from 'lib/performance'
|
import { markOnce } from 'lib/performance'
|
||||||
|
import { ACTOR_IDS } from './machineConstants'
|
||||||
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
|
|
||||||
const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV
|
const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ export type Events =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||||
const persistedToken =
|
export const persistedToken =
|
||||||
VITE_KC_DEV_TOKEN ||
|
VITE_KC_DEV_TOKEN ||
|
||||||
getCookie(COOKIE_NAME) ||
|
getCookie(COOKIE_NAME) ||
|
||||||
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
|
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
|
||||||
@ -69,18 +71,17 @@ export const authMachine = setup({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
|
||||||
goToIndexPage: () => {},
|
|
||||||
goToSignInPage: () => {},
|
|
||||||
},
|
|
||||||
actors: {
|
actors: {
|
||||||
getUser: fromPromise(({ input }: { input: { token?: string } }) =>
|
getUser: fromPromise(({ input }: { input: { token?: string } }) =>
|
||||||
getUser(input)
|
getUser(input)
|
||||||
),
|
),
|
||||||
|
logout: fromPromise(async () =>
|
||||||
|
isDesktop() ? writeTokenFile('') : logout()
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwAWQ9gBspuQCYAnAGYAHPYCsx+4ccAaEAE9E1q7YcoZyxrYR1m7mcrYAvnE+aFh4BMTk1LSQjExgAE55VHnYKmIAhuhkRQC2qcLikpDSDPJKSCBqGlo67QYI9gDs5tge5o6h5vau7oY+-v3mA9jWco4u5iu21ua2YcYJSRg4Eln0zJkABFQYrbqdmtoMun2GA7YjxuPmLqvGNh5zRCfJaOcyLUzuAYuFyGcwHEDJY6NCAAeQwTEuskUd3UDx6oD6Im2wUcAzkMJ2cjBxlMgIWLmwZLWljecjJTjh8IYVAgcF0iJxXUez0QIgGxhJZIpu2ptL8AWwtje1nCW2iq1shns8MRdXSlGRjEFeKevUQjkcy3sqwGHimbg83nlCF22GMytVUWMMUc8USCKO2BOdCN7Xu3VNBKMKsVFp2hm2vu+1id83slkVrgTxhcW0pNJ1geDkDR6GNEZFCAT1kZZLk9cMLltb0WdPMjewjjC1mzOZCtk5CSAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOhzEwGsBJAMwBkB7KGCa-AYgkcJIIDdGlMGWwVKAWgA2zVhIIBtAAwBdRKAAOjWLgAuuHupAAPRAGYArAEYSADgu2AnGYBMLpVYBsZz7YA0IACeiG6OJM62tmZKLgDsno5KtvEAvikBaFh4hKTkVHRMLJDsHGAATmWMZSQaUui6tFWoouLSspDy+MpqSCBaOvqGvaYIljb2Tq7uXj7+QYgALFYW4clWy1ZmVgsWsZtpGRg4BMQkMkVsnIUABIwArrrdRv16BvhGI74LJBYW7o5WKJmKILObBUZeEgJP4LTxKMwIhZmBYLA4gTLHHJnWQEKAAeQeXB4IgEQhEGOyp3OUFxBN0CFJmHqb26T16L0G72GiCsSg8PyszkBCViTiUjgC4Jcnhc4SUsQcvgsoL2VjRFJOpGptMJ5Uq1Vq9UaZWaGqx2vw+IeDPwgiZnNZqme2leQ1An1s31+-0BCJBYJCLm+lk8CRl9hRyos6qOlK17QgdI4N0UTvZLs5Hx58NsJARuys0tDSl+AYQthsgNi0TMqt2LjVaPwjAgcCMZuIzoGbyzCAknkliH7Maympa+QYCfYXddXPdixcg4QvKUdk2u2iLkcsXhCRHmKpU7nfQzPe5CAsMpIXi8MvFKM8VliS5c1jzj53W3isNFqPS6NjMcLStXQZ0zc8ohsJI-kcFxXEcR9HAWF9gTzDxbCUXxAQWEsdn3ONsQuOkwLPedl22MIzFg3YP1gl9PG+bYvGsSxlUcRJozSFIgA */
|
||||||
id: 'Auth',
|
id: ACTOR_IDS.AUTH,
|
||||||
initial: 'checkIfLoggedIn',
|
initial: 'checkIfLoggedIn',
|
||||||
context: {
|
context: {
|
||||||
token: persistedToken,
|
token: persistedToken,
|
||||||
@ -112,19 +113,30 @@ export const authMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
loggedIn: {
|
loggedIn: {
|
||||||
entry: ['goToIndexPage'],
|
|
||||||
on: {
|
on: {
|
||||||
'Log out': {
|
'Log out': {
|
||||||
target: 'loggedOut',
|
target: 'loggingOut',
|
||||||
actions: () => {
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
},
|
||||||
if (isDesktop()) writeTokenFile('')
|
},
|
||||||
},
|
loggingOut: {
|
||||||
|
invoke: {
|
||||||
|
src: 'logout',
|
||||||
|
onDone: 'loggedOut',
|
||||||
|
onError: {
|
||||||
|
target: 'loggedIn',
|
||||||
|
actions: [
|
||||||
|
({ event }) => {
|
||||||
|
console.error(
|
||||||
|
'Error while logging out',
|
||||||
|
'error' in event ? `: ${event.error}` : ''
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
loggedOut: {
|
loggedOut: {
|
||||||
entry: ['goToSignInPage'],
|
|
||||||
on: {
|
on: {
|
||||||
'Log in': {
|
'Log in': {
|
||||||
target: 'checkIfLoggedIn',
|
target: 'checkIfLoggedIn',
|
||||||
@ -235,3 +247,12 @@ async function getAndSyncStoredToken(input: {
|
|||||||
localStorage.setItem(TOKEN_PERSIST_KEY, fileToken)
|
localStorage.setItem(TOKEN_PERSIST_KEY, fileToken)
|
||||||
return fileToken
|
return fileToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||||
|
if (isDesktop()) return Promise.resolve(null)
|
||||||
|
return fetch(withBaseUrl('/logout'), {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ 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'
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { authCommands } from 'lib/commandBarConfigs/authCommandConfig'
|
||||||
|
|
||||||
export type CommandBarContext = {
|
export type CommandBarContext = {
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
@ -80,6 +81,7 @@ export type CommandBarMachineEvent =
|
|||||||
export const commandBarMachine = setup({
|
export const commandBarMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
context: {} as CommandBarContext,
|
context: {} as CommandBarContext,
|
||||||
|
input: {} as { commands: Command[] },
|
||||||
events: {} as CommandBarMachineEvent,
|
events: {} as CommandBarMachineEvent,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -409,8 +411,8 @@ export const commandBarMachine = setup({
|
|||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAIwB2AHTiAHAE45AZjkAmdcoaSArCoA0IAJ6JxDOdJ2SF4gCySHaqZIa2Avm8NpMuAsXJUYKSMLEggHLA8fAJhIgiiOgBssokKTpJqiXK2OsqJaoYm8eJqytKJ9hqq+eJW2h5eGNh4RKRkAGKcLb74tJRgAMbc+ANNviGCEVH8gnEKubK5ympVrrlyhabm0rZ5CtYM4hVq83ININ7NfqTSVDSQZADybGA4E2FTvDOxiGoMDNJMjo9H9lLYGDpKpsEModOJpA5XOIwakwcidOdLj1-LdqLQIGQAIIQAijHx4WDvdhcL4xUBxURqSTw3LzfYKZSSOQZWzQ5S1crMuTAiElRKuTFjFo4u74sgAJTA6FQADcwCMpRBKcxJjTorMxEzkhouftuXCGGpIdCpADmZJxfzJLY5Cp5pLydcSLj7gSqeE9d96Yh+TppKoXfkGKdlHloUyFIDUvMXBzbFl3J4LprWt6AMpgfpDLpQDWesgFovDMlXf2ffU-BBZZKuczWWylRRwvlM6Q2LIMZQ2QdHZQeq65245vqDbgPOuBunCEP88MqPQchwVNLKaHptTSYUuRI6f4npyJcfYm5Ylozobz8ShamRWkGhBmeTSQeJX+JXQ6H88jQnCMiugoELCpypQKJeWa3l6U6er0hazvOajPgGr4NsGH6WhYsHOkycglLCEJ7iclgaIosKSLGGgKFe0o3AA4lg3AABZgCQJb4KQUAAK7oK83CwBQHG4DAIwCSQJAiXxJCCcJODcAu2FBsuH55GGJ7irYHYqHpGzGKYWiWB2nK2Kcv6wWO8E5jibGcdxvH8UJIliQAInAqFDGWtY6h8i7vlkZREbYZg6JuwEmQgfxhnkHbiHYJ7yHYTGIU5XE8TgpZucponSISADuWBRLl+BdGwAncBWAkAEboDwClKSJanTEucS1Bk36DicDCpOYLoKHu-x9tGuhgoozKpBlk5ZS5FX5R50gAGpYJQnAQOxJZkBA-BgNIXQqqgADWh0qhtW3sWAeYlv0hKKe5KntW+YRFGi5RyOKDrZEaUW8pppTguGuwDRFEO-nNjnsdlrlPQVsBrVd228LlZDcSQqDemwlDsQAZtj6DSJdm2o7d91gI9rUvYFL4dYIH0RV9P0Zv9CiA3EwMAmCWgVHYKVwY0yE4oqKqcGAxV1Y1zU1uMdNYQzjbMpYcgQlFxFq66u6xXRALbkyg7-Bz0bQzcYsS1LxIEMttOYfWGldYcCWwQmNiRrU0Jq2GRxJCbHZqC6ahm96FuSwqSqquqtuqQrDudVs4iJhUyZtn9qjQokYKHjk6hSLsZhciH0hh1LACiEDNTHr04ZpJT6bIEXxcKp50YDRT-mGrrJbUgFDp3xfIFxAynbx1PPaJe0HUdOAnedJMozd4+IzXjuIJCLIQqUWjJS4MVFBByS7BzyJq38WTB-ZIs3sPo8VcvHlTzgh3HWdF2L3OD8qZST66upCdxUTLBJOUUHB6GFPpSQXt1CWEGpaOiv4EyD1vmPBGj9MbY2kLjAmRMF5kyXmg7+q8AFJwbjkXQEVsj5FyO3RAiQjjfi5CReYPck7iA8FmHAqAIBwEEAhXMf8la4VEMsWwgJuTTXNGYK0tC4rwkDvsAaSQ-qlAhIPPEkBBFvWEVycoFQhywUHGaHWH04Q7FUNBdc+x2FXwnDiSss5eJyzwFo2ucQIplBWHrcEVhuoFFirCeEuxsj8mWEOFYmZhZ2JvNOXyc4ICuLXggaxCJwG70AiUHQfIzHBKHGYDMJFhTFwWjlPKhDRKJJIRFGQcIFBsiOIHJw8ZIQ7CUGrY+9g9AnmKbDRaZSaaFRKmVNGpYqo1Uqe+FKYZan1PyElbJYjrB6C5CUJQ9DL5ROvN6Ep8MBlI3WvgkZEzGzyAbnkZK-wjan38UzeEzZtBswdADYupdjm4XoTIOwuwHB5HyGkYyRRdBBLosCIE6ZvpnFsVs24KD77lPgEFf+kzxRH32EyFYlpOzQjAZYV2ehTkfI4W4IAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAIwB2AHTiAHAE45AZjkAmdcoaSArCoA0IAJ6JxDOdJ2SF4gCySHaqZIa2Avm8NpMuAsXJUYKSMLEggHLA8fAJhIgiiOgBssokKTpJqiXK2OsqJaoYm8eJqytKJ9hqq+eJW2h5eGNh4RKRkAGKcLb74tJRgAMbc+ANNviGCEVH8gnEKubK5ympVrrlyhabm0rZ5CtYM4hVq83ININ7NfqTSVDSQZADybGA4E2FTvDOxiGoMDNJMjo9H9lLYGDpKpsEModOJpA5XOIwakwcidOdLj1-LdqLQIGQAIIQAijHx4WDvdhcL4xUBxURqSTw3LzfYKZSSOQZWzQ5S1crMuTAiElRKuTFjFo4u74sgAJTA6FQADcwCMpRBKcxJjTorMxEzkhouftuXCGGpIdCpADmZJxfzJLY5Cp5pLydcSLj7gSqeE9d96Yh+TppKoXfkGKdlHloUyFIDUvMXBzbFl3J4LprWt6AMpgfpDLpQDWesgFovDMlXf2ffU-BBZZKuczWWylRRwvlM6Q2LIMZQ2QdHZQeq65245vqDbgPOuBunCEP88MqPQchwVNLKaHptTSYUuRI6f4npyJcfYm5Ylozobz8ShamRWkGhBmeTSQeJX+JXQ6H88jQnCMiugoELCpypQKJeWa3l6U6er0hazvOajPgGr4NsGH6WhYsHOkycglLCEJ7iclgaIosKSLGGgKFe0o3AA4lg3AABZgCQJb4KQUAAK7oK83CwBQHG4DAIwCSQJAiXxJCCcJODcAu2FBsuH55GGJ7irYHYqHpGzGKYWiWB2nK2Kcv6wWO8E5jibGcdxvH8UJIliQAInAqFDGWtY6h8i7vlkZREbYZg6JuwEmQgfxhnkHbiHYJ7yHYTGIU5XE8TgpZucponSISADuWBRLl+BdGwAncBWAkAEboDwClKSJanTEucS1Bk36DicDCpOYLoKHu-x9tGuhgoozKpBlk5ZS5FX5R50gAGpYJQnAQOxJZkBA-BgNIXQqqgADWh0qhtW3sWAeYlv0hKKe5KntW+YRFGi5RyOKDrZEaUW8pppTguGuwDRFEO-nNjnsdlrlPQVsBrVd228LlZDcSQqDemwlDsQAZtj6DSJdm2o7d91gI9rUvYFL4dYIH0RV9P0Zv9CiA3EwMAmCWgVHYKVwY0yE4oqKqcGAxV1Y1zU1uMdNYQzjbMpYcgQlFxFq66u6xXRALbkyg7-Bz0bQzcYsS1LxIEMttOYfWGldYcCWwQmNiRrU0Jq2GRxJCbHZqC6ahm96FuSwqSqquqtuqQrDudVs4iJhUyZtn9qjQokYKHjk6hSLsZhciH0hh1LACiEDNTHr04ZpJT6bIEXxcKp50YDRT-mGrrJbUgFDp3xfIFxAynbx1PPaJe0HUdOAnedJMozd4+IzXjuIJCLIQqUWjJS4MVFBByS7BzyJq38WTB-ZIs3sPo8VcvHlTzgh3HWdF2L3OD8qZST66upCdxUTLBJOUUHB6GFPpSQXt1CWEGpaOiv4EyD1vmPBGj9MbY2kLjAmRMF5kyXmg7+q8AFJwbjkXQEVsj5FyO3RAiQjjfi5CReYPck7iA8FmHAqAIBwEEAhXMf8la4VEMsWwgJuTTXNGYK0tC4rwkDvsAaSQ-qlAhIPPEkBBFvWEVycoFQhywUHGaHWH04Q7FUNBdc+x2FXwnDiSss5eJyzwFo2ucQIplBWHrcEVhuoFFirCeEuxsj8mWEOFYmZhZ2JvNOXyc4ICuLXggaxCJwG70AiUHQfIzHBKHGYDMJFhTFwWjlPKhDRKJJIRFGQcIFBsiOIHJw8ZIQ7CUGrY+9g9AnmKbDRaZSaaFRKmVNGpYqo1Uqe+FKYZan1PyElbJYjrB6C5CUJQ9DL5ROvN6Ep8MBlI3WvgkZEzGzyAbnkZK-wjan38UzeEzZtBswdADYupdjm4XoTIOwuwHB5HyGkYyRRdBBLosCIE6ZvpnFsVs24KD77lPgEFf+kzxRH32EyFYlpOzQjAZYV2ehTkfI4W4IAA */
|
||||||
context: {
|
context: ({ input }) => ({
|
||||||
commands: [],
|
commands: input.commands || [],
|
||||||
selectedCommand: undefined,
|
selectedCommand: undefined,
|
||||||
currentArgument: undefined,
|
currentArgument: undefined,
|
||||||
selectionRanges: {
|
selectionRanges: {
|
||||||
@ -425,7 +427,7 @@ export const commandBarMachine = setup({
|
|||||||
setCurrentMachine: () => {},
|
setCurrentMachine: () => {},
|
||||||
noMachinesReason: () => undefined,
|
noMachinesReason: () => undefined,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
id: 'Command Bar',
|
id: 'Command Bar',
|
||||||
initial: 'Closed',
|
initial: 'Closed',
|
||||||
states: {
|
states: {
|
||||||
@ -631,7 +633,11 @@ function sortCommands(a: Command, b: Command) {
|
|||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const commandBarActor = createActor(commandBarMachine).start()
|
export const commandBarActor = createActor(commandBarMachine, {
|
||||||
|
input: {
|
||||||
|
commands: [...authCommands],
|
||||||
|
},
|
||||||
|
}).start()
|
||||||
|
|
||||||
/** Basic state snapshot selector */
|
/** Basic state snapshot selector */
|
||||||
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
||||||
|
3
src/machines/machineConstants.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const ACTOR_IDS = {
|
||||||
|
AUTH: 'auth',
|
||||||
|
}
|
@ -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,
|
||||||
@ -585,6 +583,13 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
// end guards
|
// end guards
|
||||||
actions: {
|
actions: {
|
||||||
|
toastError: ({ event }) => {
|
||||||
|
if ('output' in event && event.output instanceof Error) {
|
||||||
|
toast.error(event.output.message)
|
||||||
|
} else if ('data' in event && event.data instanceof Error) {
|
||||||
|
toast.error(event.data.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
'assign tool in context': assign({
|
'assign tool in context': assign({
|
||||||
currentTool: ({ event }) =>
|
currentTool: ({ event }) =>
|
||||||
'data' in event && event.data && 'tool' in event.data
|
'data' in event && event.data && 'tool' in event.data
|
||||||
@ -639,56 +644,6 @@ export const modelingMachine = setup({
|
|||||||
sketchDetails: event.output,
|
sketchDetails: event.output,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
'AST extrude': ({ context: { store }, event }) => {
|
|
||||||
if (event.type !== 'Extrude') return
|
|
||||||
;(async () => {
|
|
||||||
if (!event.data) return
|
|
||||||
const { selection, distance } = event.data
|
|
||||||
let ast = kclManager.ast
|
|
||||||
if (
|
|
||||||
'variableName' in distance &&
|
|
||||||
distance.variableName &&
|
|
||||||
distance.insertIndex !== undefined
|
|
||||||
) {
|
|
||||||
const newBody = [...ast.body]
|
|
||||||
newBody.splice(
|
|
||||||
distance.insertIndex,
|
|
||||||
0,
|
|
||||||
distance.variableDeclarationAst
|
|
||||||
)
|
|
||||||
ast.body = newBody
|
|
||||||
}
|
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
selection.graphSelections[0]?.codeRef.range
|
|
||||||
)
|
|
||||||
const extrudeSketchRes = extrudeSketch(
|
|
||||||
ast,
|
|
||||||
pathToNode,
|
|
||||||
false,
|
|
||||||
'variableName' in distance
|
|
||||||
? distance.variableIdentifierAst
|
|
||||||
: distance.valueAst
|
|
||||||
)
|
|
||||||
if (trap(extrudeSketchRes)) return
|
|
||||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
|
|
||||||
|
|
||||||
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
|
||||||
focusPath: [pathToExtrudeArg],
|
|
||||||
zoomToFit: true,
|
|
||||||
zoomOnRangeAndType: {
|
|
||||||
range: selection.graphSelections[0]?.codeRef.range,
|
|
||||||
type: 'path',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
|
||||||
|
|
||||||
if (updatedAst?.selections) {
|
|
||||||
editorManager.selectRange(updatedAst?.selections)
|
|
||||||
}
|
|
||||||
})().catch(reportRejection)
|
|
||||||
},
|
|
||||||
'AST revolve': ({ context: { store }, event }) => {
|
'AST revolve': ({ context: { store }, event }) => {
|
||||||
if (event.type !== 'Revolve') return
|
if (event.type !== 'Revolve') return
|
||||||
;(async () => {
|
;(async () => {
|
||||||
@ -1493,6 +1448,63 @@ export const modelingMachine = setup({
|
|||||||
return {} as SetSelections
|
return {} as SetSelections
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
extrudeAstMod: fromPromise<
|
||||||
|
void,
|
||||||
|
ModelingCommandSchema['Extrude'] | undefined
|
||||||
|
>(async ({ input }) => {
|
||||||
|
if (!input) return Promise.reject('No input provided')
|
||||||
|
const { selection, distance } = input
|
||||||
|
let ast = structuredClone(kclManager.ast)
|
||||||
|
let extrudeName: string | undefined = undefined
|
||||||
|
|
||||||
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
selection.graphSelections[0]?.codeRef.range
|
||||||
|
)
|
||||||
|
// Add an extrude statement to the AST
|
||||||
|
const extrudeSketchRes = extrudeSketch(
|
||||||
|
ast,
|
||||||
|
pathToNode,
|
||||||
|
false,
|
||||||
|
'variableName' in distance
|
||||||
|
? distance.variableIdentifierAst
|
||||||
|
: distance.valueAst
|
||||||
|
)
|
||||||
|
if (err(extrudeSketchRes)) return Promise.reject(extrudeSketchRes)
|
||||||
|
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
|
||||||
|
|
||||||
|
// Insert the distance variable if the user has provided a variable name
|
||||||
|
if (
|
||||||
|
'variableName' in distance &&
|
||||||
|
distance.variableName &&
|
||||||
|
typeof pathToExtrudeArg[1][0] === 'number'
|
||||||
|
) {
|
||||||
|
const insertIndex = Math.min(
|
||||||
|
pathToExtrudeArg[1][0],
|
||||||
|
distance.insertIndex
|
||||||
|
)
|
||||||
|
const newBody = [...modifiedAst.body]
|
||||||
|
newBody.splice(insertIndex, 0, distance.variableDeclarationAst)
|
||||||
|
modifiedAst.body = newBody
|
||||||
|
// Since we inserted a new variable, we need to update the path to the extrude argument
|
||||||
|
pathToExtrudeArg[1][0]++
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
||||||
|
focusPath: [pathToExtrudeArg],
|
||||||
|
zoomToFit: true,
|
||||||
|
zoomOnRangeAndType: {
|
||||||
|
range: selection.graphSelections[0]?.codeRef.range,
|
||||||
|
type: 'path',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||||
|
|
||||||
|
if (updatedAst?.selections) {
|
||||||
|
editorManager.selectRange(updatedAst?.selections)
|
||||||
|
}
|
||||||
|
}),
|
||||||
offsetPlaneAstMod: fromPromise(
|
offsetPlaneAstMod: fromPromise(
|
||||||
async ({
|
async ({
|
||||||
input,
|
input,
|
||||||
@ -1823,9 +1835,8 @@ export const modelingMachine = setup({
|
|||||||
],
|
],
|
||||||
|
|
||||||
Extrude: {
|
Extrude: {
|
||||||
target: 'idle',
|
target: 'Applying extrude',
|
||||||
actions: ['AST extrude'],
|
reenter: true,
|
||||||
reenter: false,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
Revolve: {
|
Revolve: {
|
||||||
@ -2550,7 +2561,7 @@ export const modelingMachine = setup({
|
|||||||
|
|
||||||
'Delete segment': {
|
'Delete segment': {
|
||||||
reenter: false,
|
reenter: false,
|
||||||
actions: ['Delete segment', 'Set sketchDetails'],
|
actions: ['Delete segment', 'Set sketchDetails', 'reset selections'],
|
||||||
},
|
},
|
||||||
'code edit during sketch': '.clean slate',
|
'code edit during sketch': '.clean slate',
|
||||||
},
|
},
|
||||||
@ -2622,6 +2633,22 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Applying extrude': {
|
||||||
|
invoke: {
|
||||||
|
src: 'extrudeAstMod',
|
||||||
|
id: 'extrudeAstMod',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Extrude') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['idle'],
|
||||||
|
onError: {
|
||||||
|
target: 'idle',
|
||||||
|
actions: 'toastError',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
'Applying offset plane': {
|
'Applying offset plane': {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'offsetPlaneAstMod',
|
src: 'offsetPlaneAstMod',
|
||||||
|
@ -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
@ -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.
|
||||||
|
@ -25,6 +25,7 @@ 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 { commandBarActor } from 'machines/commandBarMachine'
|
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.
|
||||||
@ -33,6 +34,18 @@ const Home = () => {
|
|||||||
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 {
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
import { OnboardingButtons, useDismiss, useNextClick } from '.'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useUser } from 'machines/appMachine'
|
||||||
|
|
||||||
export default function UserMenu() {
|
export default function UserMenu() {
|
||||||
const { auth } = useSettingsAuthContext()
|
const user = useUser()
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.PROJECT_MENU)
|
const next = useNextClick(onboardingPaths.PROJECT_MENU)
|
||||||
const [avatarErrored, setAvatarErrored] = useState(false)
|
const [avatarErrored, setAvatarErrored] = useState(false)
|
||||||
|
|
||||||
const user = auth?.context?.user
|
|
||||||
const errorOrNoImage = !user?.image || avatarErrored
|
const errorOrNoImage = !user?.image || avatarErrored
|
||||||
const buttonDescription = errorOrNoImage ? 'the menu button' : 'your avatar'
|
const buttonDescription = errorOrNoImage ? 'the menu button' : 'your avatar'
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
|||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import { authActor } from 'machines/appMachine'
|
||||||
|
|
||||||
const subtleBorder =
|
const subtleBorder =
|
||||||
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
|
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
|
||||||
@ -22,7 +23,6 @@ const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:t
|
|||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
const [userCode, setUserCode] = useState('')
|
const [userCode, setUserCode] = useState('')
|
||||||
const {
|
const {
|
||||||
auth: { send },
|
|
||||||
settings: {
|
settings: {
|
||||||
state: {
|
state: {
|
||||||
context: {
|
context: {
|
||||||
@ -70,7 +70,7 @@ const SignIn = () => {
|
|||||||
toast.error('Error while trying to log in')
|
toast.error('Error while trying to log in')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
send({ type: 'Log in', token })
|
authActor.send({ type: 'Log in', token })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
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.33"
|
||||||
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.33"
|
||||||
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",
|
||||||
] }
|
] }
|
||||||
|
@ -13,9 +13,7 @@ use tower_lsp::lsp_types::{
|
|||||||
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
|
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::execution::Sketch;
|
use crate::{execution::Sketch, std::Primitive};
|
||||||
|
|
||||||
use crate::std::Primitive;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
|
@ -197,10 +197,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
source_range: SourceRange,
|
source_range: SourceRange,
|
||||||
cmd: &ModelingCmd,
|
cmd: &ModelingCmd,
|
||||||
) -> Result<(), crate::errors::KclError> {
|
) -> Result<(), crate::errors::KclError> {
|
||||||
let execution_kind = self.execution_kind();
|
|
||||||
if execution_kind.is_isolated() {
|
|
||||||
return Err(KclError::Semantic(KclErrorDetails { message: "Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file.".to_owned(), source_ranges: vec![source_range] }));
|
|
||||||
}
|
|
||||||
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
|
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
|
||||||
cmd: cmd.clone(),
|
cmd: cmd.clone(),
|
||||||
cmd_id: id.into(),
|
cmd_id: id.into(),
|
||||||
|
@ -7,9 +7,9 @@ use crate::{
|
|||||||
KclError, SourceRange,
|
KclError, SourceRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) const SETTINGS: &str = "settings";
|
pub(crate) const SETTINGS: &str = "settings";
|
||||||
pub(super) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
|
pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
|
||||||
pub(super) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
|
pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub(super) enum AnnotationScope {
|
pub(super) enum AnnotationScope {
|
||||||
|