Merge remote-tracking branch 'origin/main' into kurt-scale-sketch
@ -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 |
|
||||
|
||||
|
||||
@ -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 |
|
||||
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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...`)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
]),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
})
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)`
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 } => {
|
||||
|
||||
@ -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],
|
||||
))),
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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+] {}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -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": []
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
---
|
||||
source: kcl-lib/src/simulation_tests.rs
|
||||
description: Artifact graph flowchart non_english_identifiers.kcl
|
||||
extension: md
|
||||
snapshot_kind: binary
|
||||
---
|
||||
@ -0,0 +1,3 @@
|
||||
```mermaid
|
||||
flowchart LR
|
||||
```
|
||||
284
rust/kcl-lib/tests/non_english_identifiers/ast.snap
Normal 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
|
||||
}
|
||||
}
|
||||
5
rust/kcl-lib/tests/non_english_identifiers/input.kcl
Normal 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)
|
||||
229
rust/kcl-lib/tests/non_english_identifiers/ops.snap
Normal 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": []
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
rust/kcl-lib/tests/non_english_identifiers/rendered_model.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
9
rust/kcl-lib/tests/non_english_identifiers/unparsed.snap
Normal 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)
|
||||
13
src/App.tsx
@ -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={[
|
||||
|
||||
@ -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,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
5
src/components/CommandBar/CommandBarDivider.tsx
Normal 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" />
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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'][] }) {
|
||||
|
||||
@ -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',
|
||||
|
||||
3
src/editor/compartments.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Compartment } from '@codemirror/state'
|
||||
|
||||
export const historyCompartment = new Compartment()
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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],
|
||||
})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -149,6 +149,7 @@ function moreNodePathFromSourceRange(
|
||||
return moreNodePathFromSourceRange(init, sourceRange, path)
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
if (_node.type === 'UnaryExpression' && isInRange) {
|
||||
const { argument } = _node
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||