Merge remote-tracking branch 'origin/main' into kurt-scale-sketch

This commit is contained in:
Kurt Hutten Irev-Dev
2025-06-23 21:42:18 +10:00
113 changed files with 1780 additions and 755 deletions

View File

@ -10,9 +10,11 @@ Extend the current sketch with a new involute circular curve.
```kcl
involuteCircular(
@sketch: Sketch,
startRadius: number(Length),
endRadius: number(Length),
angle: number(Angle),
startRadius?: number(Length),
endRadius?: number(Length),
startDiameter?: number(Length),
endDiameter?: number(Length),
reverse?: bool,
tag?: TagDecl,
): Sketch
@ -25,9 +27,11 @@ involuteCircular(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `sketch` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) | Which sketch should this path be added to? | Yes |
| `startRadius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The involute is described between two circles, start_radius is the radius of the inner circle. | Yes |
| `endRadius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The involute is described between two circles, end_radius is the radius of the outer circle. | Yes |
| `angle` | [`number(Angle)`](/docs/kcl-std/types/std-types-number) | The angle to rotate the involute by. A value of zero will produce a curve with a tangent along the x-axis at the start point of the curve. | Yes |
| `startRadius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The involute is described between two circles, startRadius is the radius of the inner circle. Either `startRadius` or `startDiameter` must be given (but not both). | No |
| `endRadius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The involute is described between two circles, endRadius is the radius of the outer circle. Either `endRadius` or `endDiameter` must be given (but not both). | No |
| `startDiameter` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The involute is described between two circles, startDiameter describes the inner circle. Either `startRadius` or `startDiameter` must be given (but not both). | No |
| `endDiameter` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The involute is described between two circles, endDiameter describes the outer circle. Either `endRadius` or `endDiameter` must be given (but not both). | No |
| `reverse` | [`bool`](/docs/kcl-std/types/std-types-bool) | If reverse is true, the segment will start from the end of the involute, otherwise it will start from that start. | No |
| `tag` | [`TagDecl`](/docs/kcl-std/types/std-types-TagDecl) | Create a new tag which refers to this line. | No |

View File

@ -29,7 +29,7 @@ The sketches need to be closed and on different planes that are parallel.
| `vDegree` | [`number(_)`](/docs/kcl-std/types/std-types-number) | Degree of the interpolation. Must be greater than zero. For example, use 2 for quadratic, or 3 for cubic interpolation in the V direction. | No |
| `bezApproximateRational` | [`bool`](/docs/kcl-std/types/std-types-bool) | Attempt to approximate rational curves (such as arcs) using a bezier. This will remove banding around interpolations between arcs and non-arcs. It may produce errors in other scenarios. Over time, this field won't be necessary. | No |
| `baseCurveIndex` | [`number(_)`](/docs/kcl-std/types/std-types-number) | This can be set to override the automatically determined topological base curve, which is usually the first section encountered. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Tolerance for the loft operation. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters. | No |
| `tagStart` | [`TagDecl`](/docs/kcl-std/types/std-types-TagDecl) | A named tag for the face at the start of the loft, i.e. the original sketch. | No |
| `tagEnd` | [`TagDecl`](/docs/kcl-std/types/std-types-TagDecl) | A named tag for the face at the end of the loft. | No |

View File

@ -38,7 +38,7 @@ revolved around the same axis.
| `sketches` | [`[Sketch; 1+]`](/docs/kcl-std/types/std-types-Sketch) | The sketch or set of sketches that should be revolved | Yes |
| `axis` | [`Axis2d`](/docs/kcl-std/types/std-types-Axis2d) or [`Edge`](/docs/kcl-std/types/std-types-Edge) | Axis of revolution. | Yes |
| `angle` | [`number(Angle)`](/docs/kcl-std/types/std-types-number) | Angle to revolve (in degrees). Default is 360. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Tolerance for the revolve operation. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters. | No |
| `symmetric` | [`bool`](/docs/kcl-std/types/std-types-bool) | If true, the extrusion will happen symmetrically around the sketch. Otherwise, the extrusion will happen on only one side of the sketch. | No |
| `bidirectionalAngle` | [`number(Angle)`](/docs/kcl-std/types/std-types-number) | If specified, will also revolve in the opposite direction to 'angle' to the specified angle. If 'symmetric' is true, this value is ignored. | No |
| `tagStart` | [`TagDecl`](/docs/kcl-std/types/std-types-TagDecl) | A named tag for the face at the start of the revolve, i.e. the original sketch. | No |

View File

@ -35,7 +35,7 @@ swept along the same path.
| `sketches` | [`[Sketch; 1+]`](/docs/kcl-std/types/std-types-Sketch) | The sketch or set of sketches that should be swept in space. | Yes |
| `path` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) or [`Helix`](/docs/kcl-std/types/std-types-Helix) | The path to sweep the sketch along. | Yes |
| `sectional` | [`bool`](/docs/kcl-std/types/std-types-bool) | If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Tolerance for this operation. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters. | No |
| `relativeTo` | [`string`](/docs/kcl-std/types/std-types-string) | What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. | No |
| `tagStart` | [`TagDecl`](/docs/kcl-std/types/std-types-TagDecl) | A named tag for the face at the start of the sweep, i.e. the original sketch. | No |
| `tagEnd` | [`TagDecl`](/docs/kcl-std/types/std-types-TagDecl) | A named tag for the face at the end of the sweep. | No |

View File

@ -28,7 +28,7 @@ will smoothly blend the transition.
| `solid` | [`Solid`](/docs/kcl-std/types/std-types-Solid) | The solid whose edges should be filletted | Yes |
| `radius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The radius of the fillet | Yes |
| `tags` | [`[Edge; 1+]`](/docs/kcl-std/types/std-types-Edge) | The paths you want to fillet | Yes |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The tolerance for this fillet | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters. | No |
| `tag` | [`TagDecl`](/docs/kcl-std/types/std-types-TagDecl) | Create a new tag which refers to this fillet | No |
### Returns

View File

@ -24,7 +24,7 @@ verifying fit, and analyzing overlapping geometries in assemblies.
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `solids` | `[Solid; 2+]` | The solids to intersect. | Yes |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The tolerance to use for the intersection operation. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters. | No |
### Returns

View File

@ -27,7 +27,7 @@ and complex multi-body part modeling.
|----------|------|-------------|----------|
| `solids` | [`[Solid; 1+]`](/docs/kcl-std/types/std-types-Solid) | The solids to use as the base to subtract from. | Yes |
| `tools` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) | The solids to subtract. | Yes |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The tolerance to use for the subtraction operation. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters. | No |
### Returns

View File

@ -21,7 +21,7 @@ union(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `solids` | `[Solid; 2+]` | The solids to union. | Yes |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The tolerance to use for the union operation. | No |
| `tolerance` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters. | No |
### Returns

View File

@ -307,7 +307,7 @@ test.describe('Command bar tests', () => {
)
const continueButton = page.getByRole('button', { name: 'Continue' })
const submitButton = page.getByRole('button', { name: 'Submit command' })
const submitButton = page.getByTestId('command-bar-submit')
await continueButton.click()
// Review step and argument hotkeys

View File

@ -54,9 +54,7 @@ test(
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
// Expect it to succeed
const errorToastMessage = page.getByText(`Error while exporting`)
@ -119,9 +117,7 @@ test(
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
// Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`)

View File

@ -229,11 +229,12 @@ test.describe('Feature Tree pane', () => {
const initialCode = `sketch001 = startSketchOn(XZ)
|> circle(center = [0, 0], radius = 5)
renamedExtrude = extrude(sketch001, length = ${initialInput})`
const newConstantName = 'length001'
const expectedCode = `${newConstantName} = 23
const newParameterName = 'length001'
const expectedCode = `${newParameterName} = 23
sketch001 = startSketchOn(XZ)
|> circle(center = [0, 0], radius = 5)
renamedExtrude = extrude(sketch001, length = ${newConstantName})`
renamedExtrude = extrude(sketch001, length = ${newParameterName})`
const editedParameterValue = '23 * 2'
await context.folderSetupFn(async (dir) => {
const testDir = join(dir, 'test-sample')
@ -279,7 +280,7 @@ test.describe('Feature Tree pane', () => {
})
})
await test.step('Add a named constant for distance argument and submit', async () => {
await test.step('Add a parameter for distance argument and submit', async () => {
await expect(cmdBar.currentArgumentInput).toBeVisible()
await cmdBar.variableCheckbox.click()
await cmdBar.progressCmdBar()
@ -296,13 +297,43 @@ test.describe('Feature Tree pane', () => {
highlightedCode: '',
diagnostics: [],
activeLines: [
`renamedExtrude = extrude(sketch001, length = ${newConstantName})`,
`renamedExtrude = extrude(sketch001, length = ${newParameterName})`,
],
})
await editor.expectEditor.toContain(expectedCode, {
shouldNormalise: true,
})
})
await test.step('Edit the parameter via the feature tree', async () => {
const parameter = await toolbar.getFeatureTreeOperation('Parameter', 0)
await parameter.dblclick()
await cmdBar.expectState({
commandName: 'Edit parameter',
currentArgKey: 'value',
currentArgValue: '23',
headerArguments: {
Name: newParameterName,
Value: '23',
},
stage: 'arguments',
highlightedHeaderArg: 'value',
})
await cmdBar.argumentInput
.locator('[contenteditable]')
.fill(editedParameterValue)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
commandName: 'Edit parameter',
headerArguments: {
Name: newParameterName,
Value: '46', // Shows calculated result
},
})
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain(editedParameterValue)
})
})
test(`User can edit an offset plane operation from the feature tree`, async ({
context,

View File

@ -118,15 +118,11 @@ export class CmdBarFixture {
return
}
const arrowButton = this.page.getByRole('button', {
name: 'arrow right Continue',
})
const arrowButton = this.page.getByTestId('command-bar-continue')
if (await arrowButton.isVisible()) {
await arrowButton.click()
await this.continue()
} else {
await this.page
.getByRole('button', { name: 'checkmark Submit command' })
.click()
await this.submit()
}
}

View File

@ -183,14 +183,15 @@ export class EditorFixture {
scrollToText(text: string, placeCursor?: boolean) {
return this.page.evaluate(
(args: { text: string; placeCursor?: boolean }) => {
const editorView = window.editorManager.getEditorView()
// error TS2339: Property 'docView' does not exist on type 'EditorView'.
// Except it does so :shrug:
// @ts-ignore
let index = window.editorManager._editorView?.docView.view.state.doc
const index = editorView?.docView.view.state.doc
.toString()
.indexOf(args.text)
window.editorManager._editorView?.focus()
window.editorManager._editorView?.dispatch({
editorView?.focus()
editorView?.dispatch({
selection: window.EditorSelection.create([
window.EditorSelection.cursor(index),
]),

View File

@ -5,7 +5,7 @@ import type {
FullResult,
} from '@playwright/test/reporter'
class MyAPIReporter implements Reporter {
class APIReporter implements Reporter {
private pendingRequests: Promise<void>[] = []
private allResults: Record<string, any>[] = []
private blockingResults: Record<string, any>[] = []
@ -32,7 +32,7 @@ class MyAPIReporter implements Reporter {
'X-API-Key': process.env.TAB_API_KEY || '',
}),
body: JSON.stringify({
project: 'https://github.com/KittyCAD/modeling-app',
project: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}`,
branch:
process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || '',
@ -60,7 +60,7 @@ class MyAPIReporter implements Reporter {
const payload = {
// Required information
project: 'https://github.com/KittyCAD/modeling-app',
project: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}`,
suite: process.env.CI_SUITE || 'e2e',
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || '',
@ -124,4 +124,4 @@ class MyAPIReporter implements Reporter {
}
}
export default MyAPIReporter
export default APIReporter

View File

@ -1083,14 +1083,13 @@ openSketch = startSketchOn(XY)
cmdBar,
}) => {
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 }
const testPoint = { x: 700, y: 200 }
// TODO: replace the testPoint selection with a feature tree click once that's supported #7544
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane(XZ, offset = 5)`
await homePage.goToModelingScene()
// FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue.
// The engine may not be connected
await page.waitForTimeout(15000)
await scene.settled(cmdBar)
await test.step(`Look for the blue of the XZ plane`, async () => {
//await scene.expectPixelColor([50, 51, 96], testPoint, 15) // FIXME
@ -1829,7 +1828,6 @@ profile002 = startProfile(sketch002, at = [0, 0])
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '',
Path: '',
},
@ -1843,7 +1841,6 @@ profile002 = startProfile(sketch002, at = [0, 0])
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Path: '',
},
@ -1856,7 +1853,6 @@ profile002 = startProfile(sketch002, at = [0, 0])
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Path: '',
},
@ -1869,7 +1865,6 @@ profile002 = startProfile(sketch002, at = [0, 0])
headerArguments: {
Profiles: '1 profile',
Path: '1 segment',
Sectional: '',
},
stage: 'review',
})
@ -1894,6 +1889,9 @@ profile002 = startProfile(sketch002, at = [0, 0])
0
)
await operationButton.dblclick({ button: 'left' })
await page
.getByRole('button', { name: 'sectional', exact: false })
.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'sectional',
@ -1971,7 +1969,6 @@ profile001 = ${circleCode}`
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '',
Path: '',
},
@ -1986,7 +1983,6 @@ profile001 = ${circleCode}`
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Path: '',
},
@ -2000,7 +1996,6 @@ profile001 = ${circleCode}`
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Profiles: '1 profile',
Path: '',
},
@ -2013,7 +2008,6 @@ profile001 = ${circleCode}`
headerArguments: {
Profiles: '1 profile',
Path: '1 helix',
Sectional: '',
},
stage: 'review',
})
@ -4734,7 +4728,6 @@ path001 = startProfile(sketch001, at = [0, 0])
headerArguments: {
Profiles: '',
Path: '',
Sectional: '',
},
highlightedHeaderArg: 'Profiles',
commandName: 'Sweep',
@ -4747,7 +4740,6 @@ path001 = startProfile(sketch001, at = [0, 0])
headerArguments: {
Profiles: '2 profiles',
Path: '',
Sectional: '',
},
highlightedHeaderArg: 'path',
commandName: 'Sweep',
@ -4760,7 +4752,6 @@ path001 = startProfile(sketch001, at = [0, 0])
headerArguments: {
Profiles: '2 profiles',
Path: '1 segment',
Sectional: '',
},
commandName: 'Sweep',
})

View File

@ -475,6 +475,7 @@ test.describe('Can export from electron app', () => {
},
tronApp.projectDirName,
page,
cmdBar,
method
)
)
@ -779,9 +780,6 @@ test.describe(`Project management commands`, () => {
const commandContinueButton = page.getByRole('button', {
name: 'Continue',
})
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully renamed`)
await test.step(`Setup`, async () => {
@ -800,8 +798,7 @@ test.describe(`Project management commands`, () => {
await expect(commandContinueButton).toBeVisible()
await commandContinueButton.click()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await cmdBar.submit()
await expect(toastMessage).toBeVisible()
})
@ -837,9 +834,6 @@ test.describe(`Project management commands`, () => {
})
const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully deleted`)
const noProjectsMessage = page.getByText('No projects found')
@ -859,8 +853,7 @@ test.describe(`Project management commands`, () => {
await projectNameOption.click()
await expect(commandWarning).toBeVisible()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await cmdBar.submit()
await expect(toastMessage).toBeVisible()
})
@ -894,9 +887,6 @@ test.describe(`Project management commands`, () => {
const commandContinueButton = page.getByRole('button', {
name: 'Continue',
})
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully renamed`)
await test.step(`Setup`, async () => {
@ -914,8 +904,7 @@ test.describe(`Project management commands`, () => {
await expect(commandContinueButton).toBeVisible()
await commandContinueButton.click()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await cmdBar.submit()
await expect(toastMessage).toBeVisible()
})
@ -949,9 +938,6 @@ test.describe(`Project management commands`, () => {
})
const projectNameOption = page.getByRole('option', { name: projectName })
const commandWarning = page.getByText('Are you sure you want to delete?')
const commandSubmitButton = page.getByRole('button', {
name: 'Submit command',
})
const toastMessage = page.getByText(`Successfully deleted`)
const noProjectsMessage = page.getByText('No projects found')
@ -967,8 +953,7 @@ test.describe(`Project management commands`, () => {
await projectNameOption.click()
await expect(commandWarning).toBeVisible()
await expect(commandSubmitButton).toBeVisible()
await commandSubmitButton.click()
await cmdBar.submit()
await expect(toastMessage).toBeVisible()
})

View File

@ -1,6 +1,7 @@
import path from 'path'
import { bracket } from '@e2e/playwright/fixtures/bracket'
import type { Page } from '@playwright/test'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { reportRejection } from '@src/lib/trap'
import * as fsp from 'fs/promises'
@ -421,10 +422,7 @@ extrude002 = extrude(profile002, length = 150)
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
// Find the toast.
// Look out for the toast message
@ -461,8 +459,7 @@ extrude002 = extrude(profile002, length = 150)
await page.keyboard.press('Enter')
// Click the checkbox
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
// Find the toast.
// Look out for the toast message
@ -482,6 +479,7 @@ extrude002 = extrude(profile002, length = 150)
test('ensure you CAN export while an export is already going', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
await test.step('Set up the code and durations', async () => {
@ -516,11 +514,11 @@ extrude002 = extrude(profile002, length = 150)
const successToastMessage = page.getByText(`Exported successfully`)
await test.step('second export', async () => {
await clickExportButton(page)
await clickExportButton(page, cmdBar)
await expect(exportingToastMessage).toBeVisible()
await clickExportButton(page)
await clickExportButton(page, cmdBar)
await test.step('The first export still succeeds', async () => {
await Promise.all([
@ -537,7 +535,7 @@ extrude002 = extrude(profile002, length = 150)
await test.step('Successful, unblocked export', async () => {
// Try exporting again.
await clickExportButton(page)
await clickExportButton(page, cmdBar)
// Find the toast.
// Look out for the toast message
@ -880,7 +878,7 @@ s2 = startSketchOn(XY)
})
})
async function clickExportButton(page: Page) {
async function clickExportButton(page: Page, cmdBar: CmdBarFixture) {
await test.step('Running export flow', async () => {
// export the model
const exportButton = page.getByTestId('export-pane-button')
@ -896,9 +894,6 @@ async function clickExportButton(page: Page) {
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await cmdBar.submit()
})
}

View File

@ -1478,6 +1478,7 @@ sketch001 = startSketchOn(XZ)
await page.mouse.move(1200, 139)
await page.mouse.down()
await page.mouse.move(870, 250)
await page.mouse.up()
await page.waitForTimeout(200)
@ -1487,6 +1488,60 @@ sketch001 = startSketchOn(XZ)
)
})
test('Can undo with closed code pane', async ({
page,
homePage,
editor,
toolbar,
scene,
cmdBar,
}) => {
const u = await getUtils(page)
const viewportSize = { width: 1500, height: 750 }
await page.setBodyDimensions(viewportSize)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> startProfile(at = [-10, -10])
|> line(end = [20.0, 10.0])
|> tangentialArc(end = [5.49, 8.37])`
)
})
await homePage.goToModelingScene()
await toolbar.waitForFeatureTreeToBeBuilt()
await scene.settled(cmdBar)
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
await page.waitForTimeout(1000)
await page.mouse.move(1200, 139)
await page.mouse.down()
await page.mouse.move(870, 250)
await page.mouse.up()
await editor.expectEditor.toContain(`tangentialArc(end=[-5.85,4.32])`, {
shouldNormalise: true,
})
await u.closeKclCodePanel()
// Undo the last change
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await u.openKclCodePanel()
await editor.expectEditor.toContain(`tangentialArc(end = [5.49, 8.37])`, {
shouldNormalise: true,
})
})
test('Can delete a single segment line with keyboard', async ({
page,
scene,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -22,6 +22,7 @@ export const token = process.env.token || ''
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'
import type { ElectronZoo } from '@e2e/playwright/fixtures/fixtureSetup'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
import { test } from '@e2e/playwright/zoo-test'
@ -158,10 +159,10 @@ async function openKclCodePanel(page: Page) {
await page.evaluate(() => {
// editorManager is available on the window object.
//@ts-ignore this is in an entirely different context that tsc can't see.
editorManager._editorView.dispatch({
editorManager.getEditorView().dispatch({
selection: {
//@ts-ignore this is in an entirely different context that tsc can't see.
anchor: editorManager._editorView.docView.length,
anchor: editorManager.getEditorView().docView.length,
},
scrollIntoView: true,
})
@ -737,6 +738,7 @@ export const doExport = async (
output: Models['OutputFormat3d_type'],
rootDir: string,
page: Page,
cmdBar: CmdBarFixture,
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => {
if (exportFrom === 'dropdown') {
@ -780,9 +782,7 @@ export const doExport = async (
.click()
await page.locator('#arg-form').waitFor({ state: 'detached' })
}
await expect(page.getByText('Confirm Export')).toBeVisible()
await page.getByRole('button', { name: 'Submit command' }).click()
await cmdBar.submit()
await expect(page.getByText('Exported successfully')).toBeVisible()

View File

@ -10,7 +10,7 @@ import {
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Testing constraints', () => {
test('Can constrain line length', async ({ page, homePage }) => {
test('Can constrain line length', async ({ page, homePage, cmdBar }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -50,11 +50,7 @@ test.describe('Testing constraints', () => {
await page.waitForTimeout(100)
await page.getByTestId('constraint-length').click()
await page.getByTestId('cmd-bar-arg-value').getByRole('textbox').fill('20')
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await cmdBar.continue()
await expect(page.locator('.cm-content')).toHaveText(
`length001 = 20sketch001 = startSketchOn(XY) |> startProfile(at = [-10, -10]) |> line(end = [20, 0]) |> angledLine(angle = 90, length = length001) |> xLine(length = -20)`
@ -681,9 +677,6 @@ test.describe('Testing constraints', () => {
.getByRole('textbox')
const cmdBarKclVariableNameInput =
page.getByPlaceholder('Variable name')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await page.addInitScript(async () => {
localStorage.setItem(
@ -736,7 +729,7 @@ part002 = startSketchOn(XZ)
await page.waitForTimeout(500)
const [ang, len] = value.split(', ')
const changedCode = `|> angledLine(angle = ${ang}, length = ${len})`
await cmdBarSubmitButton.click()
await cmdBar.continue()
await expect(page.locator('.cm-content')).toContainText(changedCode)
// checking active assures the cursor is where it should be
@ -1101,11 +1094,7 @@ part002 = startSketchOn(XZ)
await page.waitForTimeout(500)
await page.getByTestId('cmd-bar-arg-value').getByRole('textbox').fill('10')
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await cmdBar.continue()
await pollEditorLinesSelectedLength(page, 1)
activeLinesContent = await page.locator('.cm-activeLine').all()

View File

@ -21,7 +21,7 @@ test.describe('Testing loading external models', () => {
// We have no more web tests
test.fail(
'Web: should overwrite current code, cannot create new file',
async ({ editor, context, page, homePage }) => {
async ({ editor, context, page, homePage, cmdBar }) => {
const u = await getUtils(page)
await test.step(`Test setup`, async () => {
await context.addInitScript((code) => {
@ -52,9 +52,6 @@ test.describe('Testing loading external models', () => {
name,
})
const warningText = page.getByText('Overwrite current file with sample?')
const confirmButton = page.getByRole('button', {
name: 'Submit command',
})
await test.step(`Precondition: check the initial code`, async () => {
await u.openKclCodePanel()
@ -70,7 +67,7 @@ test.describe('Testing loading external models', () => {
await expect(commandMethodOption('Create new file')).not.toBeVisible()
await commandMethodOption('Overwrite').click()
await expect(warningText).toBeVisible()
await confirmButton.click()
await cmdBar.submit()
await editor.expectEditor.toContain('// ' + newSample.title)
})

View File

@ -3,6 +3,7 @@ import type { LineInputsType } from '@src/lang/std/sketchcombos'
import { uuidv4 } from '@src/lib/utils'
import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { deg, getUtils, wiggleMove } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
@ -18,7 +19,7 @@ test.describe('Testing segment overlays', () => {
* @param {number} options.steps - The number of steps to perform
*/
const _clickConstrained =
(page: Page, editor: EditorFixture) =>
(page: Page, editor: EditorFixture, cmdBar: CmdBarFixture) =>
async ({
hoverPos,
constraintType,
@ -93,11 +94,7 @@ test.describe('Testing segment overlays', () => {
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.waitForTimeout(500)
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await cmdBar.continue()
await editor.expectEditor.toContain(expectFinal, {
shouldNormalise: true,
})
@ -113,7 +110,7 @@ test.describe('Testing segment overlays', () => {
* @param {number} options.steps - The number of steps to perform
*/
const _clickUnconstrained =
(page: Page, editor: EditorFixture) =>
(page: Page, editor: EditorFixture, cmdBar: CmdBarFixture) =>
async ({
hoverPos,
constraintType,
@ -163,11 +160,7 @@ test.describe('Testing segment overlays', () => {
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.waitForTimeout(500)
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await cmdBar.continue()
await editor.expectEditor.toContain(expectAfterUnconstrained, {
shouldNormalise: true,
})
@ -239,8 +232,8 @@ test.describe('Testing segment overlays', () => {
await expect(page.getByTestId('segment-overlay')).toHaveCount(14)
const clickUnconstrained = _clickUnconstrained(page, editor)
const clickConstrained = _clickConstrained(page, editor)
const clickUnconstrained = _clickUnconstrained(page, editor, cmdBar)
const clickConstrained = _clickConstrained(page, editor, cmdBar)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -664,7 +657,7 @@ profile002 = circle(sketch001, center = [345, 0], radius = 238.38)
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify the X constraint was added
await editor.expectEditor.toContain('center = [xAbs001, 0]', {
@ -682,7 +675,7 @@ profile002 = circle(sketch001, center = [345, 0], radius = 238.38)
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify the Y constraint was added
await editor.expectEditor.toContain('center = [xAbs001, yAbs001]', {
@ -700,7 +693,7 @@ profile002 = circle(sketch001, center = [345, 0], radius = 238.38)
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify all constraints were added
await editor.expectEditor.toContain(
@ -887,7 +880,7 @@ profile003 = startProfile(sketch001, at = [64.39, 35.16])
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify the constraint was added
await editor.expectEditor.toContain(
@ -910,7 +903,7 @@ profile003 = startProfile(sketch001, at = [64.39, 35.16])
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify both constraints were added
await editor.expectEditor.toContain(
@ -935,7 +928,7 @@ profile003 = startProfile(sketch001, at = [64.39, 35.16])
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify the constraint was added
await editor.expectEditor.toContain('endAbsolute = [xAbs002, 84.07]', {
@ -955,7 +948,7 @@ profile003 = startProfile(sketch001, at = [64.39, 35.16])
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await cmdBar.continue()
// Verify all constraints were added
await editor.expectEditor.toContain(

View File

@ -32,7 +32,7 @@ test('Units menu', async ({ page, homePage }) => {
test(
'Successful export shows a success toast',
{ tag: '@skipLocalEngine' },
async ({ page, homePage, tronApp }) => {
async ({ page, homePage, cmdBar, tronApp }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
@ -94,7 +94,8 @@ part001 = startSketchOn(-XZ)
presentation: 'pretty',
},
tronApp?.projectDirName,
page
page,
cmdBar
)
}
)
@ -254,6 +255,7 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
test('Basic default modeling and sketch hotkeys work', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
await test.step(`Set up test`, async () => {
@ -397,11 +399,8 @@ test('Basic default modeling and sketch hotkeys work', async ({
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible({
timeout: 20_000,
})
await page.getByRole('button', { name: 'Continue' }).click()
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeVisible()
await page.getByRole('button', { name: 'Submit command' }).click()
await cmdBar.continue()
await cmdBar.submit()
await expect(page.locator('.cm-content')).toContainText('extrude(')
})
@ -575,8 +574,7 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34])
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await cmdBar.progressCmdBar()
await cmdBar.submit()
const result2 = result.genNext`
const sketch002 = extrude(sketch002, length = ${[5, 5]} + 7)`

View File

@ -111,7 +111,8 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
PipeSubstitution { "%" }
identifier { (@asciiLetter | "_") (@asciiLetter | @digit | "_")* }
// Includes non-whitespace unicode characters.
identifier { $[a-zA-Z_\u{a1}-\u{167f}\u{1681}-\u{1fff}\u{200e}-\u{2027}\u{202a}-\u{202e}\u{2030}-\u{205e}\u{2061}-\u{2fff}\u{3001}-\u{fefe}\u{ff00}-\u{10ffff}] $[a-zA-Z0-9_\u{a1}-\u{167f}\u{1681}-\u{1fff}\u{200e}-\u{2027}\u{202a}-\u{202e}\u{2030}-\u{205e}\u{2061}-\u{2fff}\u{3001}-\u{fefe}\u{ff00}-\u{10ffff}]* }
AnnotationName { "@" identifier? }
PropertyName { identifier }
TagDeclarator { "$" identifier }

View File

@ -42,15 +42,15 @@ fn helicalGear(nTeeth, module, pressureAngle, helixAngle, gearHeight) {
helicalGearSketch = startSketchOn(offsetPlane(XY, offset = offsetHeight))
|> startProfile(at = polar(angle = helixCalc, length = baseDiameter / 2))
|> involuteCircular(
startRadius = baseDiameter / 2,
endRadius = tipDiameter / 2,
startDiameter = baseDiameter,
endDiameter = tipDiameter,
angle = helixCalc,
tag = $seg01,
)
|> line(endAbsolute = polar(angle = 160 / nTeeth + helixCalc, length = tipDiameter / 2))
|> involuteCircular(
startRadius = baseDiameter / 2,
endRadius = tipDiameter / 2,
startDiameter = baseDiameter,
endDiameter = tipDiameter,
angle = -(4 * atan(segEndY(seg01) / segEndX(seg01)) - (3 * helixCalc)),
reverse = true,
)

View File

@ -3334,7 +3334,7 @@ mod tests {
use super::*;
use crate::{
parsing::ast::types::{BodyItem, Expr, VariableKind},
KclError, ModuleId,
ModuleId,
};
fn assert_reserved(word: &str) {
@ -4398,14 +4398,10 @@ secondExtrude = startSketchOn(XY)
#[test]
fn test_parse_parens_unicode() {
let result = crate::parsing::top_level_parse("");
let KclError::Lexical { details } = result.0.unwrap_err() else {
panic!();
};
// TODO: Better errors when program cannot tokenize.
let details = result.0.unwrap().1.pop().unwrap();
// TODO: Highlight where the unmatched open parenthesis is.
// https://github.com/KittyCAD/modeling-app/issues/696
assert_eq!(details.message, "found unknown token 'ޜ'");
assert_eq!(details.source_ranges[0].start(), 1);
assert_eq!(details.source_ranges[0].end(), 2);
assert_eq!(details.message, "Unexpected end of file. The compiler expected )");
}
#[test]

View File

@ -6,7 +6,7 @@ use winnow::{
error::{ContextError, ParseError},
prelude::*,
stream::{Location, Stream},
token::{any, none_of, one_of, take_till, take_until},
token::{any, none_of, take_till, take_until, take_while},
LocatingSlice, Stateful,
};
@ -163,8 +163,8 @@ fn whitespace(i: &mut Input<'_>) -> ModalResult<Token> {
}
fn inner_word(i: &mut Input<'_>) -> ModalResult<()> {
one_of(('a'..='z', 'A'..='Z', '_')).parse_next(i)?;
repeat::<_, _, (), _, _>(0.., one_of(('a'..='z', 'A'..='Z', '0'..='9', '_'))).parse_next(i)?;
take_while(1.., |c: char| c.is_alphabetic() || c == '_').parse_next(i)?;
take_while(0.., |c: char| c.is_alphabetic() || c.is_ascii_digit() || c == '_').parse_next(i)?;
Ok(())
}
@ -786,6 +786,7 @@ const things = "things"
};
assert_eq!(actual.tokens[0], expected);
}
#[test]
fn test_word_starting_with_keyword() {
let module_id = ModuleId::default();
@ -799,4 +800,18 @@ const things = "things"
};
assert_eq!(actual.tokens[0], expected);
}
#[test]
fn non_english_identifiers() {
let module_id = ModuleId::default();
let actual = lex("亞當", module_id).unwrap();
let expected = Token {
token_type: TokenType::Word,
value: "亞當".to_owned(),
start: 0,
end: 6,
module_id,
};
assert_eq!(actual.tokens[0], expected);
}
}

View File

@ -3605,3 +3605,24 @@ mod user_reported_union_2_bug {
super::execute(TEST_NAME, false).await
}
}
mod non_english_identifiers {
const TEST_NAME: &str = "non_english_identifiers";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -9,7 +9,7 @@ use kittycad_modeling_cmds::{
websocket::OkWebSocketResponseData,
};
use super::{args::TyF64, DEFAULT_TOLERANCE};
use super::{args::TyF64, DEFAULT_TOLERANCE_MM};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{types::RuntimeType, ExecState, KclValue, ModelingCmdMeta, Solid},
@ -57,7 +57,7 @@ pub(crate) async fn inner_union(
ModelingCmdMeta::from_args_id(&args, solid_out_id),
ModelingCmd::from(mcmd::BooleanUnion {
solid_ids: solids.iter().map(|s| s.id).collect(),
tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
tolerance: LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)),
}),
)
.await?;
@ -122,7 +122,7 @@ pub(crate) async fn inner_intersect(
ModelingCmdMeta::from_args_id(&args, solid_out_id),
ModelingCmd::from(mcmd::BooleanIntersection {
solid_ids: solids.iter().map(|s| s.id).collect(),
tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
tolerance: LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)),
}),
)
.await?;
@ -186,7 +186,7 @@ pub(crate) async fn inner_subtract(
ModelingCmd::from(mcmd::BooleanSubtract {
target_ids: solids.iter().map(|s| s.id).collect(),
tool_ids: tools.iter().map(|s| s.id).collect(),
tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
tolerance: LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)),
}),
)
.await?;

View File

@ -18,7 +18,7 @@ use kittycad_modeling_cmds::{
};
use uuid::Uuid;
use super::{args::TyF64, utils::point_to_mm, DEFAULT_TOLERANCE};
use super::{args::TyF64, utils::point_to_mm, DEFAULT_TOLERANCE_MM};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
@ -79,7 +79,7 @@ async fn inner_extrude(
) -> Result<Vec<Solid>, KclError> {
// Extrude the element(s).
let mut solids = Vec::new();
let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE));
let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
return Err(KclError::new_semantic(KclErrorDetails::new(

View File

@ -6,7 +6,7 @@ use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingC
use kittycad_modeling_cmds as kcmc;
use serde::{Deserialize, Serialize};
use super::{args::TyF64, DEFAULT_TOLERANCE};
use super::{args::TyF64, DEFAULT_TOLERANCE_MM};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
@ -122,7 +122,7 @@ async fn inner_fillet(
strategy: Default::default(),
object_id: solid.id,
radius: LengthUnit(radius.to_mm()),
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)),
cut_type: CutType::Fillet,
}),
)

View File

@ -6,7 +6,7 @@ use anyhow::Result;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use super::{args::TyF64, DEFAULT_TOLERANCE};
use super::{args::TyF64, DEFAULT_TOLERANCE_MM};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
@ -84,7 +84,7 @@ async fn inner_loft(
section_ids: sketches.iter().map(|group| group.id).collect(),
base_curve_index,
bez_approximate_rational,
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)),
v_degree,
}),
)

View File

@ -442,5 +442,5 @@ pub(crate) fn std_ty(path: &str, fn_name: &str) -> (PrimitiveType, StdFnProps) {
}
}
/// The default tolerance for modeling commands in [`kittycad_modeling_cmds::length_unit::LengthUnit`].
const DEFAULT_TOLERANCE: f64 = 0.0000001;
/// The default tolerance for modeling commands in millimeters.
const DEFAULT_TOLERANCE_MM: f64 = 0.0000001;

View File

@ -9,7 +9,7 @@ use kcmc::{
};
use kittycad_modeling_cmds::{self as kcmc, shared::Point3d};
use super::{args::TyF64, DEFAULT_TOLERANCE};
use super::{args::TyF64, DEFAULT_TOLERANCE_MM};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
@ -133,7 +133,7 @@ async fn inner_revolve(
let mut solids = Vec::new();
for sketch in &sketches {
let id = exec_state.next_uuid();
let tolerance = tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE);
let tolerance = tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM);
let direction = match &axis {
Axis2dOrEdgeReference::Axis { direction, origin } => {

View File

@ -415,16 +415,26 @@ pub(crate) fn get_radius(
radius: Option<TyF64>,
diameter: Option<TyF64>,
source_range: SourceRange,
) -> Result<TyF64, KclError> {
get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
}
pub(crate) fn get_radius_labelled(
radius: Option<TyF64>,
diameter: Option<TyF64>,
source_range: SourceRange,
label_radius: &'static str,
label_diameter: &'static str,
) -> Result<TyF64, KclError> {
match (radius, diameter) {
(Some(radius), None) => Ok(radius),
(None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
(None, None) => Err(KclError::new_type(KclErrorDetails::new(
"This function needs either `diameter` or `radius`".to_string(),
format!("This function needs either `{label_diameter}` or `{label_radius}`"),
vec![source_range],
))),
(Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
"You cannot specify both `diameter` and `radius`, please remove one".to_string(),
format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
vec![source_range],
))),
}

View File

@ -11,7 +11,7 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::shapes::get_radius;
use super::shapes::{get_radius, get_radius_labelled};
#[cfg(feature = "artifact-graph")]
use crate::execution::{Artifact, ArtifactId, CodeRef, StartSketchOnFace, StartSketchOnPlane};
use crate::{
@ -101,13 +101,26 @@ pub const NEW_TAG_KW: &str = "tag";
pub async fn involute_circular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
let start_radius: TyF64 = args.get_kw_arg("startRadius", &RuntimeType::length(), exec_state)?;
let end_radius: TyF64 = args.get_kw_arg("endRadius", &RuntimeType::length(), exec_state)?;
let start_radius: Option<TyF64> = args.get_kw_arg_opt("startRadius", &RuntimeType::length(), exec_state)?;
let end_radius: Option<TyF64> = args.get_kw_arg_opt("endRadius", &RuntimeType::length(), exec_state)?;
let start_diameter: Option<TyF64> = args.get_kw_arg_opt("startDiameter", &RuntimeType::length(), exec_state)?;
let end_diameter: Option<TyF64> = args.get_kw_arg_opt("endDiameter", &RuntimeType::length(), exec_state)?;
let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::angle(), exec_state)?;
let reverse = args.get_kw_arg_opt("reverse", &RuntimeType::bool(), exec_state)?;
let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
let new_sketch =
inner_involute_circular(sketch, start_radius, end_radius, angle, reverse, tag, exec_state, args).await?;
let new_sketch = inner_involute_circular(
sketch,
start_radius,
end_radius,
start_diameter,
end_diameter,
angle,
reverse,
tag,
exec_state,
args,
)
.await?;
Ok(KclValue::Sketch {
value: Box::new(new_sketch),
})
@ -123,8 +136,10 @@ fn involute_curve(radius: f64, angle: f64) -> (f64, f64) {
#[allow(clippy::too_many_arguments)]
async fn inner_involute_circular(
sketch: Sketch,
start_radius: TyF64,
end_radius: TyF64,
start_radius: Option<TyF64>,
end_radius: Option<TyF64>,
start_diameter: Option<TyF64>,
end_diameter: Option<TyF64>,
angle: TyF64,
reverse: Option<bool>,
tag: Option<TagNode>,
@ -133,6 +148,22 @@ async fn inner_involute_circular(
) -> Result<Sketch, KclError> {
let id = exec_state.next_uuid();
let longer_args_dot_source_range = args.source_range;
let start_radius = get_radius_labelled(
start_radius,
start_diameter,
args.source_range,
"startRadius",
"startDiameter",
)?;
let end_radius = get_radius_labelled(
end_radius,
end_diameter,
longer_args_dot_source_range,
"endRadius",
"endDiameter",
)?;
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(&args, id),

View File

@ -6,7 +6,7 @@ use kittycad_modeling_cmds::{self as kcmc, shared::RelativeTo};
use schemars::JsonSchema;
use serde::Serialize;
use super::{args::TyF64, DEFAULT_TOLERANCE};
use super::{args::TyF64, DEFAULT_TOLERANCE_MM};
use crate::{
errors::KclError,
execution::{
@ -93,7 +93,7 @@ async fn inner_sweep(
target: sketch.id.into(),
trajectory,
sectional: sectional.unwrap_or(false),
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)),
relative_to,
}),
)

View File

@ -622,7 +622,7 @@ export fn revolve(
axis: Axis2d | Edge,
/// Angle to revolve (in degrees). Default is 360.
angle?: number(Angle),
/// Tolerance for the revolve operation.
/// Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters.
tolerance?: number(Length),
/// If true, the extrusion will happen symmetrically around the sketch. Otherwise, the extrusion will happen on only one side of the sketch.
symmetric?: bool,
@ -961,7 +961,7 @@ export fn sweep(
path: Sketch | Helix,
/// If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components.
sectional?: bool,
/// Tolerance for this operation.
/// Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters.
tolerance?: number(Length),
/// What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'.
relativeTo?: string = 'trajectoryCurve',
@ -1047,7 +1047,7 @@ export fn loft(
bezApproximateRational?: bool = false,
/// This can be set to override the automatically determined topological base curve, which is usually the first section encountered.
baseCurveIndex?: number(Count),
/// Tolerance for the loft operation.
/// Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters.
tolerance?: number(Length),
/// A named tag for the face at the start of the loft, i.e. the original sketch.
tagStart?: TagDecl,
@ -1499,12 +1499,20 @@ export fn profileStartY(
export fn involuteCircular(
/// Which sketch should this path be added to?
@sketch: Sketch,
/// The involute is described between two circles, start_radius is the radius of the inner circle.
startRadius: number(Length),
/// The involute is described between two circles, end_radius is the radius of the outer circle.
endRadius: number(Length),
/// The angle to rotate the involute by. A value of zero will produce a curve with a tangent along the x-axis at the start point of the curve.
angle: number(Angle),
/// The involute is described between two circles, startRadius is the radius of the inner circle.
/// Either `startRadius` or `startDiameter` must be given (but not both).
startRadius?: number(Length),
/// The involute is described between two circles, endRadius is the radius of the outer circle.
/// Either `endRadius` or `endDiameter` must be given (but not both).
endRadius?: number(Length),
/// The involute is described between two circles, startDiameter describes the inner circle.
/// Either `startRadius` or `startDiameter` must be given (but not both).
startDiameter?: number(Length),
/// The involute is described between two circles, endDiameter describes the outer circle.
/// Either `endRadius` or `endDiameter` must be given (but not both).
endDiameter?: number(Length),
/// If reverse is true, the segment will start from the end of the involute, otherwise it will start from that start.
reverse?: bool = false,
/// Create a new tag which refers to this line.

View File

@ -70,7 +70,7 @@ export fn fillet(
radius: number(Length),
/// The paths you want to fillet
tags: [Edge; 1+],
/// The tolerance for this fillet
/// Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters.
tolerance?: number(Length),
/// Create a new tag which refers to this fillet
tag?: TagDecl,
@ -799,7 +799,7 @@ export fn patternCircular3d(
export fn union(
/// The solids to union.
@solids: [Solid; 2+],
/// The tolerance to use for the union operation.
/// Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters.
tolerance?: number(Length),
): [Solid; 1+] {}
@ -857,7 +857,7 @@ export fn union(
export fn intersect(
/// The solids to intersect.
@solids: [Solid; 2+],
/// The tolerance to use for the intersection operation.
/// Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters.
tolerance?: number(Length),
): [Solid; 1+] {}
@ -917,7 +917,7 @@ export fn subtract(
@solids: [Solid; 1+],
/// The solids to subtract.
tools: [Solid],
/// The tolerance to use for the subtraction operation.
/// Defines the smallest distance below which two entities are considered coincident, intersecting, coplanar, or similar. For most use cases, it should not be changed from its default value of 10^-7 millimeters.
tolerance?: number(Length),
): [Solid; 1+] {}

View File

@ -20,37 +20,37 @@ flowchart LR
subgraph path11 [Path]
11["Path<br>[1779, 1849, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 1 }]
12["Segment<br>[1859, 2025, 0]"]
12["Segment<br>[1859, 2021, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 2 }]
13["Segment<br>[2035, 2120, 0]"]
13["Segment<br>[2031, 2116, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 3 }]
14["Segment<br>[2130, 2351, 0]"]
14["Segment<br>[2126, 2343, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 4 }]
15["Segment<br>[2438, 2524, 0]"]
15["Segment<br>[2430, 2516, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 5 }]
16["Segment<br>[2813, 2820, 0]"]
16["Segment<br>[2805, 2812, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 7 }]
17[Solid2d]
end
subgraph path19 [Path]
19["Path<br>[1779, 1849, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 1 }]
20["Segment<br>[1859, 2025, 0]"]
20["Segment<br>[1859, 2021, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 2 }]
21["Segment<br>[2035, 2120, 0]"]
21["Segment<br>[2031, 2116, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 3 }]
22["Segment<br>[2130, 2351, 0]"]
22["Segment<br>[2126, 2343, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 4 }]
23["Segment<br>[2438, 2524, 0]"]
23["Segment<br>[2430, 2516, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 5 }]
24["Segment<br>[2813, 2820, 0]"]
24["Segment<br>[2805, 2812, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 7 }]
25[Solid2d]
end
subgraph path27 [Path]
27["Path<br>[1779, 1849, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 1 }]
32["Segment<br>[2813, 2820, 0]"]
32["Segment<br>[2805, 2812, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 11 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 1 }, VariableDeclarationDeclaration, VariableDeclarationInit, PipeBodyItem { index: 7 }]
33[Solid2d]
end
@ -66,7 +66,7 @@ flowchart LR
29["SweepEdge Opposite"]
30["SweepEdge Opposite"]
31["SweepEdge Opposite"]
34["Sweep Loft<br>[3337, 3404, 0]"]
34["Sweep Loft<br>[3329, 3396, 0]"]
%% [ProgramBodyItem { index: 0 }, VariableDeclarationDeclaration, VariableDeclarationInit, FunctionExpressionBody, FunctionExpressionBodyItem { index: 15 }, VariableDeclarationDeclaration, VariableDeclarationInit]
35[Wall]
%% face_code_ref=Missing NodePath

View File

@ -1735,45 +1735,25 @@ description: Result of parsing helical-gear.kcl
"label": {
"commentStart": 0,
"end": 0,
"name": "startRadius",
"name": "startDiameter",
"start": 0,
"type": "Identifier"
},
"arg": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"left": {
"abs_path": false,
"name": {
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "baseDiameter",
"start": 0,
"type": "Identifier"
},
"path": [],
"name": "baseDiameter",
"start": 0,
"type": "Name",
"type": "Name"
},
"operator": "/",
"right": {
"commentStart": 0,
"end": 0,
"raw": "2",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"suffix": "None"
}
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "BinaryExpression",
"type": "BinaryExpression"
"type": "Name",
"type": "Name"
}
},
{
@ -1781,45 +1761,25 @@ description: Result of parsing helical-gear.kcl
"label": {
"commentStart": 0,
"end": 0,
"name": "endRadius",
"name": "endDiameter",
"start": 0,
"type": "Identifier"
},
"arg": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"left": {
"abs_path": false,
"name": {
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "tipDiameter",
"start": 0,
"type": "Identifier"
},
"path": [],
"name": "tipDiameter",
"start": 0,
"type": "Name",
"type": "Name"
},
"operator": "/",
"right": {
"commentStart": 0,
"end": 0,
"raw": "2",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"suffix": "None"
}
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "BinaryExpression",
"type": "BinaryExpression"
"type": "Name",
"type": "Name"
}
},
{
@ -2072,45 +2032,25 @@ description: Result of parsing helical-gear.kcl
"label": {
"commentStart": 0,
"end": 0,
"name": "startRadius",
"name": "startDiameter",
"start": 0,
"type": "Identifier"
},
"arg": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"left": {
"abs_path": false,
"name": {
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "baseDiameter",
"start": 0,
"type": "Identifier"
},
"path": [],
"name": "baseDiameter",
"start": 0,
"type": "Name",
"type": "Name"
},
"operator": "/",
"right": {
"commentStart": 0,
"end": 0,
"raw": "2",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"suffix": "None"
}
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "BinaryExpression",
"type": "BinaryExpression"
"type": "Name",
"type": "Name"
}
},
{
@ -2118,45 +2058,25 @@ description: Result of parsing helical-gear.kcl
"label": {
"commentStart": 0,
"end": 0,
"name": "endRadius",
"name": "endDiameter",
"start": 0,
"type": "Identifier"
},
"arg": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"left": {
"abs_path": false,
"name": {
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "tipDiameter",
"start": 0,
"type": "Identifier"
},
"path": [],
"name": "tipDiameter",
"start": 0,
"type": "Name",
"type": "Name"
},
"operator": "/",
"right": {
"commentStart": 0,
"end": 0,
"raw": "2",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"suffix": "None"
}
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "BinaryExpression",
"type": "BinaryExpression"
"type": "Name",
"type": "Name"
}
},
{

View File

@ -0,0 +1,18 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands non_english_identifiers.kcl
---
{
"rust/kcl-lib/tests/non_english_identifiers/input.kcl": [],
"std::appearance": [],
"std::array": [],
"std::math": [],
"std::prelude": [],
"std::sketch": [],
"std::solid": [],
"std::sweep": [],
"std::transform": [],
"std::turns": [],
"std::types": [],
"std::units": []
}

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart non_english_identifiers.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

View File

@ -0,0 +1,284 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing non_english_identifiers.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "comprimentoTotal",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"raw": "100",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 100.0,
"suffix": "None"
}
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "亞當",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"raw": "100",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 100.0,
"suffix": "None"
}
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "comprimentoRosca",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"left": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "亞當",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
"operator": "*",
"right": {
"commentStart": 0,
"end": 0,
"raw": "0.8",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.8,
"suffix": "None"
}
},
"start": 0,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "comprimentoCabeça",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"left": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "comprimentoTotal",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
"operator": "-",
"right": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "comprimentoRosca",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
},
"start": 0,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"end": 0,
"expression": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "isEqualTo",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "20",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 20.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "assert",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "comprimentoCabeça",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
},
"start": 0,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"commentStart": 0,
"end": 0,
"nonCodeMeta": {
"nonCodeNodes": {
"2": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "inlineComment",
"value": "80% do comprimento total é roscado",
"style": "line"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -0,0 +1,5 @@
comprimentoTotal = 100
亞當 = 100
comprimentoRosca = 亞當 * 0.8 // 80% do comprimento total é roscado
comprimentoCabeça = comprimentoTotal - comprimentoRosca
assert(comprimentoCabeça, isEqualTo = 20)

View File

@ -0,0 +1,229 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed non_english_identifiers.kcl
---
{
"rust/kcl-lib/tests/non_english_identifiers/input.kcl": [
{
"type": "VariableDeclaration",
"name": "comprimentoTotal",
"value": {
"type": "Number",
"value": 100.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"visibility": "default",
"nodePath": {
"steps": [
{
"type": "ProgramBodyItem",
"index": 0
},
{
"type": "VariableDeclarationDeclaration"
},
{
"type": "VariableDeclarationInit"
}
]
},
"sourceRange": []
},
{
"type": "VariableDeclaration",
"name": "亞當",
"value": {
"type": "Number",
"value": 100.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"visibility": "default",
"nodePath": {
"steps": [
{
"type": "ProgramBodyItem",
"index": 1
},
{
"type": "VariableDeclarationDeclaration"
},
{
"type": "VariableDeclarationInit"
}
]
},
"sourceRange": []
},
{
"type": "VariableDeclaration",
"name": "comprimentoRosca",
"value": {
"type": "Number",
"value": 80.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"visibility": "default",
"nodePath": {
"steps": [
{
"type": "ProgramBodyItem",
"index": 2
},
{
"type": "VariableDeclarationDeclaration"
},
{
"type": "VariableDeclarationInit"
}
]
},
"sourceRange": []
},
{
"type": "VariableDeclaration",
"name": "comprimentoCabeça",
"value": {
"type": "Number",
"value": 20.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"visibility": "default",
"nodePath": {
"steps": [
{
"type": "ProgramBodyItem",
"index": 3
},
{
"type": "VariableDeclarationDeclaration"
},
{
"type": "VariableDeclarationInit"
}
]
},
"sourceRange": []
}
],
"std::appearance": [],
"std::array": [],
"std::math": [
{
"type": "VariableDeclaration",
"name": "PI",
"value": {
"type": "Number",
"value": 3.141592653589793,
"ty": {
"type": "Unknown"
}
},
"visibility": "export",
"nodePath": {
"steps": []
},
"sourceRange": []
},
{
"type": "VariableDeclaration",
"name": "E",
"value": {
"type": "Number",
"value": 2.718281828459045,
"ty": {
"type": "Known",
"type": "Count"
}
},
"visibility": "export",
"nodePath": {
"steps": []
},
"sourceRange": []
},
{
"type": "VariableDeclaration",
"name": "TAU",
"value": {
"type": "Number",
"value": 6.283185307179586,
"ty": {
"type": "Known",
"type": "Count"
}
},
"visibility": "export",
"nodePath": {
"steps": []
},
"sourceRange": []
}
],
"std::prelude": [
{
"type": "VariableDeclaration",
"name": "START",
"value": {
"type": "String",
"value": "start"
},
"visibility": "export",
"nodePath": {
"steps": []
},
"sourceRange": []
},
{
"type": "VariableDeclaration",
"name": "END",
"value": {
"type": "String",
"value": "end"
},
"visibility": "export",
"nodePath": {
"steps": []
},
"sourceRange": []
}
],
"std::sketch": [],
"std::solid": [],
"std::sweep": [],
"std::transform": [],
"std::turns": [],
"std::types": [],
"std::units": []
}

View File

@ -0,0 +1,58 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing non_english_identifiers.kcl
---
{
"comprimentoCabeça": {
"type": "Number",
"value": 20.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"comprimentoRosca": {
"type": "Number",
"value": 80.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"comprimentoTotal": {
"type": "Number",
"value": 100.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"亞當": {
"type": "Number",
"value": 100.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,9 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing non_english_identifiers.kcl
---
comprimentoTotal = 100
亞當 = 100
comprimentoRosca = 亞當 * 0.8 // 80% do comprimento total é roscado
comprimentoCabeça = comprimentoTotal - comprimentoRosca
assert(comprimentoCabeça, isEqualTo = 20)

View File

@ -28,6 +28,7 @@ import {
codeManager,
kclManager,
settingsActor,
editorManager,
getSettings,
} from '@src/lib/singletons'
import { maybeWriteToDisk } from '@src/lib/telemetry'
@ -107,6 +108,16 @@ export function App() {
useHotkeys('backspace', (e) => {
e.preventDefault()
})
// Since these already exist in the editor, we don't need to define them
// with the wrapper.
useHotkeys('mod+z', (e) => {
e.preventDefault()
editorManager.undo()
})
useHotkeys('mod+shift+z', (e) => {
e.preventDefault()
editorManager.redo()
})
useHotkeyWrapper(
[isDesktop() ? 'mod + ,' : 'shift + mod + ,'],
() => navigate(filePath + PATHS.SETTINGS),
@ -256,7 +267,7 @@ export function App() {
<StatusBar
globalItems={[
networkHealthStatus,
networkMachineStatus,
...(isDesktop() ? [networkMachineStatus] : []),
...defaultGlobalStatusBarItems({ location, filePath }),
]}
localItems={[

View File

@ -88,14 +88,14 @@ export function Toolbar({
modelingState: state,
modelingSend: send,
sketchPathId,
editorHasFocus: editorManager.editorView?.hasFocus,
editorHasFocus: editorManager.getEditorView()?.hasFocus,
}),
[
state,
send,
commandBarActor.send,
sketchPathId,
editorManager.editorView?.hasFocus,
editorManager.getEditorView()?.hasFocus,
]
)

View File

@ -4078,7 +4078,7 @@ function isGroupStartProfileForCurrentProfile(sketchEntryNodePath: PathToNode) {
}
}
// Returns the 2D tangent direction vector at the end of the segmentGroup if it's an arc.
// Returns the 2D tangent direction vector at the end of the segmentGroup
function findTangentDirection(segmentGroup: Group) {
let tangentDirection: Coords2d | undefined
if (segmentGroup.userData.type === TANGENTIAL_ARC_TO_SEGMENT) {
@ -4107,11 +4107,6 @@ function findTangentDirection(segmentGroup: Group) {
const from = segmentGroup.userData.from as Coords2d
tangentDirection = subVec(to, from)
tangentDirection = normalizeVec(tangentDirection)
} else {
console.warn(
'Unsupported segment type for tangent direction calculation: ',
segmentGroup.userData.type
)
}
return tangentDirection
}

View File

@ -21,7 +21,7 @@ export const CommandBar = () => {
const commandBarState = useCommandBarState()
const { immediateState } = useNetworkContext()
const {
context: { selectedCommand, currentArgument, commands },
context: { selectedCommand, currentArgument, commands, argumentsToSubmit },
} = commandBarState
const isArgumentThatShouldBeHardToDismiss =
currentArgument?.inputType === 'selection' ||
@ -68,16 +68,23 @@ export const CommandBar = () => {
})
function stepBack() {
const entries = Object.entries(selectedCommand?.args || {}).filter(
([argName, arg]) => {
const argValue =
(typeof argumentsToSubmit[argName] === 'function'
? argumentsToSubmit[argName](commandBarState.context)
: argumentsToSubmit[argName]) || ''
const isRequired =
typeof arg.required === 'function'
? arg.required(commandBarState.context)
: arg.required
return !arg.hidden && (argValue || isRequired)
}
)
if (!currentArgument) {
if (commandBarState.matches('Review')) {
const entries = Object.entries(selectedCommand?.args || {}).filter(
([_, argConfig]) =>
!argConfig.hidden &&
(typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context)
: argConfig.required)
)
const currentArgName = entries[entries.length - 1][0]
const currentArg = {
name: currentArgName,
@ -94,9 +101,6 @@ export const CommandBar = () => {
commandBarActor.send({ type: 'Deselect command' })
}
} else {
const entries = Object.entries(selectedCommand?.args || {}).filter(
(a) => !a[1].hidden
)
const index = entries.findIndex(
([key, _]) => key === currentArgument.name
)
@ -183,20 +187,6 @@ export const CommandBar = () => {
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
</Tooltip>
</button>
{!commandBarState.matches('Selecting command') && (
<button onClick={stepBack} className="m-0 p-0 border-none">
<CustomIcon name="arrowLeft" className="w-5 h-5 rounded-sm" />
<Tooltip position="bottom">
Step back{' '}
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">
Shift
</kbd>
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">
Bksp
</kbd>
</Tooltip>
</button>
)}
</div>
</WrapperComponent.Panel>
</Transition.Child>

View File

@ -1,11 +1,12 @@
import CommandArgOptionInput from '@src/components/CommandBar/CommandArgOptionInput'
import CommandBarBasicInput from '@src/components/CommandBar/CommandBarBasicInput'
import CommandBarHeader from '@src/components/CommandBar/CommandBarHeader'
import CommandBarHeaderFooter from '@src/components/CommandBar/CommandBarHeaderFooter'
import CommandBarKclInput from '@src/components/CommandBar/CommandBarKclInput'
import CommandBarPathInput from '@src/components/CommandBar/CommandBarPathInput'
import CommandBarSelectionInput from '@src/components/CommandBar/CommandBarSelectionInput'
import CommandBarSelectionMixedInput from '@src/components/CommandBar/CommandBarSelectionMixedInput'
import CommandBarTextareaInput from '@src/components/CommandBar/CommandBarTextareaInput'
import CommandBarDivider from '@src/components/CommandBar/CommandBarDivider'
import type { CommandArgument } from '@src/lib/commandTypes'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
@ -28,13 +29,14 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
return (
currentArgument && (
<CommandBarHeader>
<CommandBarHeaderFooter stepBack={stepBack}>
<ArgumentInput
arg={currentArgument}
stepBack={stepBack}
onSubmit={onSubmit}
/>
</CommandBarHeader>
<CommandBarDivider />
</CommandBarHeaderFooter>
)
)
}

View File

@ -0,0 +1,5 @@
export default function CommandBarDivider() {
return (
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
)
}

View File

@ -5,6 +5,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { ActionButton } from '@src/components/ActionButton'
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import CommandBarDivider from '@src/components/CommandBar/CommandBarDivider'
import type {
KclCommandValue,
KclExpressionWithVariable,
@ -14,7 +15,10 @@ import { getSelectionTypeDisplayText } from '@src/lib/selections'
import { roundOff } from '@src/lib/utils'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
function CommandBarHeaderFooter({
children,
stepBack,
}: React.PropsWithChildren<object> & { stepBack: () => void }) {
const commandBarState = useCommandBarState()
const {
context: { selectedCommand, currentArgument, argumentsToSubmit },
@ -102,19 +106,23 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
<span className="pr-2" />
)}
</p>
{Object.entries(nonHiddenArgs || {})
.filter(
([_, argConfig]) =>
argConfig.skip === false ||
(typeof argConfig.required === 'function'
? argConfig.required(commandBarState.context)
: argConfig.required)
)
.map(([argName, arg], i) => {
{Object.entries(nonHiddenArgs || {}).flatMap(
([argName, arg], i) => {
const argValue =
(typeof argumentsToSubmit[argName] === 'function'
? argumentsToSubmit[argName](commandBarState.context)
: argumentsToSubmit[argName]) || ''
const isCurrentArg = argName === currentArgument?.name
const isSkipFalse = arg.skip === false
const isRequired =
typeof arg.required === 'function'
? arg.required(commandBarState.context)
: arg.required
// We actually want to show non-hidden optional args that have a value set already
if (!(argValue || isCurrentArg || isSkipFalse || isRequired)) {
return []
}
return (
<button
@ -161,7 +169,9 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
),
4
)
) : arg.inputType === 'text' && !arg.valueSummary ? (
) : arg.inputType === 'text' &&
!arg.valueSummary &&
typeof argValue === 'string' ? (
`${argValue.slice(0, 12)}${argValue.length > 12 ? '...' : ''}`
) : typeof argValue === 'object' ? (
arg.valueSummary ? (
@ -207,8 +217,14 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
)}
</button>
)
})}
}
)}
</div>
</div>
<CommandBarDivider />
{children}
<div className="px-4 pb-2 flex justify-between items-center gap-2">
<StepBackButton stepBack={stepBack} />
{isReviewing ? (
<ReviewingButton
bgClassName={
@ -237,8 +253,6 @@ function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
/>
)}
</div>
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
{children}
</>
)
)
@ -258,16 +272,16 @@ function ReviewingButton({ bgClassName, iconClassName }: ButtonProps) {
ref={buttonRef}
type="submit"
form="review-form"
className="w-fit !p-0 rounded-sm hover:shadow focus:outline-current"
className={`w-fit !p-0 rounded-sm hover:brightness-110 hover:shadow focus:outline-current ${bgClassName}`}
tabIndex={0}
data-testid="command-bar-submit"
iconStart={{
iconEnd={{
icon: 'checkmark',
bgClassName: `p-1 rounded-sm hover:brightness-110 ${bgClassName}`,
bgClassName: `p-1 rounded-sm ${bgClassName}`,
iconClassName: `${iconClassName}`,
}}
>
<span className="sr-only">Submit command</span>
<span className={`pl-2 ${iconClassName}`}>Submit</span>
</ActionButton>
)
}
@ -278,18 +292,48 @@ function GatheringArgsButton({ bgClassName, iconClassName }: ButtonProps) {
Element="button"
type="submit"
form="arg-form"
className="w-fit !p-0 rounded-sm hover:shadow focus:outline-current"
className={`w-fit !p-0 rounded-sm hover:brightness-110 hover:shadow focus:outline-current ${bgClassName}`}
tabIndex={0}
data-testid="command-bar-continue"
iconStart={{
iconEnd={{
icon: 'arrowRight',
bgClassName: `p-1 rounded-sm hover:brightness-110 ${bgClassName}`,
bgClassName: `p-1 rounded-sm ${bgClassName}`,
iconClassName: `${iconClassName}`,
}}
>
<span className="sr-only">Continue</span>
<span className={`pl-2 ${iconClassName}`}>Continue</span>
</ActionButton>
)
}
export default CommandBarHeader
function StepBackButton({
bgClassName,
iconClassName,
stepBack,
}: ButtonProps & { stepBack: () => void }) {
return (
<ActionButton
Element="button"
type="button"
form="arg-form"
className={`w-fit !p-0 rounded-sm hover:brightness-110 hover:shadow focus:outline-current bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80 ${bgClassName}`}
tabIndex={0}
data-testid="command-bar-step-back"
iconStart={{
icon: 'arrowLeft',
bgClassName: `p-1 rounded-sm bg-chalkboard-20/50 dark:bg-chalkboard-80/50 ${bgClassName}`,
iconClassName: `${iconClassName}`,
}}
onClick={stepBack}
>
<span className={`pr-2 ${iconClassName}`}>Step back</span>
<Tooltip position="bottom">
Step back
<kbd className="hotkey ml-4 dark:!bg-chalkboard-80">Shift</kbd>
<kbd className="hotkey ml-2 dark:!bg-chalkboard-80">Bksp</kbd>
</Tooltip>
</ActionButton>
)
}
export default CommandBarHeaderFooter

View File

@ -332,65 +332,67 @@ function CommandBarKclInput({
)}
</span>
</label>
<div className="flex items-baseline gap-4 mx-4">
<input
type="checkbox"
id="variable-checkbox"
data-testid="cmd-bar-variable-checkbox"
checked={createNewVariable}
onChange={(e) => {
setCreateNewVariable(e.target.checked)
}}
className="bg-chalkboard-10 dark:bg-chalkboard-80"
/>
<label
htmlFor="variable-checkbox"
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
>
Create new variable
</label>
{createNewVariable && (
<>
<input
type="text"
id="variable-name"
name="variable-name"
className="flex-1 border-solid border-0 border-b border-chalkboard-50 bg-transparent focus:outline-none"
placeholder="Variable name"
value={newVariableName}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
spellCheck="false"
autoFocus
onChange={(e) => setNewVariableName(e.target.value)}
onKeyDown={(e) => {
if (
e.currentTarget.value === '' &&
e.key === 'Backspace' &&
arg.createVariable !== 'force'
) {
setCreateNewVariable(false)
{arg.createVariable !== 'disallow' && (
<div className="flex items-baseline gap-4 mx-4">
<input
type="checkbox"
id="variable-checkbox"
data-testid="cmd-bar-variable-checkbox"
checked={createNewVariable}
onChange={(e) => {
setCreateNewVariable(e.target.checked)
}}
className="bg-chalkboard-10 dark:bg-chalkboard-80"
/>
<label
htmlFor="variable-checkbox"
className="text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1"
>
Create new variable
</label>
{createNewVariable && (
<>
<input
type="text"
id="variable-name"
name="variable-name"
className="flex-1 border-solid border-0 border-b border-chalkboard-50 bg-transparent focus:outline-none"
placeholder="Variable name"
value={newVariableName}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
spellCheck="false"
autoFocus
onChange={(e) => setNewVariableName(e.target.value)}
onKeyDown={(e) => {
if (
e.currentTarget.value === '' &&
e.key === 'Backspace' &&
arg.createVariable !== 'force'
) {
setCreateNewVariable(false)
}
}}
onKeyUp={(e) => {
if (e.key === 'Enter' && canSubmit) {
handleSubmit()
}
}}
/>
<span
className={
isNewVariableNameUnique
? 'text-succeed-60 dark:text-succeed-40'
: 'text-destroy-60 dark:text-destroy-40'
}
}}
onKeyUp={(e) => {
if (e.key === 'Enter' && canSubmit) {
handleSubmit()
}
}}
/>
<span
className={
isNewVariableNameUnique
? 'text-succeed-60 dark:text-succeed-40'
: 'text-destroy-60 dark:text-destroy-40'
}
>
{isNewVariableNameUnique ? 'Available' : 'Unavailable'}
</span>
</>
)}
</div>
>
{isNewVariableNameUnique ? 'Available' : 'Unavailable'}
</span>
</>
)}
</div>
)}
{isConstrainWithNamedValueCommand && (
<div className="flex items-baseline gap-4 mx-4">
<input

View File

@ -1,7 +1,10 @@
import { useHotkeys } from 'react-hotkeys-hook'
import CommandBarHeader from '@src/components/CommandBar/CommandBarHeader'
import CommandBarHeaderFooter from '@src/components/CommandBar/CommandBarHeaderFooter'
import CommandBarDivider from '@src/components/CommandBar/CommandBarDivider'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
import { useMemo } from 'react'
import { CustomIcon } from '@src/components/CustomIcon'
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
const commandBarState = useCommandBarState()
@ -57,19 +60,71 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
})
}
const availableOptionalArgs = useMemo(() => {
if (!selectedCommand?.args) return undefined
const s = { ...selectedCommand.args }
for (const [name, arg] of Object.entries(s)) {
const value =
(typeof argumentsToSubmit[name] === 'function'
? argumentsToSubmit[name](commandBarState.context)
: argumentsToSubmit[name]) || ''
const isHidden =
typeof arg.hidden === 'function'
? arg.hidden(commandBarState.context)
: arg.hidden
const isRequired =
typeof arg.required === 'function'
? arg.required(commandBarState.context)
: arg.required
if (isHidden || isRequired || value) {
delete s[name]
}
}
return s
}, [selectedCommand, argumentsToSubmit, commandBarState.context])
return (
<CommandBarHeader>
<p className="px-4 pb-2">
{selectedCommand?.reviewMessage ? (
selectedCommand.reviewMessage instanceof Function ? (
selectedCommand.reviewMessage(commandBarState.context)
) : (
selectedCommand.reviewMessage
)
) : (
<>Confirm {selectedCommand?.displayName || selectedCommand?.name}</>
)}
</p>
<CommandBarHeaderFooter stepBack={stepBack}>
{selectedCommand?.reviewMessage && (
<>
<p className="px-4 py-2">
{selectedCommand.reviewMessage instanceof Function
? selectedCommand.reviewMessage(commandBarState.context)
: selectedCommand.reviewMessage}
</p>
<CommandBarDivider />
</>
)}
{Object.entries(availableOptionalArgs || {}).length > 0 && (
<>
<div className="px-4 flex flex-wrap gap-2 items-baseline">
<span className="text-sm mr-4">Optional</span>
{Object.entries(availableOptionalArgs || {}).map(
([argName, arg]) => {
return (
<button
data-testid="cmd-bar-add-optional-arg"
type="button"
onClick={() => {
commandBarActor.send({
type: 'Edit argument',
data: { arg: { ...arg, name: argName } },
})
}}
key={argName}
className="w-fit px-2 py-1 m-0 rounded-sm flex gap-2 items-center border"
>
<span className="capitalize">
{arg.displayName || argName}
</span>
<CustomIcon name="plus" className="w-4 h-4" />
</button>
)
}
)}
</div>
<CommandBarDivider />
</>
)}
<form
id="review-form"
className="absolute opacity-0 inset-0 pointer-events-none"
@ -97,7 +152,7 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
)
})}
</form>
</CommandBarHeader>
</CommandBarHeaderFooter>
)
}

View File

@ -6,6 +6,8 @@ export type DownloadAppToastProps = {
onDismiss: () => void
}
export const desktopAppPitchMessage = `The present web app is limited in features. We don't want you to miss out!`
export function DownloadAppToast({
onAccept,
onDismiss,
@ -20,8 +22,7 @@ export function DownloadAppToast({
<section>
<h2>Zoo Design Studio is primarily a desktop app</h2>
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
The present web app is limited in features. We don't want you to
miss out!
{desktopAppPitchMessage}
</p>
{!navigator?.userAgent.includes('Chrome') && (
<p className="mt-2 text-sm font-semibold text-chalkboard-70 dark:text-chalkboard-30">

View File

@ -610,7 +610,7 @@ export const EngineStream = (props: {
dataTestId="loading-engine"
className="fixed inset-0 h-screen"
>
Connecting and setting up scene
Connecting and setting up scene...
</Loading>
)}
</div>

View File

@ -65,7 +65,7 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
} = props
const editor = useRef<HTMLDivElement>(null)
const { view, state, container } = useCodeMirror({
const { view, container } = useCodeMirror({
container: editor.current,
onCreateEditor,
extensions,
@ -77,8 +77,8 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
useImperativeHandle(
ref,
() => ({ editor: editor.current, view: view, state: state }),
[editor, container, view, state]
() => ({ editor: editor.current, view: view, state: view?.state }),
[editor, container, view]
)
return <div ref={editor}></div>
@ -138,7 +138,7 @@ export function useCodeMirror(props: UseCodeMirror) {
parent: container,
})
setView(viewCurrent)
onCreateEditor && onCreateEditor(viewCurrent)
onCreateEditor?.(viewCurrent)
}
}
return () => {
@ -156,6 +156,7 @@ export function useCodeMirror(props: UseCodeMirror) {
if (view) {
view.destroy()
setView(undefined)
onCreateEditor?.(null)
}
},
[view]
@ -175,7 +176,7 @@ export function useCodeMirror(props: UseCodeMirror) {
}
}, [targetExtensions, view, isFirstRender])
return { view, setView, container, setContainer, state, setState }
return { view, container, setContainer, state }
}
export default CodeEditor

View File

@ -4,7 +4,7 @@ import type { ComponentProps } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import type { Actor, Prop } from 'xstate'
import type { Operation } from '@rust/kcl-lib/bindings/Operation'
import type { Operation, OpKclValue } from '@rust/kcl-lib/bindings/Operation'
import { ContextMenu, ContextMenuItem } from '@src/components/ContextMenu'
import type { CustomIconName } from '@src/components/CustomIcon'
@ -45,7 +45,7 @@ export const FeatureTreePane = () => {
guards: {
codePaneIsOpen: () =>
modelingState.context.store.openPanes.includes('code') &&
editorManager.editorView !== null,
editorManager.getEditorView() !== null,
},
actions: {
openCodePane: () => {
@ -241,6 +241,7 @@ const OperationItemWrapper = ({
name,
variableName,
visibilityToggle,
valueDetail,
menuItems,
errors,
customSuffix,
@ -252,6 +253,7 @@ const OperationItemWrapper = ({
name: string
variableName?: string
visibilityToggle?: VisibilityToggleProps
valueDetail?: { calculated: OpKclValue; display: string }
customSuffix?: JSX.Element
menuItems?: ComponentProps<typeof ContextMenu>['items']
errors?: Diagnostic[]
@ -266,19 +268,24 @@ const OperationItemWrapper = ({
>
<button
{...props}
className={`reset flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : 'border-none cursor-default'} ${className}`}
className={`reset !py-0.5 !px-1 flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : 'border-none cursor-default'} ${className}`}
>
<CustomIcon name={icon} className="w-5 h-5 block" />
<div className="flex items-baseline align-baseline">
<div className="mr-2">
<div className="flex flex-1 items-baseline align-baseline">
<div className="flex-1 inline-flex items-baseline flex-wrap gap-x-2">
{name}
{variableName && (
<span className="ml-2 opacity-50 text-[11px] font-semibold">
<span className="text-chalkboard-70 dark:text-chalkboard-40 text-xs">
{variableName}
</span>
)}
{customSuffix && customSuffix}
</div>
{customSuffix && customSuffix}
{valueDetail && (
<code className="px-1 text-right text-chalkboard-70 dark:text-chalkboard-40 text-xs">
{valueDetail.display}
</code>
)}
</div>
</button>
{errors && errors.length > 0 && (
@ -302,6 +309,19 @@ const OperationItem = (props: {
}) => {
const kclContext = useKclContext()
const name = getOperationLabel(props.item)
const valueDetail = useMemo(
() =>
props.item.type === 'VariableDeclaration'
? {
display: kclContext.code.slice(
props.item.sourceRange[0],
props.item.sourceRange[1]
),
calculated: props.item.value,
}
: undefined,
[props.item, kclContext.code]
)
const variableName = useMemo(() => {
return getOperationVariableName(props.item, kclContext.ast)
@ -334,7 +354,10 @@ const OperationItem = (props: {
* TODO: https://github.com/KittyCAD/modeling-app/issues/4442
*/
function enterEditFlow() {
if (props.item.type === 'StdLibCall') {
if (
props.item.type === 'StdLibCall' ||
props.item.type === 'VariableDeclaration'
) {
props.send({
type: 'enterEditFlow',
data: {
@ -449,15 +472,25 @@ const OperationItem = (props: {
</ContextMenuItem>,
]
: []),
...(props.item.type === 'StdLibCall'
...(props.item.type === 'StdLibCall' ||
props.item.type === 'VariableDeclaration'
? [
<ContextMenuItem
disabled={!stdLibMap[props.item.name]?.prepareToEdit}
disabled={
!(
stdLibMap[props.item.name]?.prepareToEdit ||
props.item.type === 'VariableDeclaration'
)
}
onClick={enterEditFlow}
hotkey="Double click"
>
Edit
</ContextMenuItem>,
]
: []),
...(props.item.type === 'StdLibCall'
? [
<ContextMenuItem
disabled={!stdLibMap[props.item.name]?.supportsAppearance}
onClick={enterAppearanceFlow}
@ -517,6 +550,7 @@ const OperationItem = (props: {
icon={getOperationIcon(props.item)}
name={name}
variableName={variableName}
valueDetail={valueDetail}
menuItems={menuItems}
onClick={selectOperation}
onDoubleClick={enterEditFlow}

View File

@ -6,6 +6,7 @@ import {
import {
defaultKeymap,
history,
historyField,
historyKeymap,
indentWithTab,
} from '@codemirror/commands'
@ -37,13 +38,12 @@ import interact from '@replit/codemirror-interact'
import { TEST } from '@src/env'
import { useSelector } from '@xstate/react'
import { useEffect, useMemo, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useLspContext } from '@src/components/LspProvider'
import CodeEditor from '@src/components/ModelingSidebar/ModelingPanes/CodeEditor'
import { lineHighlightField } from '@src/editor/highlightextension'
import { modelingMachineEvent } from '@src/editor/manager'
import { codeManagerHistoryCompartment } from '@src/lang/codeManager'
import { historyCompartment } from '@src/editor/compartments'
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
import { Themes, getSystemTheme } from '@src/lib/theme'
import { onMouseDragMakeANewNumber, onMouseDragRegex } from '@src/lib/utils'
@ -75,17 +75,6 @@ export const KclEditorPane = () => {
: context.app.theme.current
const { copilotLSP, kclLSP } = useLspContext()
// Since these already exist in the editor, we don't need to define them
// with the wrapper.
useHotkeys('mod+z', (e) => {
e.preventDefault()
editorManager.undo()
})
useHotkeys('mod+shift+z', (e) => {
e.preventDefault()
editorManager.redo()
})
// When this component unmounts, we need to tell the machine that the editor
useEffect(() => {
return () => {
@ -96,12 +85,13 @@ export const KclEditorPane = () => {
}, [])
useEffect(() => {
if (!editorIsMounted || !lastSelectionEvent || !editorManager.editorView) {
const editorView = editorManager.getEditorView()
if (!editorIsMounted || !lastSelectionEvent || !editorView) {
return
}
try {
editorManager.editorView.dispatch({
editorView.dispatch({
selection: lastSelectionEvent.codeMirrorSelection,
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
scrollIntoView: lastSelectionEvent.scrollIntoView,
@ -119,13 +109,21 @@ export const KclEditorPane = () => {
// Instead, hot load hotkeys via code mirror native.
const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
// When opening the editor, use the existing history in editorManager.
// This is needed to ensure users can undo beyond when the editor has been openeed.
// (Another solution would be to reuse the same state instead of creating a new one in CodeEditor.)
const existingHistory = editorManager.editorState.field(historyField)
const initialHistory = existingHistory
? historyField.init(() => existingHistory)
: history()
const editorExtensions = useMemo(() => {
const extensions = [
drawSelection({
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
}),
lineHighlightField,
codeManagerHistoryCompartment.of(history()),
historyCompartment.of(initialHistory),
closeBrackets(),
codeFolding(),
keymap.of([
@ -206,10 +204,9 @@ export const KclEditorPane = () => {
extensions={editorExtensions}
theme={theme}
onCreateEditor={(_editorView) => {
if (_editorView === null) return
editorManager.setEditorView(_editorView)
kclEditorActor.send({ type: 'setKclEditorMounted', data: true })
if (!_editorView) return
// Update diagnostics as they are cleared when the editor is unmounted.
// Without this, errors would not be shown when closing and reopening the editor.

View File

@ -1,65 +1,29 @@
import { Popover } from '@headlessui/react'
import { useContext } from 'react'
import { CustomIcon } from '@src/components/CustomIcon'
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
import Tooltip from '@src/components/Tooltip'
import { isDesktop } from '@src/lib/isDesktop'
import type { components } from '@src/lib/machine-api'
import type { StatusBarItemType } from '@src/components/StatusBar/statusBarTypes'
export const NetworkMachineIndicator = ({
className,
}: {
className?: string
}) => {
export const useNetworkMachineStatus = (): StatusBarItemType => {
const {
noMachinesReason,
machines,
machines: { length: machineCount },
} = useContext(MachineManagerContext)
const reason = noMachinesReason()
return isDesktop() ? (
<Popover className="relative">
<Popover.Button
className={`flex items-center p-0 border-none bg-transparent dark:bg-transparent relative ${className || ''}`}
data-testid="network-machine-toggle"
>
<NetworkMachinesIcon machineCount={machineCount} />
<Tooltip position="top-left" wrapperClassName="ui-open:hidden">
Network machines ({machineCount}) {reason && `: ${reason}`}
</Tooltip>
</Popover.Button>
<Popover.Panel
className="absolute left-0 bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm"
data-testid="network-popover"
>
<NetworkMachinesPopoverContent machines={machines} />
</Popover.Panel>
</Popover>
) : null
}
export const useNetworkMachineStatus = (): StatusBarItemType => {
return {
id: 'network-machines',
component: NetworkMachineIndicator,
'data-testid': `network-machine-toggle`,
label: `${machineCount}`,
hideLabel: machineCount === 0,
toolTip: {
children: `Network machines (${machineCount}) ${reason ? `: ${reason}` : ''}`,
},
element: 'popover',
icon: 'printer3d',
popoverContent: <NetworkMachinesPopoverContent machines={machines} />,
}
}
function NetworkMachinesIcon({ machineCount }: { machineCount: number }) {
return (
<>
<CustomIcon name="printer3d" className="w-5 h-5" />
{machineCount > 0 && (
<p aria-hidden className="flex items-center justify-center text-xs">
{machineCount}
</p>
)}
</>
)
}
function NetworkMachinesPopoverContent({
machines,
}: { machines: components['schemas']['MachineInfoResponse'][] }) {

View File

@ -1,7 +1,7 @@
import type { StatusBarItemType } from '@src/components/StatusBar/statusBarTypes'
import type { Location } from 'react-router-dom'
import { PATHS } from '@src/lib/paths'
import { APP_VERSION } from '@src/routes/utils'
import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
import {
BillingRemaining,
BillingRemainingMode,
@ -11,6 +11,10 @@ import { BillingDialog } from '@src/components/BillingDialog'
import { Popover } from '@headlessui/react'
import Tooltip from '@src/components/Tooltip'
import { HelpMenu } from '@src/components/HelpMenu'
import { isDesktop } from '@src/lib/isDesktop'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
import { APP_DOWNLOAD_PATH } from '@src/lib/constants'
import { desktopAppPitchMessage } from '@src/components/DownloadAppToast'
export const defaultGlobalStatusBarItems = ({
location,
@ -19,15 +23,26 @@ export const defaultGlobalStatusBarItems = ({
location: Location
filePath?: string
}): StatusBarItemType[] => [
{
id: 'version',
element: 'externalLink',
label: `v${APP_VERSION}`,
href: `https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`,
toolTip: {
children: 'View the release notes on GitHub',
},
},
isDesktop()
? {
id: 'version',
element: 'externalLink',
label: `v${APP_VERSION}`,
href: getReleaseUrl(),
toolTip: {
children: 'View the release notes on GitHub',
},
}
: {
id: 'download-desktop-app',
element: 'externalLink',
label: 'Download the app',
href: `${VITE_KC_SITE_BASE_URL}/${APP_DOWNLOAD_PATH}`,
icon: 'download',
toolTip: {
children: desktopAppPitchMessage,
},
},
{
id: 'telemetry',
element: 'link',

View File

@ -0,0 +1,3 @@
import { Compartment } from '@codemirror/state'
export const historyCompartment = new Compartment()

View File

@ -1,10 +1,22 @@
import { redo, undo } from '@codemirror/commands'
import {
defaultKeymap,
history,
historyKeymap,
redo,
undo,
} from '@codemirror/commands'
import { syntaxTree } from '@codemirror/language'
import type { Diagnostic } from '@codemirror/lint'
import { forEachDiagnostic, setDiagnosticsEffect } from '@codemirror/lint'
import { Annotation, EditorSelection, Transaction } from '@codemirror/state'
import {
Annotation,
EditorSelection,
EditorState,
Transaction,
type TransactionSpec,
} from '@codemirror/state'
import type { ViewUpdate } from '@codemirror/view'
import { EditorView } from '@codemirror/view'
import { EditorView, keymap } from '@codemirror/view'
import type { StateFrom } from 'xstate'
import {
@ -22,6 +34,8 @@ import type {
ModelingMachineEvent,
modelingMachine,
} from '@src/machines/modelingMachine'
import { historyCompartment } from '@src/editor/compartments'
import type CodeManager from '@src/lang/codeManager'
declare global {
interface Window {
@ -65,11 +79,28 @@ export default class EditorManager {
private _highlightRange: Array<[number, number]> = [[0, 0]]
public _editorView: EditorView | null = null
private _editorState: EditorState
private _editorView: EditorView | null = null
public kclManager?: KclManager
public codeManager?: CodeManager
constructor(engineCommandManager: EngineCommandManager) {
this.engineCommandManager = engineCommandManager
this._editorState = EditorState.create({
doc: '',
extensions: [
historyCompartment.of(history()),
keymap.of([...defaultKeymap, ...historyKeymap]),
],
})
}
get editorState(): EditorState {
return this._editorView?.state || this._editorState
}
get state() {
return this.editorState
}
setCopilotEnabled(enabled: boolean) {
@ -80,12 +111,25 @@ export default class EditorManager {
return this._copilotEnabled
}
setEditorView(editorView: EditorView) {
// Invoked when editorView is created and each time when it is updated (eg. user is sketching)..
setEditorView(editorView: EditorView | null) {
// Update editorState to the latest editorView state.
// This is needed because if kcl pane is closed, editorView will become null but we still want to use the last state.
this._editorState = editorView?.state || this._editorState
this._editorView = editorView
kclEditorActor.send({ type: 'setKclEditorMounted', data: true })
kclEditorActor.send({
type: 'setKclEditorMounted',
data: Boolean(editorView),
})
this.overrideTreeHighlighterUpdateForPerformanceTracking()
}
getEditorView(): EditorView | null {
return this._editorView
}
overrideTreeHighlighterUpdateForPerformanceTracking() {
// @ts-ignore
this._editorView?.plugins.forEach((e) => {
@ -132,10 +176,6 @@ export default class EditorManager {
return this._isAllTextSelected
}
get editorView(): EditorView | null {
return this._editorView
}
get isShiftDown(): boolean {
return this._isShiftDown
}
@ -287,12 +327,39 @@ export default class EditorManager {
undo() {
if (this._editorView) {
undo(this._editorView)
} else if (this._editorState) {
const undoPerformed = undo(this) // invokes dispatch which updates this._editorState
if (undoPerformed) {
const newState = this._editorState
// Update the code, this is similar to kcl/index.ts / update, updateDoc,
// needed to update the code, so sketch segments can update themselves.
// In the editorView case this happens within the kcl plugin's update method being called during updates.
this.codeManager!.code = newState.doc.toString()
void this.kclManager!.executeCode()
}
}
}
redo() {
if (this._editorView) {
redo(this._editorView)
} else if (this._editorState) {
const redoPerformed = redo(this)
if (redoPerformed) {
const newState = this._editorState
this.codeManager!.code = newState.doc.toString()
void this.kclManager!.executeCode()
}
}
}
// Invoked by codeMirror during undo/redo.
// Call with incorrect "this" so it needs to be an arrow function.
dispatch = (spec: TransactionSpec) => {
if (this._editorView) {
this._editorView.dispatch(spec)
} else if (this._editorState) {
this._editorState = this._editorState.update(spec).state
}
}

View File

@ -389,7 +389,7 @@ export class KclManager extends EventTarget {
if (err(result)) {
const kclError: KCLError = result as KCLError
this.diagnostics = kclErrorsToDiagnostics([kclError])
this.diagnostics = kclErrorsToDiagnostics([kclError], code)
this._astParseFailed = true
await this.checkIfSwitchedFilesShouldClear()
@ -403,8 +403,8 @@ export class KclManager extends EventTarget {
this._kclErrorsCallBack([])
this._logsCallBack([])
this.addDiagnostics(compilationErrorsToDiagnostics(result.errors))
this.addDiagnostics(compilationErrorsToDiagnostics(result.warnings))
this.addDiagnostics(compilationErrorsToDiagnostics(result.errors, code))
this.addDiagnostics(compilationErrorsToDiagnostics(result.warnings, code))
if (result.errors.length > 0) {
this._astParseFailed = true
@ -465,7 +465,12 @@ export class KclManager extends EventTarget {
// Program was not interrupted, setup the scene
// Do not send send scene commands if the program was interrupted, go to clean up
if (!isInterrupted) {
this.addDiagnostics(await lintAst({ ast: ast }))
this.addDiagnostics(
await lintAst({
ast,
sourceCode: this.singletons.codeManager.code,
})
)
await setSelectionFilterToDefault(this.engineCommandManager)
}
@ -486,11 +491,16 @@ export class KclManager extends EventTarget {
this.logs = logs
this.errors = errors
const code = this.singletons.codeManager.code
// Do not add the errors since the program was interrupted and the error is not a real KCL error
this.addDiagnostics(isInterrupted ? [] : kclErrorsToDiagnostics(errors))
this.addDiagnostics(
isInterrupted ? [] : kclErrorsToDiagnostics(errors, code)
)
// Add warnings and non-fatal errors
this.addDiagnostics(
isInterrupted ? [] : compilationErrorsToDiagnostics(execState.errors)
isInterrupted
? []
: compilationErrorsToDiagnostics(execState.errors, code)
)
this.execState = execState
if (!errors.length) {

View File

@ -2,10 +2,11 @@
// NOT updating the code state when we don't need to.
// This prevents re-renders of the codemirror editor, when typing.
import { history } from '@codemirror/commands'
import { Annotation, Compartment, Transaction } from '@codemirror/state'
import type { EditorView, KeyBinding } from '@codemirror/view'
import { Annotation, Transaction } from '@codemirror/state'
import type { KeyBinding } from '@codemirror/view'
import toast from 'react-hot-toast'
import { historyCompartment } from '@src/editor/compartments'
import type { Program } from '@src/lang/wasm'
import { parse, recast } from '@src/lang/wasm'
import { bracket } from '@src/lib/exampleKcl'
@ -17,7 +18,6 @@ const PERSIST_CODE_KEY = 'persistCode'
const codeManagerUpdateAnnotation = Annotation.define<boolean>()
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true)
export const codeManagerHistoryCompartment = new Compartment()
export default class CodeManager {
private _code: string = bracket
@ -103,25 +103,24 @@ export default class CodeManager {
/**
* Update the code in the editor.
* This is invoked when a segment is being dragged on the canvas, among other things.
*/
updateCodeEditor(code: string, clearHistory?: boolean): void {
this.code = code
if (editorManager.editorView) {
if (clearHistory) {
clearCodeMirrorHistory(editorManager.editorView)
}
editorManager.editorView.dispatch({
changes: {
from: 0,
to: editorManager.editorView.state.doc.length,
insert: code,
},
annotations: [
codeManagerUpdateEvent,
Transaction.addToHistory.of(!clearHistory),
],
})
if (clearHistory) {
clearCodeMirrorHistory()
}
editorManager.dispatch({
changes: {
from: 0,
to: editorManager.editorState?.doc.length || 0,
insert: code,
},
annotations: [
codeManagerUpdateEvent,
Transaction.addToHistory.of(!clearHistory),
],
})
}
/**
@ -213,16 +212,16 @@ function safeLSSetItem(key: string, value: string) {
localStorage?.setItem(key, value)
}
function clearCodeMirrorHistory(view: EditorView) {
function clearCodeMirrorHistory() {
// Clear history
view.dispatch({
effects: [codeManagerHistoryCompartment.reconfigure([])],
editorManager.dispatch({
effects: [historyCompartment.reconfigure([])],
annotations: [codeManagerUpdateEvent],
})
// Add history back
view.dispatch({
effects: [codeManagerHistoryCompartment.reconfigure([history()])],
editorManager.dispatch({
effects: [historyCompartment.reconfigure([history()])],
annotations: [codeManagerUpdateEvent],
})
}

View File

@ -1,8 +1,35 @@
import type { KCLError } from '@src/lang/errors'
import { kclErrorsToDiagnostics } from '@src/lang/errors'
import { kclErrorsToDiagnostics, toUtf16, toUtf8 } from '@src/lang/errors'
import { defaultArtifactGraph } from '@src/lang/std/artifactGraph'
import { topLevelRange } from '@src/lang/util'
describe('test UTF conversions', () => {
it('Converts UTF-8 to UTF-16', () => {
// This KCL program has an error. The variable `亞當` cannot be +3 because
// it holds a string. So that variable, on line 2, should be highlighted by
// a source range.
const sourceCode = "亞當 = 'adam'\nx = 亞當 + 3"
// Start with a SourceRange from the KCL interpreter,
// which is a UTF-8 range, on where the variable is used on the second line.
const utf8SourceRange = [20, 26, 0]
// JS string of the program uses UTF-16, so check we can correctly find the
// source range offset in UTF-16.
const actualStart = toUtf16(utf8SourceRange[0], sourceCode)
const actualEnd = toUtf16(utf8SourceRange[1], sourceCode)
const textInSourceRange = sourceCode.slice(actualStart, actualEnd)
expect(actualStart).toBe(16)
expect(actualEnd).toBe(18)
expect(textInSourceRange).toBe('亞當')
// Test we can convert the UTF-16 source range back to UTF-8,
// getting the original source range back.
const utf16Range: [number, number, number] = [actualStart, actualEnd, 0]
const actualUtf8Range = toUtf8(utf16Range, sourceCode)
expect(actualUtf8Range).toStrictEqual(utf8SourceRange)
})
})
describe('test kclErrToDiagnostic', () => {
it('converts KCL errors to CodeMirror diagnostics', () => {
const errors: KCLError[] = [
@ -33,7 +60,7 @@ describe('test kclErrToDiagnostic', () => {
defaultPlanes: null,
},
]
const diagnostics = kclErrorsToDiagnostics(errors)
const diagnostics = kclErrorsToDiagnostics(errors, 'TEST PROGRAM')
expect(diagnostics).toEqual([
{
from: 0,

View File

@ -290,6 +290,38 @@ export class KCLUndefinedValueError extends KCLError {
}
}
/**
Convert this UTF-16 source range offset to UTF-8 as SourceRange is always a UTF-8
*/
export function toUtf8(
utf16SourceRange: SourceRange,
sourceCode: string
): SourceRange {
const moduleId = utf16SourceRange[2]
const textEncoder = new TextEncoder()
const prefixUtf16 = sourceCode.slice(0, utf16SourceRange[0])
const prefixUtf8 = textEncoder.encode(prefixUtf16)
const prefixLen = prefixUtf8.length
const toHighlightUtf16 = sourceCode.slice(
utf16SourceRange[0],
utf16SourceRange[1]
)
const toHighlightUtf8 = textEncoder.encode(toHighlightUtf16)
const toHighlightLen = toHighlightUtf8.length
return [prefixLen, prefixLen + toHighlightLen, moduleId]
}
/**
Convert this UTF-8 source range offset to UTF-16 for display in CodeMirror,
as it relies on JS-style string encoding which is UTF-16.
*/
export function toUtf16(utf8Offset: number, sourceCode: string): number {
const sourceUtf8 = new TextEncoder().encode(sourceCode)
const prefix = sourceUtf8.slice(0, utf8Offset)
const backTo16 = new TextDecoder().decode(prefix)
return backTo16.length
}
/**
* Maps the lsp diagnostic to an array of KclErrors.
* Currently the diagnostics are all errors, but in the future they could include lints.
@ -299,20 +331,23 @@ export function lspDiagnosticsToKclErrors(
diagnostics: LspDiagnostic[]
): KCLError[] {
return diagnostics
.flatMap(
({ range, message }) =>
new KCLError(
'unexpected',
message,
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
[],
[],
[],
defaultArtifactGraph(),
{},
null
)
)
.flatMap(({ range, message }) => {
const sourceRange = toUtf8(
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, 0],
doc.toString()
)
return new KCLError(
'unexpected',
message,
sourceRange,
[],
[],
[],
defaultArtifactGraph(),
{},
null
)
})
.sort((a, b) => {
const c = a.sourceRange[0]
const d = b.sourceRange[0]
@ -331,7 +366,8 @@ export function lspDiagnosticsToKclErrors(
* Currently the diagnostics are all errors, but in the future they could include lints.
* */
export function kclErrorsToDiagnostics(
errors: KCLError[]
errors: KCLError[],
sourceCode: string
): CodeMirrorDiagnostic[] {
let nonFatal: CodeMirrorDiagnostic[] = []
const errs = errors
@ -350,8 +386,8 @@ export function kclErrorsToDiagnostics(
item.sourceRange[1] !== err.sourceRange[1]
) {
diagnostics.push({
from: item.sourceRange[0],
to: item.sourceRange[1],
from: toUtf16(item.sourceRange[0], sourceCode),
to: toUtf16(item.sourceRange[1], sourceCode),
message: 'Part of the error backtrace',
severity: 'hint',
})
@ -365,11 +401,13 @@ export function kclErrorsToDiagnostics(
}
}
if (err.nonFatal.length > 0) {
nonFatal = nonFatal.concat(compilationErrorsToDiagnostics(err.nonFatal))
nonFatal = nonFatal.concat(
compilationErrorsToDiagnostics(err.nonFatal, sourceCode)
)
}
diagnostics.push({
from: err.sourceRange[0],
to: err.sourceRange[1],
from: toUtf16(err.sourceRange[0], sourceCode),
to: toUtf16(err.sourceRange[1], sourceCode),
message,
severity: 'error',
})
@ -379,7 +417,8 @@ export function kclErrorsToDiagnostics(
}
export function compilationErrorsToDiagnostics(
errors: CompilationError[]
errors: CompilationError[],
sourceCode: string
): CodeMirrorDiagnostic[] {
return errors
?.filter((err) => isTopLevelModule(err.sourceRange))
@ -397,8 +436,8 @@ export function compilationErrorsToDiagnostics(
apply: (view: EditorView, from: number, to: number) => {
view.dispatch({
changes: {
from: suggestion.source_range[0],
to: suggestion.source_range[1],
from: toUtf16(suggestion.source_range[0], sourceCode),
to: toUtf16(suggestion.source_range[1], sourceCode),
insert: suggestion.insert,
},
annotations: [lspCodeActionEvent],
@ -408,8 +447,8 @@ export function compilationErrorsToDiagnostics(
]
}
return {
from: err.sourceRange[0],
to: err.sourceRange[1],
from: toUtf16(err.sourceRange[0], sourceCode),
to: toUtf16(err.sourceRange[1], sourceCode),
message: err.message,
severity,
actions,

View File

@ -2,7 +2,7 @@ import type { Diagnostic } from '@codemirror/lint'
import { lspCodeActionEvent } from '@kittycad/codemirror-lsp-client'
import type { Node } from '@rust/kcl-lib/bindings/Node'
import { KCLError } from '@src/lang/errors'
import { KCLError, toUtf16 } from '@src/lang/errors'
import type { ExecState, Program } from '@src/lang/wasm'
import { emptyExecState, kclLint } from '@src/lang/wasm'
import { EXECUTE_AST_INTERRUPT_ERROR_STRING } from '@src/lib/constants'
@ -142,8 +142,10 @@ function handleExecuteError(e: any): ExecutionResult {
export async function lintAst({
ast,
sourceCode,
}: {
ast: Program
sourceCode: string
}): Promise<Array<Diagnostic>> {
try {
const discovered_findings = await kclLint(ast)
@ -157,8 +159,8 @@ export async function lintAst({
apply: (view: EditorView, from: number, to: number) => {
view.dispatch({
changes: {
from: suggestion.source_range[0],
to: suggestion.source_range[1],
from: toUtf16(suggestion.source_range[0], sourceCode),
to: toUtf16(suggestion.source_range[1], sourceCode),
insert: suggestion.insert,
},
annotations: [lspCodeActionEvent],
@ -168,8 +170,8 @@ export async function lintAst({
]
}
return {
from: lint.pos[0],
to: lint.pos[1],
from: toUtf16(lint.pos[0], sourceCode),
to: toUtf16(lint.pos[1], sourceCode),
message: lint.finding.title,
severity: 'info',
actions,

View File

@ -149,6 +149,7 @@ function moreNodePathFromSourceRange(
return moreNodePathFromSourceRange(init, sourceRange, path)
}
}
return path
}
if (_node.type === 'UnaryExpression' && isInRange) {
const { argument } = _node

View File

@ -415,10 +415,10 @@ export const errFromErrWithOutputs = (e: any): KCLError => {
export const kclLint = async (ast: Program): Promise<Array<Discovered>> => {
try {
const discovered_findings: Array<Discovered> = await kcl_lint(
const discoveredFindings: Array<Discovered> = await kcl_lint(
JSON.stringify(ast)
)
return discovered_findings
return discoveredFindings
} catch (e: any) {
return Promise.reject(e)
}

Some files were not shown because too many files have changed in this diff Show More