Merge branch 'main' into pierremtb/issue5301-Expose-the-sectional-argument-in-the-Sweep-command-flow

This commit is contained in:
Pierre Jacquier
2025-03-18 14:25:42 -04:00
committed by GitHub
126 changed files with 22145 additions and 1471 deletions

View File

@ -22,6 +22,13 @@
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-unused-vars": ["error", {
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_",
"ignoreRestSiblings": true,
"vars": "all",
"args": "none"
}],
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",

View File

@ -75,7 +75,7 @@ jobs:
prepare-wasm:
# seperate job on Ubuntu to build or fetch the wasm blob once on the fastest runner
runs-on: namespace-profile-ubuntu-8-cores
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
needs: conditions
steps:
- uses: actions/checkout@v4
@ -163,7 +163,7 @@ jobs:
snapshots:
name: playwright:snapshots:ubuntu
runs-on: namespace-profile-ubuntu-8-cores
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
needs: [conditions, prepare-wasm]
steps:
- uses: actions/create-github-app-token@v1
@ -237,19 +237,14 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
with:
name: playwright-report-snapshots-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-ubuntu-snapshot-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
overwrite: true
- name: Clean up test-results
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
continue-on-error: true
run: rm -r test-results
- name: check for changes
if: ${{ needs.conditions.outputs.should-run == 'true' && matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 && github.ref != 'refs/heads/main' }}
if: ${{ needs.conditions.outputs.should-run == 'true' && github.ref != 'refs/heads/main' }}
shell: bash
id: git-check
run: |
@ -270,31 +265,36 @@ jobs:
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
git commit -m "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
git commit -m "A snapshot a day keeps the bugs away! 📷🐛" || true
git push
git push origin ${{ github.head_ref }}
# only upload artifacts if there's actually changes
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.git-check.outputs.modified == 'true' }}
with:
name: playwright-report-ubuntu-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
electron:
needs: [conditions, prepare-wasm]
timeout-minutes: 60
name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }}
env:
OS_NAME: ${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }}
name: playwright:electron:${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }}:${{ matrix.shardIndex }}:${{ matrix.shardTotal }}
strategy:
fail-fast: false
matrix:
# TODO: enable self-hosted-windows-8-cores once available
os: [namespace-profile-ubuntu-8-cores, namespace-profile-macos-8-cores, windows-16-cores]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
# TODO: enable namespace-profile-windows-latest once available
os:
- "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
- namespace-profile-macos-8-cores
- windows-latest
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
# Disable macos and windows tests on hourly e2e tests since we only care
# about server side changes.
# Technique from https://github.com/joaomcteixeira/python-project-skeleton/pull/31/files
isScheduled:
- ${{ github.event_name == 'schedule' }}
exclude:
- os: namespace-profile-macos-8-cores
isScheduled: true
- os: windows-latest
isScheduled: true
# TODO: add ref here for main and latest release tag
runs-on: ${{ matrix.os }}
steps:
@ -351,7 +351,7 @@ jobs:
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
name: test-results-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run playwright/electron flow (with retries)
@ -360,8 +360,8 @@ jobs:
uses: nick-fields/retry@v3.0.2
with:
shell: bash
command: .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}}
timeout_minutes: 30
command: .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{ env.OS_NAME }}
timeout_minutes: 45
max_attempts: 15
env:
CI: true
@ -374,7 +374,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && always() }}
with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
name: test-results-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
include-hidden-files: true
retention-days: 30
@ -383,7 +383,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && always() }}
with:
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30

File diff suppressed because it is too large Load Diff

View File

@ -126,6 +126,30 @@ A base path.
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----
A base path.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArcThreePoint`| | No |
| `p1` |`[number, number]`| Point 1 of the arc (base on the end of previous segment) | No |
| `p2` |`[number, number]`| Point 2 of the arc (interior kwarg) | No |
| `p3` |`[number, number]`| Point 3 of the arc (end kwarg) | No |
| `from` |`[number, number]`| The from point. | No |
| `to` |`[number, number]`| The to point. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----
A path that is horizontal.

View File

@ -100,7 +100,8 @@ test(
try {
const outputGltf = await fsp.readFile(firstFileFullPath)
return outputGltf.byteLength
} catch (e) {
} catch (error: unknown) {
void error
return 0
}
},
@ -179,7 +180,8 @@ test(
try {
const outputGltf = await fsp.readFile(secondFileFullPath)
return outputGltf.byteLength
} catch (e) {
} catch (error: unknown) {
void error
return 0
}
},

View File

@ -1197,7 +1197,7 @@ test.describe('Undo and redo do not keep history when navigating between files',
`cloned file has an incremented name and same contents`,
{ tag: '@electron' },
async ({ page, context, homePage }, testInfo) => {
const { panesOpen, createNewFile, cloneFile } = await getUtils(page, test)
const { panesOpen, cloneFile } = await getUtils(page, test)
const { dir } = await context.folderSetupFn(async (dir) => {
const finalDir = join(dir, 'testDefault')

View File

@ -3,25 +3,15 @@
import type {
BrowserContext,
ElectronApplication,
Fixtures as PlaywrightFixtures,
TestInfo,
Page,
} from '@playwright/test'
import {
_electron as electron,
PlaywrightTestArgs,
PlaywrightWorkerArgs,
} from '@playwright/test'
import { _electron as electron } from '@playwright/test'
import * as TOML from '@iarna/toml'
import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
TEST_SETTINGS,
TEST_SETTINGS_DEFAULT_THEME,
} from '../storageStates'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import { TEST_SETTINGS } from '../storageStates'
import { SETTINGS_FILE_NAME } from 'lib/constants'
import { getUtils, setup } from '../test-utils'
import fsp from 'fs/promises'
import fs from 'node:fs'
@ -31,7 +21,6 @@ import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
import { SceneFixture } from './sceneFixture'
import { HomePageFixture } from './homePageFixture'
import { unsafeTypedKeys } from 'lib/utils'
import { DeepPartial } from 'lib/types'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -278,13 +267,14 @@ export class ElectronZoo {
if (fs.existsSync(this.projectDirName)) {
await fsp.rm(this.projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
} catch (_e) {
console.error(_e)
}
try {
await fsp.mkdir(this.projectDirName)
} catch (e) {
} catch (error: unknown) {
void error
// Not a problem if it already exists.
}

View File

@ -8,6 +8,7 @@ import {
} from '../test-utils'
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
import { ToolbarModeName } from 'lib/toolbar'
export class ToolbarFixture {
public page: Page
@ -120,6 +121,15 @@ export class ToolbarFixture {
// this is for the engine animation, as it takes 500ms to complete
await this.page.waitForTimeout(600)
}
private _getMode = () =>
this.page.locator('[data-current-mode]').getAttribute('data-current-mode')
expectToolbarMode = {
toBe: (mode: ToolbarModeName) => expect.poll(this._getMode).toEqual(mode),
not: {
toBe: (mode: ToolbarModeName) =>
expect.poll(this._getMode).not.toEqual(mode),
},
}
private _serialiseFileTree = async () => {
return this.page
@ -176,6 +186,22 @@ export class ToolbarFixture {
).toBeVisible()
await this.page.getByTestId('dropdown-circle-three-points').click()
}
selectArc = async () => {
await this.page
.getByRole('button', { name: 'caret down Tangential Arc:' })
.click()
await expect(this.page.getByTestId('dropdown-arc')).toBeVisible()
await this.page.getByTestId('dropdown-arc').click()
}
selectThreePointArc = async () => {
await this.page
.getByRole('button', { name: 'caret down Tangential Arc:' })
.click()
await expect(
this.page.getByTestId('dropdown-three-point-arc')
).toBeVisible()
await this.page.getByTestId('dropdown-three-point-arc').click()
}
async closePane(paneId: SidebarType) {
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)

View File

@ -1956,7 +1956,6 @@ extrude001 = extrude(sketch001, length = -12)
// Locators
const firstEdgeLocation = { x: 600, y: 193 }
const secondEdgeLocation = { x: 600, y: 383 }
const bodyLocation = { x: 630, y: 290 }
const [clickOnFirstEdge] = scene.makeMouseHelpers(
firstEdgeLocation.x,
firstEdgeLocation.y
@ -1969,7 +1968,6 @@ extrude001 = extrude(sketch001, length = -12)
// Colors
const edgeColorWhite: [number, number, number] = [248, 248, 248]
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
const bodyColor: [number, number, number] = [155, 155, 155]
const chamferColor: [number, number, number] = [168, 168, 168]
const backgroundColor: [number, number, number] = [30, 30, 30]
const lowTolerance = 20

View File

@ -539,7 +539,8 @@ test.describe('Can export from electron app', () => {
try {
const outputGltf = await fsp.readFile(filepath)
return outputGltf.byteLength
} catch (e) {
} catch (error: unknown) {
void error
return 0
}
},

View File

@ -53,46 +53,47 @@ sketch003 = startSketchOn('XY')
|> close()
extrude003 = extrude(sketch003, length = 20)
`
test.describe('edit with AI example snapshots', () => {
test(
`change colour`,
{ tag: '@snapshot' },
async ({ context, homePage, cmdBar, editor, page, scene }) => {
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
test(
`change colour`,
{ tag: '@snapshot' },
async ({ context, homePage, cmdBar, editor, page, scene }) => {
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 }
const [clickBody1Cap] = scene.makeMouseHelpers(
body1CapCoords.x,
body1CapCoords.y
)
const yellow: [number, number, number] = [179, 179, 131]
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const body1CapCoords = { x: 571, y: 351 }
const [clickBody1Cap] = scene.makeMouseHelpers(
body1CapCoords.x,
body1CapCoords.y
)
const yellow: [number, number, number] = [179, 179, 131]
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
await test.step('wait for scene to load select body and check selection came through', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await clickBody1Cap()
await scene.expectPixelColor(yellow, body1CapCoords, 20)
await editor.expectState({
highlightedCode: '',
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
diagnostics: [],
await test.step('wait for scene to load select body and check selection came through', async () => {
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
await clickBody1Cap()
await scene.expectPixelColor(yellow, body1CapCoords, 20)
await editor.expectState({
highlightedCode: '',
activeLines: ['|>startProfileAt([-73.64,-42.89],%)'],
diagnostics: [],
})
})
})
await test.step('fire off edit prompt', async () => {
await cmdBar.captureTextToCadRequestSnapshot(test.info())
await cmdBar.openCmdBar('promptToEdit')
// being specific about the color with a hex means asserting pixel color is more stable
await page
.getByTestId('cmd-bar-arg-value')
.fill('make this neon green please, use #39FF14')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
})
}
)
await test.step('fire off edit prompt', async () => {
await cmdBar.captureTextToCadRequestSnapshot(test.info())
await cmdBar.openCmdBar('promptToEdit')
// being specific about the color with a hex means asserting pixel color is more stable
await page
.getByTestId('cmd-bar-arg-value')
.fill('make this neon green please, use #39FF14')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
})
}
)
})

View File

@ -319,7 +319,6 @@ extrude001 = extrude(sketch001, length = 50)
'when engine fails export we handle the failure and alert the user',
{ tag: '@skipLocalEngine' },
async ({ scene, page, homePage, cmdBar }) => {
const u = await getUtils(page)
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
@ -636,11 +635,8 @@ extrude001 = extrude(sketch001, length = 50)
await homePage.goToModelingScene()
})
const toolBarMode = () =>
page.locator('[data-currentMode]').getAttribute('data-currentMode')
await test.step('Start sketch and select a plane', async () => {
await expect.poll(toolBarMode).toEqual('modeling')
await toolbar.expectToolbarMode.toBe('modeling')
// Click the start sketch button
await toolbar.startSketchPlaneSelection()
@ -649,10 +645,10 @@ extrude001 = extrude(sketch001, length = 50)
// Check that the modeling toolbar doesn't appear during the animation
// The animation typically takes around 500ms, so we'll check for a second
await expect.poll(toolBarMode, { timeout: 1000 }).not.toEqual('modeling')
await toolbar.expectToolbarMode.not.toBe('modeling')
// After animation completes, we should see the sketching toolbar
await expect.poll(toolBarMode).toEqual('sketching')
await toolbar.expectToolbarMode.toBe('sketching')
})
})

View File

@ -1,8 +1,9 @@
import { readFileSync } from 'fs'
const secrets: Record<string, string> = {}
const secretsPath = './e2e/playwright/playwright-secrets.env'
try {
const file = readFileSync('./e2e/playwright/playwright-secrets.env', 'utf8')
const file = readFileSync(secretsPath, 'utf8')
file
.split('\n')
.filter((line) => line && line.length > 1)
@ -13,11 +14,15 @@ try {
// prefer env vars over secrets file
secrets[key] = process.env[key] || (value as any).replaceAll('"', '')
})
} catch (err) {
} catch (error: unknown) {
void error
// probably running in CI
secrets.token = process.env.token || ''
secrets.snapshottoken = process.env.snapshottoken || ''
// add more env vars here to make them available in CI
console.warn(
`Error reading ${secretsPath}; environment variables will be used`
)
}
secrets.token = secrets.token || process.env.token || ''
secrets.snapshottoken = secrets.snapshottoken || process.env.snapshottoken || ''
// add more env vars here to make them available in CI
export { secrets }

View File

@ -12,6 +12,7 @@ import {
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
import { SceneFixture } from './fixtures/sceneFixture'
import { CmdBarFixture } from './fixtures/cmdBarFixture'
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
@ -191,7 +192,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
page: Page,
homePage: HomePageFixture,
openPanes: string[],
scene: SceneFixture
scene: SceneFixture,
cmdBar: CmdBarFixture
) => {
// Load the app with the code panes
await page.addInitScript(async () => {
@ -201,13 +203,22 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|> startProfileAt([4.61, -14.01], %)
|> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -5.38], %)
|> arcTo({
interior = [20.18, -1.7],
end = [11.82, -1.16]
}, %)
|> arc({
radius = 5.92,
angleStart = -89.36,
angleEnd = 135.81
}, %)
|> close()`
)
})
const u = await getUtils(page)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -242,7 +253,17 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|> startProfileAt([4.61, -14.01], %)
|> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -5.38], %)
|> close()`)
|> arcTo({
interior = [20.18, -1.7],
end = [11.82, -1.16]
}, %)
|> arc({
radius = 5.92,
angleStart = -89.36,
angleEnd = 135.81
}, %)
|> close()
`)
} else {
// Ensure we don't see the code.
await expect(u.codeLocator).not.toBeVisible()
@ -272,7 +293,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
const step5 = { steps: 5 }
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
await expect(page.getByTestId('segment-overlay')).toHaveCount(5)
// drag startProfileAt handle
await page.mouse.move(startPX[0], startPX[1])
@ -310,22 +331,93 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// drag arcTo interior handle (three point arc)
const arcToHandle = await u.getBoundingBox('[data-overlay-index="2"]')
await page.mouse.move(arcToHandle.x, arcToHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcToHandle.x - dragPX,
arcToHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag arcTo end handle (three point arc)
const arcToEndHandle = await u.getBoundingBox('[data-overlay-index="3"]')
await page.mouse.move(arcToEndHandle.x, arcToEndHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcToEndHandle.x - dragPX,
arcToEndHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag arc radius handle
const arcRadiusHandle = await u.getBoundingBox('[data-overlay-index="4"]')
await page.mouse.move(arcRadiusHandle.x, arcRadiusHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcRadiusHandle.x - dragPX,
arcRadiusHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// drag arc center handle (we'll have to hardcode the position because it doesn't have a overlay near the handle)
const arcCenterHandle = { x: 745, y: 214 }
await page.mouse.move(arcCenterHandle.x, arcCenterHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcCenterHandle.x - dragPX,
arcCenterHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// Open the code pane
await u.openKclCodePanel()
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([6.44, -12.07], %)
|> line(end = [14.72, 1.97])
|> tangentialArcTo([24.95, -5.38], %)
|> line(end = [1.97, 2.06])
|> close()`)
|> startProfileAt([6.44, -12.07], %)
|> line(end = [14.72, 1.97])
|> tangentialArcTo([26.92, -3.32], %)
|> arcTo({
interior = [18.11, -3.73],
end = [9.77, -3.19]
}, %)
|> arc({
radius = 3.75,
angleStart = -58.29,
angleEnd = 161.17
}, %)
|> close()
`)
}
test(
'code pane open at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage, scene }) => {
async ({ page, homePage, scene, cmdBar }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
@ -338,14 +430,20 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
})
)
})
await doEditSegmentsByDraggingHandle(page, homePage, ['code'], scene)
await doEditSegmentsByDraggingHandle(
page,
homePage,
['code'],
scene,
cmdBar
)
}
)
test(
'code pane closed at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage, scene }) => {
async ({ page, homePage, scene, cmdBar }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
@ -353,7 +451,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [], scene)
await doEditSegmentsByDraggingHandle(page, homePage, [], scene, cmdBar)
}
)
})
@ -362,6 +460,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
page,
editor,
homePage,
scene,
cmdBar,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -373,6 +473,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
})
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -1355,7 +1457,7 @@ test.describe('multi-profile sketching', () => {
test(
`test it removes half-finished expressions when changing tools in sketch mode`,
{ tag: ['@skipWin'] },
async ({ context, page, scene, toolbar, editor, homePage }) => {
async ({ context, page, scene, toolbar, editor, homePage, cmdBar }) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
@ -1375,7 +1477,10 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
)
})
const [continueProfile2Clk] = scene.makeMouseHelpers(954, 282)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -1386,7 +1491,13 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
const [circlePoint1] = scene.makeMouseHelpers(700, 200)
await test.step('equip circle tool and click first point', async () => {
await toolbar.circleBtn.click()
// await page.waitForTimeout(100)
await expect
.poll(async () => {
await toolbar.circleBtn.click()
return toolbar.circleBtn.getAttribute('aria-pressed')
})
.toBe('true')
await page.waitForTimeout(100)
await circlePoint1()
await editor.expectEditor.toContain(
@ -1401,6 +1512,7 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
const [circle3Point1] = scene.makeMouseHelpers(650, 200)
const [circle3Point2] = scene.makeMouseHelpers(750, 200)
// const [circle3Point3] = scene.makeMouseHelpers(700, 150)
await test.step('equip three point circle tool and click first two points', async () => {
await toolbar.selectCircleThreePoint()
@ -1411,25 +1523,40 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
await editor.expectEditor.toContain('profile003 = circleThreePoint(')
})
await test.step('equip line tool and verify three point circle code is removed', async () => {
await test.step('equip line tool and verify three-point circle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain(
'profile003 = circleThreePoint('
)
})
await test.step('equip three-point-arc tool and click first two points', async () => {
await page.waitForTimeout(200)
await toolbar.selectThreePointArc()
await page.waitForTimeout(200)
await circle3Point1()
await page.waitForTimeout(200)
await circle3Point2()
await editor.expectEditor.toContain('arcTo({')
})
await test.step('equip line tool and verify three-point-arc code is removed after second click', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('arcTo({')
})
const [cornerRectPoint1] = scene.makeMouseHelpers(600, 300)
await test.step('equip corner rectangle tool and click first point', async () => {
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await cornerRectPoint1()
await editor.expectEditor.toContain('profile003 = startProfileAt(')
await editor.expectEditor.toContain('profile004 = startProfileAt(')
})
await test.step('equip line tool and verify corner rectangle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('profile003 = startProfileAt(')
await editor.expectEditor.not.toContain('profile004 = startProfileAt(')
})
const [centerRectPoint1] = scene.makeMouseHelpers(700, 300)
@ -1438,12 +1565,24 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
await toolbar.selectCenterRectangle()
await page.waitForTimeout(100)
await centerRectPoint1()
await editor.expectEditor.toContain('profile003 = startProfileAt(')
await editor.expectEditor.toContain('profile004 = startProfileAt(')
})
await test.step('equip line tool and verify center rectangle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('profile003 = startProfileAt(')
await editor.expectEditor.not.toContain('profile004 = startProfileAt(')
})
await test.step('continue profile002 with the three point arc tool, and then switch back to the line tool to verify it only removes the last expression in the pipe', async () => {
await toolbar.selectThreePointArc()
await page.waitForTimeout(200)
await continueProfile2Clk()
await page.waitForTimeout(200)
await circle3Point1()
await editor.expectEditor.toContain('arcTo({')
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('arcTo({')
await editor.expectEditor.toContain('profile002')
})
}
)
@ -1532,6 +1671,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
}) => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -1595,7 +1735,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await test.step('Create a close profile stopping mid profile to equip the tangential arc, and than back to the line tool', async () => {
await test.step('Create a close profile stopping mid profile to equip the tangential arc, then three-point arc, and then back to the line tool', async () => {
await startProfile1()
await editor.expectEditor.toContain(
`profile001 = startProfileAt([4.61, 12.21], sketch001)`
@ -1613,12 +1753,45 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
await editor.expectEditor.toContain(
`|> tangentialArcTo([16.61, 4.14], %)`
)
// Add a three-point arc segment
await toolbar.selectThreePointArc()
await page.waitForTimeout(300)
// select end of profile again
await endLineStartTanArc()
await page.waitForTimeout(300)
// Define points for the three-point arc
const [threePointInterior, threePointInteriorMove] =
scene.makeMouseHelpers(600, 200)
const [threePointEnd, threePointEndMove] = scene.makeMouseHelpers(
590,
270
)
// Create the three-point arc
await page.waitForTimeout(300)
await threePointInteriorMove()
await threePointInterior()
await page.waitForTimeout(300)
await threePointEndMove()
await threePointEnd()
await page.waitForTimeout(300)
// Verify the three-point arc was created correctly
await editor.expectEditor.toContain(`|> arcTo(`)
// Switch back to line tool to continue
await toolbar.lineBtn.click()
await page.waitForTimeout(300)
await endArcStartLine()
// Continue with the original line segment
await threePointEnd()
await page.waitForTimeout(300)
await page.mouse.click(572, 110)
await editor.expectEditor.toContain(`|> line(end = [-11.73, 5.35])`)
await editor.expectEditor.toContain(`|> line(end = [-1.22, 10.85])`)
await startProfile1()
await editor.expectEditor.toContain(
`|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
@ -1820,8 +1993,68 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
)
})
await test.step('double check that circle three point can be unequiped', async () => {
// this was tested implicitly for other tools, but not for circle three point since it's last
await test.step('create three-point arcs in a row without an unequip', async () => {
// Define points for the first three-point arc
const [arc1Point1, arc1Point1Move] = scene.makeMouseHelpers(700, 397)
const [arc1Point2, arc1Point2Move] = scene.makeMouseHelpers(724, 346)
const [arc1Point3, arc1Point3Move] = scene.makeMouseHelpers(785, 415)
// Define points for the second three-point arc
const [arc2Point1, arc2Point1Move] = scene.makeMouseHelpers(792, 225)
const [arc2Point2, arc2Point2Move] = scene.makeMouseHelpers(820, 207)
const [arc2Point3, arc2Point3Move] = scene.makeMouseHelpers(905, 229)
// Select the three-point arc tool
await toolbar.selectThreePointArc()
// Create the first three-point arc
await arc1Point1Move()
await arc1Point1()
await page.waitForTimeout(300)
await arc1Point2Move()
await arc1Point2()
await page.waitForTimeout(300)
await arc1Point3Move()
await arc1Point3()
await page.waitForTimeout(300)
// Verify the first three-point arc was created correctly
await editor.expectEditor.toContain(
`profile011 = startProfileAt([13.56, -9.97], sketch001)
|> arcTo({
interior = [15.19, -6.51],
end = [19.33, -11.19]
}, %)`,
{ shouldNormalise: true }
)
// Create the second three-point arc
await arc2Point1Move()
await arc2Point1()
await page.waitForTimeout(300)
await arc2Point2Move()
await arc2Point2()
await page.waitForTimeout(300)
await arc2Point3Move()
await arc2Point3()
await page.waitForTimeout(300)
// Verify the second three-point arc was created correctly
await editor.expectEditor.toContain(
` |> arcTo({
interior = [19.8, 1.7],
end = [21.7, 2.92]
}, %)
|> arcTo({
interior = [27.47, 1.42],
end = [27.57, 1.52]
}, %)`,
{ shouldNormalise: true }
)
})
await test.step('double check that three-point arc can be unequipped', async () => {
// this was tested implicitly for other tools, but not for three-point arc since it's last
await page.waitForTimeout(300)
await expect
.poll(async () => {
@ -2085,7 +2318,7 @@ profile003 = circle(sketch001, center = [6.92, -4.2], radius = 3.16)
test(
'can enter sketch when there is an extrude',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, page }) => {
async ({ homePage, scene, toolbar, page, cmdBar }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -2122,6 +2355,8 @@ extrude001 = extrude(profile003, length = 5)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -2134,9 +2369,11 @@ extrude001 = extrude(profile003, length = 5)
await page.waitForTimeout(600)
await test.step('check the sketch is still drawn properly', async () => {
await scene.expectPixelColor([255, 255, 255], { x: 596, y: 165 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 641, y: 220 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 763, y: 214 }, 15)
await Promise.all([
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 596, y: 165 }, 15),
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 641, y: 220 }, 15),
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 763, y: 214 }, 15),
])
})
}
)
@ -2293,7 +2530,7 @@ extrude001 = extrude(thePart, length = 75)
test(
'Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, editor, page }) => {
async ({ homePage, scene, toolbar, editor, page, cmdBar }) => {
// TODO this test should include a test for selecting revolve walls and caps
await page.addInitScript(async () => {
@ -2378,6 +2615,8 @@ extrude003 = extrude(profile011, length = 2.5)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -2387,11 +2626,6 @@ extrude003 = extrude(profile011, length = 2.5)
{ x: 834, y: -680, z: 534 },
{ x: -54, y: -476, z: 148 }
)
const camPositionForSelectingSketchOnCapProfiles = () =>
scene.moveCameraTo(
{ x: 404, y: 690, z: 38 },
{ x: 16, y: -140, z: -10 }
)
const wallSelectionOptions = [
{
title: 'select wall segment',
@ -2414,103 +2648,25 @@ extrude003 = extrude(profile011, length = 2.5)
selectClick: scene.makeMouseHelpers(836, 103)[0],
},
] as const
const capSelectionOptions = [
{
title: 'select cap segment',
selectClick: scene.makeMouseHelpers(688, 91)[0],
},
{
title: 'select cap solid 2d',
selectClick: scene.makeMouseHelpers(733, 204)[0],
},
// TODO keeps failing
// {
// title: 'select cap circle',
// selectClick: scene.makeMouseHelpers(679, 290)[0],
// },
{
title: 'select cap extrude wall',
selectClick: scene.makeMouseHelpers(649, 402)[0],
},
{
title: 'select cap extrude cap',
selectClick: scene.makeMouseHelpers(693, 408)[0],
},
] as const
const verifyWallProfilesAreDrawn = async () =>
test.step('verify wall profiles are drawn', async () => {
// open polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 599, y: 168 },
15
)
// closed polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 656, y: 171 },
15
)
// revolved profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 655, y: 264 },
15
)
// extruded profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 808, y: 396 },
15
)
// circle
await scene.expectPixelColor(
[
TEST_COLORS.WHITE,
TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue
],
{ x: 742, y: 386 },
15
)
})
const verifyCapProfilesAreDrawn = async () =>
test.step('verify cap profiles are drawn', async () => {
// open polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
// TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue
{ x: 620, y: 58 },
15
)
// revolved profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 641, y: 110 },
15
)
// closed polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 632, y: 200 },
15
)
// extruded profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 628, y: 410 },
15
)
// circle
await scene.expectPixelColor(
[
TEST_COLORS.WHITE,
TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue
],
{ x: 681, y: 303 },
15
)
await Promise.all([
// open polygon
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 599, y: 168 }, 15),
// closed polygon
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 656, y: 171 }, 15),
// revolved profile
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 655, y: 264 }, 15),
// extruded profile
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 808, y: 396 }, 15),
// circle (When entering via the circle, it's selected and therefore blue)
scene.expectPixelColor(
[TEST_COLORS.WHITE, TEST_COLORS.BLUE],
{ x: 742, y: 386 },
15
),
])
})
await test.step('select wall profiles', async () => {

View File

@ -7,12 +7,7 @@ import { spawn } from 'child_process'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import JSZip from 'jszip'
import path from 'path'
import {
IS_PLAYWRIGHT_KEY,
TEST_SETTINGS,
TEST_SETTINGS_KEY,
} from './storageStates'
import * as TOML from '@iarna/toml'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
import { SceneFixture } from './fixtures/sceneFixture'
import { CmdBarFixture } from './fixtures/cmdBarFixture'
@ -410,9 +405,9 @@ test.describe(
test(
'Draft segments should look right',
{ tag: '@snapshot' },
async ({ page, context, scene, cmdBar }) => {
async ({ page, scene, toolbar }) => {
// FIXME: Skip on macos its being weird.
test.skip(process.platform === 'darwin', 'Skip on macos')
// test.skip(process.platform === 'darwin', 'Skip on macos')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -421,6 +416,23 @@ test(
await scene.connectionEstablished()
const startXPx = 600
const [endOfTangentClk, endOfTangentMv] = scene.makeMouseHelpers(
startXPx + PUR * 30,
500 - PUR * 20,
{ steps: 10 }
)
const [threePointArcMidPointClk, threePointArcMidPointMv] =
scene.makeMouseHelpers(800, 250, { steps: 10 })
const [threePointArcEndPointClk, threePointArcEndPointMv] =
scene.makeMouseHelpers(750, 285, { steps: 10 })
const [arcCenterClk, arcCenterMv] = scene.makeMouseHelpers(750, 210, {
steps: 10,
})
const [arcEndClk, arcEndMv] = scene.makeMouseHelpers(750, 150, {
steps: 10,
})
// click on "Start Sketch" button
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
@ -435,7 +447,6 @@ test(
await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
await expect(page.locator('.cm-content')).toHaveText(code)
@ -471,12 +482,52 @@ test(
await page.mouse.move(813, 392, { steps: 10 })
await page.waitForTimeout(500)
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await endOfTangentMv()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await endOfTangentClk()
await toolbar.selectThreePointArc()
await page.waitForTimeout(500)
await endOfTangentClk()
await threePointArcMidPointMv()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await threePointArcMidPointClk()
await page.waitForTimeout(100)
await threePointArcEndPointMv()
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await threePointArcEndPointClk()
await page.waitForTimeout(100)
await toolbar.selectArc()
await page.waitForTimeout(100)
// continue the profile
await threePointArcEndPointClk()
await page.waitForTimeout(100)
await arcCenterMv()
await page.waitForTimeout(500)
await arcCenterClk()
await arcEndMv()
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await arcEndClk()
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -0,0 +1,33 @@
{
"original_source_code": "sketch001 = startSketchOn('XZ')\nprofile001 = startProfileAt([57.81, 250.51], sketch001)\n |> line(end = [121.13, 56.63], tag = $seg02)\n |> line(end = [83.37, -34.61], tag = $seg01)\n |> line(end = [19.66, -116.4])\n |> line(end = [-221.8, -41.69])\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = 200)\nsketch002 = startSketchOn('XZ')\n |> startProfileAt([-73.64, -42.89], %)\n |> xLine(length = 173.71)\n |> line(end = [-22.12, -94.4])\n |> xLine(length = -156.98)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude002 = extrude(sketch002, length = 50)\nsketch003 = startSketchOn('XY')\n |> startProfileAt([52.92, 157.81], %)\n |> angledLine([0, 176.4], %, $rectangleSegmentA001)\n |> angledLine([\n segAng(rectangleSegmentA001) - 90,\n 53.4\n ], %, $rectangleSegmentB001)\n |> angledLine([\n segAng(rectangleSegmentA001),\n -segLen(rectangleSegmentA001)\n ], %, $rectangleSegmentC001)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude003 = extrude(sketch003, length = 20)\n",
"prompt": "make this neon green please, use #39FF14",
"source_ranges": [
{
"prompt": "The users main selection is the end cap of a general-sweep (that is an extrusion, revolve, sweep or loft).\nThe source range most likely refers to \"startProfileAt\" simply because this is the start of the profile that was swept.\nIf you need to operate on this cap, for example for sketching on the face, you can use the special string END i.e. `startSketchOn(someSweepVariable, END)`\nWhen they made this selection they main have intended this surface directly or meant something more general like the sweep body.\nSee later source ranges for more context.",
"range": {
"start": {
"line": 11,
"column": 5
},
"end": {
"line": 11,
"column": 40
}
}
},
{
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
"range": {
"start": {
"line": 17,
"column": 13
},
"end": {
"line": 17,
"column": 44
}
}
}
],
"kcl_version": "0.2.50"
}

View File

@ -1,4 +1,3 @@
import { MouseControlType } from '@rust/kcl-lib/bindings/MouseControlType'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme'

View File

@ -2,15 +2,12 @@ import {
expect,
BrowserContext,
TestInfo,
_electron as electron,
ElectronApplication,
Locator,
Page,
} from '@playwright/test'
import { test } from './zoo-test'
import { EngineCommand } from 'lang/std/artifactGraph'
import fsp from 'fs/promises'
import fsSync from 'fs'
import path from 'path'
import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs'
@ -24,14 +21,11 @@ import {
IS_PLAYWRIGHT_KEY,
} from './storageStates'
import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants'
import { isErrorWhitelisted } from './lib/console-error-whitelist'
import { isArray } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'lang/wasm'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
const toNormalizedCode = (text: string) => {
return text.replace(/\s+/g, '')
@ -928,10 +922,6 @@ export async function setup(
// await page.reload()
}
let electronApp: ElectronApplication | undefined = undefined
let context: BrowserContext | undefined = undefined
let page: Page | undefined = undefined
function failOnConsoleErrors(page: Page, testInfo?: TestInfo) {
// enabled for chrome for now
if (page.context().browser()?.browserType().name() === 'chromium') {

View File

@ -4,7 +4,6 @@ import { bracket } from 'lib/exampleKcl'
import * as fsp from 'fs/promises'
import { join } from 'path'
import { FILE_EXT } from 'lib/constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
test.describe('Testing in-app sample loading', () => {
/**
@ -49,8 +48,6 @@ test.describe('Testing in-app sample loading', () => {
})
const warningText = page.getByText('Overwrite current file and units?')
const confirmButton = page.getByRole('button', { name: 'Submit command' })
const unitsToast = (unit: UnitLength_type) =>
page.getByText(`Set default unit to "${unit}" for this project`)
await test.step(`Precondition: check the initial code`, async () => {
await u.openKclCodePanel()
@ -125,8 +122,6 @@ test.describe('Testing in-app sample loading', () => {
page.getByRole('listitem').filter({
has: page.getByRole('button', { name }),
})
const unitsToast = (unit: UnitLength_type) =>
page.getByText(`Set default unit to "${unit}" for this project`)
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })

View File

@ -159,7 +159,6 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
const unconstrainedLocator = page.locator(
`[data-constraint-type="${constraintType}"][data-is-constrained="false"]`
)
await expect(unconstrainedLocator).toBeVisible()
await unconstrainedLocator.hover()
await expect(
await page.getByTestId('constraint-symbol-popover').count()
@ -274,8 +273,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
let ang = 0
const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
ang = await u.getAngle(`[data-overlay-index="${0}"]`)
const line = await u.getBoundingBox('[data-overlay-index="0"]')
ang = await u.getAngle('[data-overlay-index="0"]')
console.log('line1', line, ang)
await clickConstrained({
hoverPos: { x: line.x, y: line.y },
@ -297,8 +296,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
locator: '[data-overlay-index="0"]',
})
const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`)
ang = await u.getAngle(`[data-overlay-index="1"]`)
const angledLine = await u.getBoundingBox('[data-overlay-index="1"]')
ang = await u.getAngle('[data-overlay-index="1"]')
console.log('angledLine1')
await clickConstrained({
hoverPos: { x: angledLine.x, y: angledLine.y },
@ -327,8 +326,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
await page.mouse.move(700, 250)
await page.waitForTimeout(100)
let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
ang = await u.getAngle(`[data-overlay-index="2"]`)
let lineTo = await u.getBoundingBox('[data-overlay-index="2"]')
ang = await u.getAngle('[data-overlay-index="2"]')
console.log('lineTo1')
await clickConstrained({
hoverPos: { x: lineTo.x, y: lineTo.y },
@ -353,8 +352,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
locator: '[data-overlay-toolbar-index="2"]',
})
const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`)
ang = await u.getAngle(`[data-overlay-index="3"]`)
const xLineTo = await u.getBoundingBox('[data-overlay-index="3"]')
ang = await u.getAngle('[data-overlay-index="3"]')
console.log('xlineTo1')
await clickConstrained({
hoverPos: { x: xLineTo.x, y: xLineTo.y },
@ -419,8 +418,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
let ang = 0
const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`)
ang = await u.getAngle(`[data-overlay-index="4"]`)
const yLineTo = await u.getBoundingBox('[data-overlay-index="4"]')
ang = await u.getAngle('[data-overlay-index="4"]')
console.log('ylineTo1')
await clickUnconstrained({
hoverPos: { x: yLineTo.x, y: yLineTo.y - 200 },
@ -432,8 +431,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
locator: '[data-overlay-toolbar-index="4"]',
})
const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`)
ang = await u.getAngle(`[data-overlay-index="5"]`)
const xLine = await u.getBoundingBox('[data-overlay-index="5"]')
ang = await u.getAngle('[data-overlay-index="5"]')
console.log('xline')
await clickUnconstrained({
hoverPos: { x: xLine.x, y: xLine.y },
@ -501,8 +500,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
let ang = 0
const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`)
ang = await u.getAngle(`[data-overlay-index="6"]`)
const yLine = await u.getBoundingBox('[data-overlay-index="6"]')
ang = await u.getAngle('[data-overlay-index="6"]')
console.log('yline1')
await clickConstrained({
hoverPos: { x: yLine.x, y: yLine.y },
@ -515,9 +514,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
const angledLineOfXLength = await u.getBoundingBox(
`[data-overlay-index="7"]`
'[data-overlay-index="7"]'
)
ang = await u.getAngle(`[data-overlay-index="7"]`)
ang = await u.getAngle('[data-overlay-index="7"]')
console.log('angledLineOfXLength1')
await clickConstrained({
hoverPos: { x: angledLineOfXLength.x, y: angledLineOfXLength.y },
@ -547,9 +546,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
const angledLineOfYLength = await u.getBoundingBox(
`[data-overlay-index="8"]`
'[data-overlay-index="8"]'
)
ang = await u.getAngle(`[data-overlay-index="8"]`)
ang = await u.getAngle('[data-overlay-index="8"]')
console.log('angledLineOfYLength1')
await clickUnconstrained({
hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y },
@ -632,8 +631,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
let ang = 0
const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`)
ang = await u.getAngle(`[data-overlay-index="9"]`)
const angledLineToX = await u.getBoundingBox('[data-overlay-index="9"]')
ang = await u.getAngle('[data-overlay-index="9"]')
console.log('angledLineToX')
await clickConstrained({
hoverPos: { x: angledLineToX.x, y: angledLineToX.y },
@ -659,9 +658,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
const angledLineToY = await u.getBoundingBox(
`[data-overlay-index="10"]`
'[data-overlay-index="10"]'
)
ang = await u.getAngle(`[data-overlay-index="10"]`)
ang = await u.getAngle('[data-overlay-index="10"]')
console.log('angledLineToY')
await clickUnconstrained({
hoverPos: { x: angledLineToY.x, y: angledLineToY.y },
@ -689,9 +688,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
const angledLineThatIntersects = await u.getBoundingBox(
`[data-overlay-index="11"]`
'[data-overlay-index="11"]'
)
ang = await u.getAngle(`[data-overlay-index="11"]`)
ang = await u.getAngle('[data-overlay-index="11"]')
console.log('angledLineThatIntersects')
await clickUnconstrained({
hoverPos: {
@ -821,6 +820,138 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
locator: '[data-overlay-toolbar-index="12"]',
})
})
test('for segment [arcTo]', async ({
page,
editor,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([56.37, 120.33], sketch001)
|> line(end = [162.86, 106.48])
|> arcTo({
interior = [360.16, 231.76],
end = [391.48, 131.54]
}, %)
|> yLine(-131.54, %)
|> arc({
radius = 126.46,
angleStart = 33.53,
angleEnd = -141.07
}, %)
`
)
localStorage.setItem('disableAxis', 'true')
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
// wait for execution done
await page.getByText('line(end = [162.86, 106.48])').click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
await expect(page.getByTestId('segment-overlay')).toHaveCount(5)
const clickUnconstrained = _clickUnconstrained(page, editor)
const clickConstrained = _clickConstrained(page, editor)
const arcTo = await u.getBoundingBox('[data-overlay-index="1"]')
let ang = await u.getAngle('[data-overlay-index="1"]')
console.log('arcTo interior x')
await clickUnconstrained({
hoverPos: { x: arcTo.x, y: arcTo.y },
constraintType: 'xAbsolute',
expectBeforeUnconstrained: `arcTo({
interior = [360.16, 231.76],
end = [391.48, 131.54]
}, %)`,
expectAfterUnconstrained: `arcTo({
interior = [360.16, 231.76],
end = [391.48, 131.54]
}, %)`,
expectFinal: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
ang: ang,
steps: 6,
locator: '[data-overlay-toolbar-index="1"]',
})
console.log('arcTo interior y')
await clickUnconstrained({
hoverPos: { x: arcTo.x, y: arcTo.y },
constraintType: 'yAbsolute',
expectBeforeUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
expectAfterUnconstrained: `arcTo({
interior = [xAbs001, yAbs001],
end = [391.48, 131.54]
}, %)`,
expectFinal: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
ang: ang,
steps: 10,
locator: '[data-overlay-toolbar-index="1"]',
})
console.log('arcTo end x')
await clickConstrained({
hoverPos: { x: arcTo.x, y: arcTo.y },
constraintType: 'xAbsolute',
expectBeforeUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
expectAfterUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [391.48, 131.54]
}, %)`,
expectFinal: `arcTo({
interior = [xAbs001, 231.76],
end = [xAbs002, 131.54]
}, %)`,
ang: ang + 180,
steps: 6,
locator: '[data-overlay-toolbar-index="1"]',
})
console.log('arcTo end y')
await clickUnconstrained({
hoverPos: { x: arcTo.x, y: arcTo.y },
constraintType: 'yAbsolute',
expectBeforeUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [xAbs002, 131.54]
}, %)`,
expectAfterUnconstrained: `arcTo({
interior = [xAbs001, 231.76],
end = [xAbs002, yAbs002]
}, %)`,
expectFinal: `arcTo({
interior = [xAbs001, 231.76],
end = [xAbs002, 131.54]
}, %)`,
ang: ang + 180,
steps: 10,
locator: '[data-overlay-toolbar-index="1"]',
})
})
test('for segment [circle]', async ({ page, editor, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
@ -928,36 +1059,55 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
shouldNormalise: true,
})
await page.locator(`[data-stdlib-fn-name="${stdLibFnName}"]`).click()
await page
.locator(`[data-stdlib-fn-name="${stdLibFnName}"]`)
.first()
.click()
await page.getByText('Delete Segment').click()
await editor.expectEditor.not.toContain(codeToBeDeleted, {
shouldNormalise: true,
})
}
test('all segment types', async ({ page, editor, homePage }) => {
test('all segment types', async ({
page,
editor,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`part001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line(end = [0.5, -14 + 0])
|> angledLine({ angle = 3 + 0, length = 32 + 0 }, %)
|> line(endAbsolute = [33, 11.5 + 0])
|> xLine(endAbsolute = 9 - 5)
|> yLine(endAbsolute = -10.77, tag = $a)
|> xLine(length = 26.04)
|> yLine(length = 21.14 + 0)
|> angledLineOfXLength({ angle = 181 + 0, length = 23.14 }, %)
|> angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)
|> angledLineToX({ angle = 3 + 0, to = 26 }, %)
|> angledLineToY({ angle = 89, to = 9.14 + 0 }, %)
|> angledLineThatIntersects({
angle = 4.14,
intersectTag = a,
offset = 9
}, %)
|> tangentialArcTo([3.14 + 13, 1.14], %)
|>startProfileAt([0, 0], %)
|> line(end = [0.5, -14 + 0])
|> angledLine({ angle = 3 + 0, length = 32 + 0 }, %)
|> line(endAbsolute = [33, 11.5 + 0])
|> xLine(endAbsolute = 9 - 5)
|> yLine(endAbsolute = -10.77, tag = $a)
|> xLine(length = 26.04)
|> yLine(length = 21.14 + 0)
|> angledLineOfXLength({ angle = 181 + 0, length = 23.14 }, %)
|> angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)
|> angledLineToX({ angle = 3 + 0, to = 26 }, %)
|> angledLineToY({ angle = 89, to = 9.14 + 0 }, %)
|> angledLineThatIntersects({
angle = 4.14,
intersectTag = a,
offset = 9
}, %)
|> tangentialArcTo([3.14 + 13, 1.14], %)
|> arcTo({
interior = [16.25, 5.12],
end = [21.61, 4.15]
}, %)
|> arc({
radius = 9.03,
angleStart = 40.27,
angleEnd = -38.05
}, %)
`
)
localStorage.setItem('disableAxis', 'true')
@ -966,27 +1116,55 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await u.waitForPageLoad()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page.getByText('xLine(endAbsolute = 9 - 5)').click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
await expect(page.getByTestId('segment-overlay')).toHaveCount(16)
const deleteSegmentSequence = _deleteSegmentSequence(page, editor)
let segmentToDelete
const getOverlayByIndex = (index: number) =>
u.getBoundingBox(`[data-overlay-index="${index}"]`)
segmentToDelete = await getOverlayByIndex(14)
let ang = await u.getAngle('[data-overlay-index="14"]')
await editor.scrollToText('angleEnd')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: `arc({
radius = 9.03,
angleStart = 40.27,
angleEnd = -38.05
}, %)`,
stdLibFnName: 'arc',
ang: ang + 180,
steps: 6,
locator: '[data-overlay-toolbar-index="14"]',
})
segmentToDelete = await getOverlayByIndex(13)
ang = await u.getAngle('[data-overlay-index="13"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: `arcTo({
interior = [16.25, 5.12],
end = [21.61, 4.15]
}, %)`,
stdLibFnName: 'arcTo',
ang: ang,
steps: 6,
locator: '[data-overlay-toolbar-index="13"]',
})
segmentToDelete = await getOverlayByIndex(12)
let ang = await u.getAngle(`[data-overlay-index="${12}"]`)
ang = await u.getAngle('[data-overlay-index="12"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'tangentialArcTo([3.14 + 13, 1.14], %)',
@ -997,7 +1175,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(11)
ang = await u.getAngle(`[data-overlay-index="${11}"]`)
ang = await u.getAngle('[data-overlay-index="11"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: `angledLineThatIntersects({
@ -1012,7 +1190,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(10)
ang = await u.getAngle(`[data-overlay-index="${10}"]`)
ang = await u.getAngle('[data-overlay-index="10"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'angledLineToY({ angle = 89, to = 9.14 + 0 }, %)',
@ -1022,7 +1200,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(9)
ang = await u.getAngle(`[data-overlay-index="${9}"]`)
ang = await u.getAngle('[data-overlay-index="9"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'angledLineToX({ angle = 3 + 0, to = 26 }, %)',
@ -1032,7 +1210,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(8)
ang = await u.getAngle(`[data-overlay-index="${8}"]`)
ang = await u.getAngle('[data-overlay-index="8"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted:
@ -1043,7 +1221,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(7)
ang = await u.getAngle(`[data-overlay-index="${7}"]`)
ang = await u.getAngle('[data-overlay-index="7"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted:
@ -1054,7 +1232,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(6)
ang = await u.getAngle(`[data-overlay-index="${6}"]`)
ang = await u.getAngle('[data-overlay-index="6"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'yLine(length = 21.14 + 0)',
@ -1064,7 +1242,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(5)
ang = await u.getAngle(`[data-overlay-index="${5}"]`)
ang = await u.getAngle('[data-overlay-index="5"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'xLine(length = 26.04)',
@ -1074,7 +1252,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(4)
ang = await u.getAngle(`[data-overlay-index="${4}"]`)
ang = await u.getAngle('[data-overlay-index="4"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'yLine(endAbsolute = -10.77, tag = $a)',
@ -1084,7 +1262,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(3)
ang = await u.getAngle(`[data-overlay-index="${3}"]`)
ang = await u.getAngle('[data-overlay-index="3"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'xLine(endAbsolute = 9 - 5)',
@ -1094,7 +1272,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(2)
ang = await u.getAngle(`[data-overlay-index="${2}"]`)
ang = await u.getAngle('[data-overlay-index="2"]')
await expect(page.getByText('Added variable')).not.toBeVisible()
const hoverPos = { x: segmentToDelete.x, y: segmentToDelete.y }
@ -1127,7 +1305,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(1)
ang = await u.getAngle(`[data-overlay-index="${1}"]`)
ang = await u.getAngle('[data-overlay-index="1"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'angledLine({ angle = 3 + 0, length = 32 + 0 }, %)',
@ -1137,7 +1315,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
})
segmentToDelete = await getOverlayByIndex(0)
ang = await u.getAngle(`[data-overlay-index="${0}"]`)
ang = await u.getAngle('[data-overlay-index="0"]')
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
codeToBeDeleted: 'line(end = [0.5, -14 + 0])',
@ -1208,7 +1386,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
return true
} catch (_) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_e) {
return false
}
})
@ -1366,7 +1545,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
await expect(page.getByText('Added variable')).not.toBeVisible()
const hoverPos = await u.getBoundingBox(`[data-overlay-index="0"]`)
let ang = await u.getAngle(`[data-overlay-index="${0}"]`)
let ang = await u.getAngle('[data-overlay-index="0"]')
ang += 180
await page.mouse.move(0, 0)

View File

@ -364,7 +364,6 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await camPosition1()
const revolve = { x: 635, y: 253 }
const parentExtrude = { x: 915, y: 133 }
const solid2d = { x: 770, y: 167 }
const individualProfile = { x: 694, y: 432 }

View File

@ -7,7 +7,7 @@ import {
createProject,
tomlToSettings,
} from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import {
TEST_SETTINGS_KEY,
@ -15,7 +15,6 @@ import {
TEST_SETTINGS,
TEST_SETTINGS_DEFAULT_THEME,
} from './storageStates'
import * as TOML from '@iarna/toml'
import { DeepPartial } from 'lib/types'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -1006,11 +1005,6 @@ fn cube`
page.getByText(
`Set highlight edges to "${String(value)}" as a user default`
)
const initialPath = testInfo.snapshotPath('toggle-settings-initial.png')
const initialScreenshot = await scene.streamWrapper.screenshot({
path: initialPath,
mask: [page.getByTestId('model-state-indicator')],
})
await test.step(`Toggle highlightEdges off`, async () => {
await cmdBar.openCmdBar()

View File

@ -1,6 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { test as playwrightTestFn, ElectronApplication } from '@playwright/test'
import { test as playwrightTestFn } from '@playwright/test'
import {
fixturesBasedOnProcessEnvPlatform,
@ -8,8 +8,6 @@ import {
ElectronZoo,
} from './fixtures/fixtureSetup'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
import { DeepPartial } from 'lib/types'
export { expect } from '@playwright/test'
declare module '@playwright/test' {

View File

@ -1,4 +1,5 @@
import { defineConfig, devices } from '@playwright/test'
import { platform } from 'os'
/**
* See https://playwright.dev/docs/test-configuration.
@ -13,7 +14,7 @@ export default defineConfig({
/* Do not retry */
retries: 0,
/* Different amount of parallelism on CI and local. */
workers: 1,
workers: platform() === 'win32' ? 1 : 2,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
['dot'],

27
rust/Cargo.lock generated
View File

@ -2194,6 +2194,17 @@ dependencies = [
"crc",
]
[[package]]
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "measurements"
version = "0.11.0"
@ -4862,6 +4873,15 @@ version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547"
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
@ -5006,9 +5026,9 @@ dependencies = [
[[package]]
name = "zip"
version = "2.2.3"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b280484c454e74e5fff658bbf7df8fdbe7a07c6b2de4a53def232c15ef138f3a"
checksum = "938cc23ac49778ac8340e366ddc422b2227ea176edb447e23fc0627608dddadd"
dependencies = [
"aes",
"arbitrary",
@ -5019,15 +5039,16 @@ dependencies = [
"deflate64",
"displaydoc",
"flate2",
"getrandom 0.3.1",
"hmac",
"indexmap 2.8.0",
"lzma-rs",
"memchr",
"pbkdf2",
"rand 0.8.5",
"sha1",
"thiserror 2.0.12",
"time",
"xz2",
"zeroize",
"zopfli",
"zstd",

View File

@ -49,7 +49,7 @@ tokio = { version = "1" }
tower-lsp = { version = "0.20.0", default-features = false }
tracing-subscriber = { version = "0.3.19", features = ["registry", "std", "fmt", "smallvec", "ansi", "tracing-log", "json"] }
uuid = { version = "1", features = ["v4", "serde"] }
zip = { version = "2.2.2", default-features = false }
zip = { version = "2.4.1", default-features = false }
[workspace.lints.clippy]
assertions_on_result_states = "warn"

View File

@ -248,6 +248,7 @@ export class Ctx {
this.clientSubscriptions = []
try {
await this._client?.dispose(2000)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// DO nothing.
}

View File

@ -18,7 +18,7 @@ use tokio::sync::{mpsc, oneshot, RwLock};
use tokio_tungstenite::tungstenite::Message as WsMsg;
use uuid::Uuid;
use super::ExecutionKind;
use super::{EngineStats, ExecutionKind};
use crate::{
engine::EngineManager,
errors::{KclError, KclErrorDetails},
@ -52,6 +52,7 @@ pub struct EngineConnection {
session_data: Arc<RwLock<Option<ModelingSessionData>>>,
execution_kind: Arc<RwLock<ExecutionKind>>,
stats: EngineStats,
}
pub struct TcpRead {
@ -344,6 +345,7 @@ impl EngineConnection {
default_planes: Default::default(),
session_data,
execution_kind: Default::default(),
stats: Default::default(),
})
}
}
@ -378,6 +380,10 @@ impl EngineManager for EngineConnection {
original
}
fn stats(&self) -> &EngineStats {
&self.stats
}
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone()
}

View File

@ -16,7 +16,7 @@ use kittycad_modeling_cmds::{self as kcmc};
use tokio::sync::RwLock;
use uuid::Uuid;
use super::ExecutionKind;
use super::{EngineStats, ExecutionKind};
use crate::{
errors::KclError,
exec::DefaultPlanes,
@ -32,6 +32,7 @@ pub struct EngineConnection {
execution_kind: Arc<RwLock<ExecutionKind>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats,
}
impl EngineConnection {
@ -42,6 +43,7 @@ impl EngineConnection {
artifact_commands: Arc::new(RwLock::new(Vec::new())),
execution_kind: Default::default(),
default_planes: Default::default(),
stats: Default::default(),
})
}
}
@ -60,6 +62,10 @@ impl crate::engine::EngineManager for EngineConnection {
Arc::new(RwLock::new(IndexMap::new()))
}
fn stats(&self) -> &EngineStats {
&self.stats
}
fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
self.artifact_commands.clone()
}

View File

@ -11,7 +11,7 @@ use uuid::Uuid;
use wasm_bindgen::prelude::*;
use crate::{
engine::ExecutionKind,
engine::{EngineStats, ExecutionKind},
errors::{KclError, KclErrorDetails},
execution::{ArtifactCommand, DefaultPlanes, IdGenerator},
SourceRange,
@ -45,6 +45,7 @@ pub struct EngineConnection {
execution_kind: Arc<RwLock<ExecutionKind>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats,
}
// Safety: WebAssembly will only ever run in a single-threaded context.
@ -62,6 +63,7 @@ impl EngineConnection {
artifact_commands: Arc::new(RwLock::new(Vec::new())),
execution_kind: Default::default(),
default_planes: Default::default(),
stats: Default::default(),
})
}
@ -141,6 +143,10 @@ impl crate::engine::EngineManager for EngineConnection {
self.responses.clone()
}
fn stats(&self) -> &EngineStats {
&self.stats
}
fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
self.artifact_commands.clone()
}

View File

@ -8,7 +8,13 @@ pub mod conn_mock;
#[cfg(feature = "engine")]
pub mod conn_wasm;
use std::{collections::HashMap, sync::Arc};
use std::{
collections::HashMap,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use indexmap::IndexMap;
use kcmc::{
@ -58,6 +64,21 @@ impl ExecutionKind {
}
}
#[derive(Default, Debug)]
pub struct EngineStats {
pub commands_batched: AtomicUsize,
pub batches_sent: AtomicUsize,
}
impl Clone for EngineStats {
fn clone(&self) -> Self {
Self {
commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
}
}
}
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of commands to be sent to the engine.
@ -97,6 +118,8 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the default planes.
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
fn stats(&self) -> &EngineStats;
/// Get the default planes, creating them if they don't exist.
async fn default_planes(
&self,
@ -254,6 +277,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Add cmd to the batch.
self.batch().write().await.push((req, source_range));
self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
Ok(())
}
@ -277,6 +301,9 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
for cmd in cmds {
extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
}
self.stats()
.commands_batched
.fetch_add(extended_cmds.len(), Ordering::Relaxed);
self.batch().write().await.extend(extended_cmds);
Ok(())
@ -303,6 +330,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Add cmd to the batch end.
self.batch_end().write().await.insert(id, (req, source_range));
self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
Ok(())
}
@ -405,6 +433,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
if batch_end {
self.batch_end().write().await.clear();
}
self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
// We pop off the responses to cleanup our mappings.
match final_req {

View File

@ -832,6 +832,19 @@ pub enum Path {
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
ArcThreePoint {
#[serde(flatten)]
base: BasePath,
/// Point 1 of the arc (base on the end of previous segment)
#[ts(type = "[number, number]")]
p1: [f64; 2],
/// Point 2 of the arc (interior kwarg)
#[ts(type = "[number, number]")]
p2: [f64; 2],
/// Point 3 of the arc (end kwarg)
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
/// A path that is horizontal.
Horizontal {
#[serde(flatten)]
@ -892,6 +905,7 @@ impl From<&Path> for PathType {
Path::AngledLineTo { .. } => Self::AngledLineTo,
Path::Base { .. } => Self::Base,
Path::Arc { .. } => Self::Arc,
Path::ArcThreePoint { .. } => Self::Arc,
}
}
}
@ -908,6 +922,7 @@ impl Path {
Path::Circle { base, .. } => base.geo_meta.id,
Path::CircleThreePoint { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id,
Path::ArcThreePoint { base, .. } => base.geo_meta.id,
}
}
@ -922,6 +937,7 @@ impl Path {
Path::Circle { base, .. } => base.tag.clone(),
Path::CircleThreePoint { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(),
Path::ArcThreePoint { base, .. } => base.tag.clone(),
}
}
@ -936,6 +952,7 @@ impl Path {
Path::Circle { base, .. } => base,
Path::CircleThreePoint { base, .. } => base,
Path::Arc { base, .. } => base,
Path::ArcThreePoint { base, .. } => base,
}
}
@ -985,6 +1002,10 @@ impl Path {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
}
Self::ArcThreePoint { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
}
}
}
@ -999,6 +1020,7 @@ impl Path {
Path::Circle { base, .. } => Some(base),
Path::CircleThreePoint { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base),
Path::ArcThreePoint { base, .. } => Some(base),
}
}
@ -1010,6 +1032,17 @@ impl Path {
center: *center,
ccw: *ccw,
},
Path::ArcThreePoint { p1, p2, p3, .. } => {
let circle_center =
crate::std::utils::calculate_circle_from_3_points([(*p1).into(), (*p2).into(), (*p3).into()]);
let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], p1);
let center_point = [circle_center.center.x, circle_center.center.y];
GetTangentialInfoFromPathsResult::Circle {
center: center_point,
ccw: true,
radius,
}
}
Path::Circle {
center, ccw, radius, ..
} => GetTangentialInfoFromPathsResult::Circle {

View File

@ -430,21 +430,14 @@ impl ExecutorContext {
}
#[cfg(target_arch = "wasm32")]
pub async fn new_mock(
fs_manager: crate::fs::wasm::FileSystemManager,
settings: ExecutorSettings,
) -> Result<Self, String> {
Ok(ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new()
.await
.map_err(|e| format!("{:?}", e))?,
)),
fs: Arc::new(FileManager::new(fs_manager)),
pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
ExecutorContext {
engine,
fs,
stdlib: Arc::new(StdLib::new()),
settings,
context_type: ContextType::Mock,
})
}
}
#[cfg(not(target_arch = "wasm32"))]
@ -757,6 +750,7 @@ impl ExecutorContext {
"Post interpretation KCL memory stats: {:#?}",
exec_state.stack().memory.stats
));
crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
if !self.is_mock() {
let mut mem = exec_state.stack().deep_clone();

View File

@ -81,7 +81,7 @@ mod walk;
mod wasm;
pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind};
pub use engine::{EngineManager, EngineStats, ExecutionKind};
pub use errors::{
CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs, Report, ReportWithOutputs,
};
@ -96,6 +96,8 @@ pub use modules::ModuleId;
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use source_range::SourceRange;
#[cfg(not(target_arch = "wasm32"))]
pub use unparser::recast_dir;
// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
// Ideally we wouldn't export these things at all, they should only be used for testing.
@ -112,6 +114,10 @@ pub mod wasm_engine {
};
}
pub mod mock_engine {
pub use crate::engine::conn_mock::EngineConnection;
}
#[cfg(not(target_arch = "wasm32"))]
pub mod native_engine {
pub use crate::engine::conn::EngineConnection;

View File

@ -11,7 +11,13 @@ use kcl_lib::{ExecState, ExecutorContext, ExecutorSettings, Program};
async fn main() {
let mut args = env::args();
args.next();
let filename = args.next().unwrap_or_else(|| "main.kcl".to_owned());
let mut filename = args.next().unwrap_or_else(|| "main.kcl".to_owned());
if !filename.ends_with(".kcl") {
if !filename.ends_with('/') && !filename.ends_with('\\') {
filename += "/";
}
filename += "main.kcl";
}
let mut f = File::open(&filename).unwrap();
let mut text = String::new();

File diff suppressed because it is too large Load Diff

View File

@ -34,10 +34,10 @@ fn parse(dir_name: &str, dir_path: &Path) {
}
#[kcl_directory_test_macro::test_all_dirs("../public/kcl-samples")]
fn unparse(dir_name: &str, dir_path: &Path) {
// kcl-samples don't always use correct formatting. We don't ignore the
// test because we want to allow the just command to work. It's actually
// fine when no test runs.
async fn unparse(dir_name: &str, dir_path: &Path) {
// TODO: turn this on when we fix the comments recasting.
// let t = test(dir_name, dir_path.join("main.kcl").to_str().unwrap().to_owned());
// super::unparse_test(&t).await;
}
#[kcl_directory_test_macro::test_all_dirs("../public/kcl-samples")]

View File

@ -265,6 +265,17 @@ pub(crate) async fn do_post_extrude(
});
Some(extrude_surface)
}
Path::ArcThreePoint { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
}
}
} else if no_engine_commands {
// Only pre-populate the extrude surface if we are in mock mode.

View File

@ -23,8 +23,8 @@ use crate::{
std::{
args::{Args, TyF64},
utils::{
arc_angles, arc_center_and_end, calculate_circle_center, get_tangential_arc_to_info, get_x_component,
get_y_component, intersection_with_parallel_line, TangentialArcInfoInput,
arc_angles, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
intersection_with_parallel_line, TangentialArcInfoInput,
},
},
};
@ -1682,18 +1682,7 @@ pub(crate) async fn inner_arc_to(
let interior = data.interior;
let end = data.end;
// compute the center of the circle since we do not have the value returned from the engine
let center = calculate_circle_center(start, interior, end);
// compute the radius since we do not have the value returned from the engine
// Pick any of the 3 points since they all lie along the circle
let sum_of_square_differences =
(center[0] - start[0] * center[0] - start[0]) + (center[1] - start[1] * center[1] - start[1]);
let radius = sum_of_square_differences.sqrt();
let ccw = is_ccw(start, interior, end);
let current_path = Path::Arc {
let current_path = Path::ArcThreePoint {
base: BasePath {
from: from.into(),
to: data.end,
@ -1704,9 +1693,9 @@ pub(crate) async fn inner_arc_to(
metadata: args.source_range.into(),
},
},
center,
radius,
ccw,
p1: start,
p2: interior,
p3: end,
};
let mut new_sketch = sketch.clone();
@ -1719,26 +1708,6 @@ pub(crate) async fn inner_arc_to(
Ok(new_sketch)
}
/// Returns true if the three-point arc is counterclockwise. The order of
/// parameters is critical.
///
/// | end
/// | /
/// | | / interior
/// | / /
/// | | /
/// |/_____________
/// start
///
/// If the slope of the line from start to interior is less than the slope of
/// the line from start to end, the arc is counterclockwise.
fn is_ccw(start: [f64; 2], interior: [f64; 2], end: [f64; 2]) -> bool {
let t1 = (interior[0] - start[0]) * (end[1] - start[1]);
let t2 = (end[0] - start[0]) * (interior[1] - start[1]);
// If these terms are equal, the points are collinear.
t1 > t2
}
/// Data to draw a tangential arc.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)]

View File

@ -842,6 +842,103 @@ impl Type {
}
}
/// Collect all the kcl files in a directory, recursively.
#[cfg(not(target_arch = "wasm32"))]
#[async_recursion::async_recursion]
pub(crate) async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
// Make sure we actually have a directory.
if !dir.is_dir() {
anyhow::bail!("`{}` is not a directory", dir.display());
}
let mut entries = tokio::fs::read_dir(dir).await?;
let mut files = Vec::new();
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_dir() {
files.extend(walk_dir(&path).await?);
} else if path.extension().is_some_and(|ext| ext == "kcl") {
files.push(path);
}
}
Ok(files)
}
/// Recast all the kcl files in a directory, recursively.
#[cfg(not(target_arch = "wasm32"))]
pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), crate::KclError> {
let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
source_ranges: vec![crate::SourceRange::default()],
})
})?;
let futures = files
.into_iter()
.map(|file| {
let options = options.clone();
tokio::spawn(async move {
let contents = tokio::fs::read_to_string(&file).await.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to read file `{}`: {:?}", file.display(), err),
source_ranges: vec![crate::SourceRange::default()],
})
})?;
let (program, ces) = crate::Program::parse(&contents)?;
for ce in &ces {
if ce.severity != crate::errors::Severity::Warning {
return Err(crate::KclError::Semantic(ce.clone().into()));
}
}
let Some(program) = program else {
return Err(crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to parse file `{}`: {:?}", file.display(), ces),
source_ranges: vec![crate::SourceRange::default()],
}));
};
let recast = program.recast_with_options(&options);
tokio::fs::write(&file, recast).await.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to write file `{}`: {:?}", file.display(), err),
source_ranges: vec![crate::SourceRange::default()],
})
})?;
Ok::<(), crate::KclError>(())
})
})
.collect::<Vec<_>>();
// Join all futures and await their completion
let results = futures::future::join_all(futures).await;
// Check if any of the futures failed.
let mut errors = Vec::new();
for result in results {
if let Err(err) = result.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to recast file: {:?}", err),
source_ranges: vec![crate::SourceRange::default()],
})
})? {
errors.push(err);
}
}
if !errors.is_empty() {
return Err(crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to recast files: {:?}", errors),
source_ranges: vec![crate::SourceRange::default()],
}));
}
Ok(())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

View File

@ -80,8 +80,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
47,
66,
48,
67,
3
],
"command": {
@ -109,8 +109,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
76,
113,
77,
114,
3
],
"command": {
@ -129,8 +129,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
76,
113,
77,
114,
3
],
"command": {
@ -140,8 +140,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
76,
113,
77,
114,
3
],
"command": {
@ -157,8 +157,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
76,
113,
77,
114,
3
],
"command": {
@ -168,8 +168,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
119,
136,
120,
137,
3
],
"command": {
@ -189,8 +189,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
142,
160,
143,
161,
3
],
"command": {
@ -210,8 +210,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
166,
184,
167,
185,
3
],
"command": {
@ -231,8 +231,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
190,
246,
191,
247,
3
],
"command": {
@ -252,8 +252,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
252,
259,
253,
260,
3
],
"command": {
@ -264,8 +264,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -284,8 +284,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -298,8 +298,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -309,8 +309,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -321,8 +321,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -334,8 +334,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -348,8 +348,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -362,8 +362,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -376,8 +376,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -390,8 +390,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -404,8 +404,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -418,8 +418,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -432,8 +432,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
265,
287,
266,
288,
3
],
"command": {
@ -458,8 +458,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
47,
66,
48,
67,
4
],
"command": {
@ -487,8 +487,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
76,
111,
77,
112,
4
],
"command": {
@ -507,8 +507,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
76,
111,
77,
112,
4
],
"command": {
@ -518,8 +518,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
76,
111,
77,
112,
4
],
"command": {
@ -535,8 +535,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
76,
111,
77,
112,
4
],
"command": {
@ -546,8 +546,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
117,
134,
118,
135,
4
],
"command": {
@ -567,8 +567,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
140,
158,
141,
159,
4
],
"command": {
@ -588,8 +588,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
164,
182,
165,
183,
4
],
"command": {
@ -609,8 +609,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
188,
244,
189,
245,
4
],
"command": {
@ -630,8 +630,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
250,
257,
251,
258,
4
],
"command": {
@ -642,8 +642,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -662,8 +662,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -676,8 +676,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -687,8 +687,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -699,8 +699,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -712,8 +712,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -726,8 +726,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -740,8 +740,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -754,8 +754,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -768,8 +768,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -782,8 +782,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -796,8 +796,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {
@ -810,8 +810,8 @@ description: Artifact commands assembly_mixed_units_cubes.kcl
{
"cmdId": "[uuid]",
"range": [
263,
285,
264,
286,
4
],
"command": {

View File

@ -1,25 +1,25 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[76, 113, 3]"]
3["Segment<br>[119, 136, 3]"]
4["Segment<br>[142, 160, 3]"]
5["Segment<br>[166, 184, 3]"]
6["Segment<br>[190, 246, 3]"]
7["Segment<br>[252, 259, 3]"]
2["Path<br>[77, 114, 3]"]
3["Segment<br>[120, 137, 3]"]
4["Segment<br>[143, 161, 3]"]
5["Segment<br>[167, 185, 3]"]
6["Segment<br>[191, 247, 3]"]
7["Segment<br>[253, 260, 3]"]
8[Solid2d]
end
subgraph path25 [Path]
25["Path<br>[76, 111, 4]"]
26["Segment<br>[117, 134, 4]"]
27["Segment<br>[140, 158, 4]"]
28["Segment<br>[164, 182, 4]"]
29["Segment<br>[188, 244, 4]"]
30["Segment<br>[250, 257, 4]"]
25["Path<br>[77, 112, 4]"]
26["Segment<br>[118, 135, 4]"]
27["Segment<br>[141, 159, 4]"]
28["Segment<br>[165, 183, 4]"]
29["Segment<br>[189, 245, 4]"]
30["Segment<br>[251, 258, 4]"]
31[Solid2d]
end
1["Plane<br>[47, 66, 3]"]
9["Sweep Extrusion<br>[265, 287, 3]"]
1["Plane<br>[48, 67, 3]"]
9["Sweep Extrusion<br>[266, 288, 3]"]
10[Wall]
11[Wall]
12[Wall]
@ -34,8 +34,8 @@ flowchart LR
21["SweepEdge Adjacent"]
22["SweepEdge Opposite"]
23["SweepEdge Adjacent"]
24["Plane<br>[47, 66, 4]"]
32["Sweep Extrusion<br>[263, 285, 4]"]
24["Plane<br>[48, 67, 4]"]
32["Sweep Extrusion<br>[264, 286, 4]"]
33[Wall]
34[Wall]
35[Wall]

View File

@ -1,5 +1,6 @@
@settings(defaultLengthUnit = in)
sketch001 = startSketchOn('XY')
cubeIn = startProfileAt([-10, -10], sketch001)
|> xLine(length = 5)

View File

@ -1,5 +1,6 @@
@settings(defaultLengthUnit = mm)
sketch001 = startSketchOn('XY')
cubeMm = startProfileAt([10, 10], sketch001)
|> xLine(length = 5)

View File

@ -11,16 +11,16 @@ description: Operations executed assembly_mixed_units_cubes.kcl
"value": "XY"
},
"sourceRange": [
61,
65,
62,
66,
3
]
}
},
"name": "startSketchOn",
"sourceRange": [
47,
66,
48,
67,
3
],
"type": "StdLibCall",
@ -43,16 +43,16 @@ description: Operations executed assembly_mixed_units_cubes.kcl
}
},
"sourceRange": [
285,
286,
287,
3
]
}
},
"name": "extrude",
"sourceRange": [
265,
287,
266,
288,
3
],
"type": "StdLibCall",
@ -64,8 +64,8 @@ description: Operations executed assembly_mixed_units_cubes.kcl
}
},
"sourceRange": [
273,
274,
275,
3
]
}
@ -78,16 +78,16 @@ description: Operations executed assembly_mixed_units_cubes.kcl
"value": "XY"
},
"sourceRange": [
61,
65,
62,
66,
4
]
}
},
"name": "startSketchOn",
"sourceRange": [
47,
66,
48,
67,
4
],
"type": "StdLibCall",
@ -110,16 +110,16 @@ description: Operations executed assembly_mixed_units_cubes.kcl
}
},
"sourceRange": [
283,
284,
285,
4
]
}
},
"name": "extrude",
"sourceRange": [
263,
285,
264,
286,
4
],
"type": "StdLibCall",
@ -131,8 +131,8 @@ description: Operations executed assembly_mixed_units_cubes.kcl
}
},
"sourceRange": [
271,
272,
273,
4
]
}

View File

@ -80,8 +80,8 @@ description: Artifact commands assembly_non_default_units.kcl
{
"cmdId": "[uuid]",
"range": [
172,
191,
173,
192,
3
],
"command": {
@ -109,8 +109,8 @@ description: Artifact commands assembly_non_default_units.kcl
{
"cmdId": "[uuid]",
"range": [
197,
232,
198,
233,
3
],
"command": {
@ -129,8 +129,8 @@ description: Artifact commands assembly_non_default_units.kcl
{
"cmdId": "[uuid]",
"range": [
197,
232,
198,
233,
3
],
"command": {
@ -140,8 +140,8 @@ description: Artifact commands assembly_non_default_units.kcl
{
"cmdId": "[uuid]",
"range": [
197,
232,
198,
233,
3
],
"command": {
@ -157,8 +157,8 @@ description: Artifact commands assembly_non_default_units.kcl
{
"cmdId": "[uuid]",
"range": [
197,
232,
198,
233,
3
],
"command": {
@ -168,8 +168,8 @@ description: Artifact commands assembly_non_default_units.kcl
{
"cmdId": "[uuid]",
"range": [
197,
232,
198,
233,
3
],
"command": {
@ -197,8 +197,8 @@ description: Artifact commands assembly_non_default_units.kcl
{
"cmdId": "[uuid]",
"range": [
197,
232,
198,
233,
3
],
"command": {

View File

@ -1,8 +1,8 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[197, 232, 3]"]
3["Segment<br>[197, 232, 3]"]
2["Path<br>[198, 233, 3]"]
3["Segment<br>[198, 233, 3]"]
4[Solid2d]
end
subgraph path6 [Path]
@ -10,7 +10,7 @@ flowchart LR
7["Segment<br>[114, 149, 4]"]
8[Solid2d]
end
1["Plane<br>[172, 191, 3]"]
1["Plane<br>[173, 192, 3]"]
5["Plane<br>[89, 108, 4]"]
1 --- 2
2 --- 3

View File

@ -1,3 +1,4 @@
@settings(defaultLengthUnit = in)
export radius = 1

View File

@ -11,16 +11,16 @@ description: Operations executed assembly_non_default_units.kcl
"value": "XZ"
},
"sourceRange": [
186,
190,
187,
191,
3
]
}
},
"name": "startSketchOn",
"sourceRange": [
172,
191,
173,
192,
3
],
"type": "StdLibCall",

View File

@ -1,5 +1,6 @@
@settings(defaultLengthUnit = in)
// This is not used, but it triggers the problem.
import radius from "globals.kcl"

View File

@ -1,4 +1,6 @@
@settings(defaultLengthUnit = mm)
import three from "import_cycle3.kcl"
export fn two = () => { return three() - 1 }
export fn two() {
return three() - 1
}

View File

@ -1,4 +1,6 @@
@settings(defaultLengthUnit = in)
import one from "input.kcl"
export fn three = () => { return one() + one() + one() }
export fn three() {
return one() + one() + one()
}

View File

@ -1,5 +1,6 @@
@settings(defaultLengthUnit = mm)
export part001 = startSketchOn('XY')
|> startProfileAt([4, 12], %)
|> line(end = [2, 0])
@ -12,4 +13,6 @@ export part001 = startSketchOn('XY')
|> close()
|> revolve({ axis = 'y' }, %) // default angle is 360
export fn two = () => { return 5 }
export fn two() {
return 5
}

View File

@ -11,16 +11,16 @@ description: Operations executed import_function_not_sketch.kcl
"value": "XY"
},
"sourceRange": [
66,
70,
67,
71,
3
]
}
},
"name": "startSketchOn",
"sourceRange": [
52,
71,
53,
72,
3
],
"type": "StdLibCall",
@ -39,8 +39,8 @@ description: Operations executed import_function_not_sketch.kcl
}
},
"sourceRange": [
312,
326,
313,
327,
3
]
},
@ -52,16 +52,16 @@ description: Operations executed import_function_not_sketch.kcl
}
},
"sourceRange": [
328,
329,
330,
3
]
}
},
"name": "revolve",
"sourceRange": [
304,
330,
305,
331,
3
],
"type": "StdLibCall",

View File

@ -1,4 +1,6 @@
export fn foo = () => { return 0 }
export fn foo() {
return 0
}
// This interacts with the engine.
part001 = startSketchOn('XY')

View File

@ -1,5 +1,5 @@
---
source: kcl/src/simulation_tests.rs
source: kcl-lib/src/simulation_tests.rs
description: Operations executed import_side_effect.kcl
---
[
@ -11,16 +11,16 @@ description: Operations executed import_side_effect.kcl
"value": "XY"
},
"sourceRange": [
91,
95,
99,
3
]
}
},
"name": "startSketchOn",
"sourceRange": [
81,
100,
77,
96,
3
],
"type": "StdLibCall",

View File

@ -8,9 +8,9 @@ mod TEST_NAME_HERE {
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.

View File

@ -490,7 +490,7 @@ async fn execute_code_and_export(code: String, export_format: FileExportFormat)
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Format the kcl code.
/// Format the kcl code. This will return the formatted code.
#[pyfunction]
fn format(code: String) -> PyResult<String> {
let program = kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
@ -499,6 +499,19 @@ fn format(code: String) -> PyResult<String> {
Ok(recasted)
}
/// Format a whole directory of kcl code.
#[pyfunction]
async fn format_dir(dir: String) -> PyResult<()> {
tokio()
.spawn(async move {
kcl_lib::recast_dir(std::path::Path::new(&dir), &Default::default())
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Lint the kcl code.
#[pyfunction]
fn lint(code: String) -> PyResult<Vec<Discovered>> {
@ -528,6 +541,7 @@ fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(execute_and_export, m)?)?;
m.add_function(wrap_pyfunction!(execute_code_and_export, m)?)?;
m.add_function(wrap_pyfunction!(format, m)?)?;
m.add_function(wrap_pyfunction!(format_dir, m)?)?;
m.add_function(wrap_pyfunction!(lint, m)?)?;
Ok(())
}

View File

@ -129,6 +129,9 @@ def test_kcl_format():
assert formatted_code is not None
assert len(formatted_code) > 0
@pytest.mark.asyncio
async def test_kcl_format_dir():
await kcl.format_dir(walkie_talkie_dir)
def test_kcl_lint():
# Read from a file.

View File

@ -4,7 +4,7 @@ use anyhow::Result;
use indexmap::IndexMap;
use kcl_lib::{
exec::{ArtifactCommand, DefaultPlanes, IdGenerator},
ExecutionKind, KclError,
EngineStats, ExecutionKind, KclError,
};
use kittycad_modeling_cmds::{
self as kcmc,
@ -26,6 +26,7 @@ pub struct EngineConnection {
execution_kind: Arc<RwLock<ExecutionKind>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats,
}
impl EngineConnection {
@ -38,6 +39,7 @@ impl EngineConnection {
core_test: result,
execution_kind: Default::default(),
default_planes: Default::default(),
stats: Default::default(),
})
}
@ -369,6 +371,10 @@ impl kcl_lib::EngineManager for EngineConnection {
Arc::new(RwLock::new(IndexMap::new()))
}
fn stats(&self) -> &EngineStats {
&self.stats
}
fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
Arc::new(RwLock::new(Vec::new()))
}

View File

@ -10,6 +10,7 @@ use wasm_bindgen::prelude::*;
pub struct Context {
engine: Arc<Box<dyn EngineManager>>,
fs: Arc<FileManager>,
mock_engine: Arc<Box<dyn EngineManager>>,
}
#[wasm_bindgen]
@ -28,16 +29,34 @@ impl Context {
.map_err(|e| format!("{:?}", e))?,
)),
fs: Arc::new(FileManager::new(fs_manager)),
mock_engine: Arc::new(Box::new(
kcl_lib::mock_engine::EngineConnection::new()
.await
.map_err(|e| format!("{:?}", e))?,
)),
})
}
fn create_executor_ctx(&self, settings: &str, path: Option<String>) -> Result<kcl_lib::ExecutorContext, String> {
fn create_executor_ctx(
&self,
settings: &str,
path: Option<String>,
is_mock: bool,
) -> Result<kcl_lib::ExecutorContext, String> {
let config: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?;
let mut settings: kcl_lib::ExecutorSettings = config.into();
if let Some(path) = path {
settings.with_current_file(std::path::PathBuf::from(path));
}
if is_mock {
return Ok(kcl_lib::ExecutorContext::new_mock(
self.mock_engine.clone(),
self.fs.clone(),
settings.into(),
));
}
Ok(kcl_lib::ExecutorContext::new(
self.engine.clone(),
self.fs.clone(),
@ -57,7 +76,7 @@ impl Context {
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?;
let ctx = self.create_executor_ctx(settings, path)?;
let ctx = self.create_executor_ctx(settings, path, false)?;
match ctx.run_with_caching(program).await {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
@ -65,4 +84,26 @@ impl Context {
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
}
}
/// Execute a program in mock mode.
#[wasm_bindgen(js_name = executeMock)]
pub async fn execute_mock(
&self,
program_ast_json: &str,
path: Option<String>,
settings: &str,
use_prev_memory: bool,
) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?;
let ctx = self.create_executor_ctx(settings, path, true)?;
match ctx.run_mock(program, use_prev_memory).await {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()),
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
}
}
}

View File

@ -3,9 +3,13 @@
#[cfg(target_arch = "wasm32")]
mod context;
#[cfg(target_arch = "wasm32")]
mod lsp;
#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg(target_arch = "wasm32")]
pub use context::*;
#[cfg(target_arch = "wasm32")]
pub use lsp::*;
#[cfg(target_arch = "wasm32")]
pub use wasm::*;

View File

@ -0,0 +1,147 @@
//! Wasm interface for our LSP servers.
use futures::stream::TryStreamExt;
use tower_lsp::{LspService, Server};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct LspServerConfig {
into_server: js_sys::AsyncIterator,
from_server: web_sys::WritableStream,
fs: kcl_lib::wasm_engine::FileSystemManager,
}
#[wasm_bindgen]
impl LspServerConfig {
#[wasm_bindgen(constructor)]
pub fn new(
into_server: js_sys::AsyncIterator,
from_server: web_sys::WritableStream,
fs: kcl_lib::wasm_engine::FileSystemManager,
) -> Self {
Self {
into_server,
from_server,
fs,
}
}
}
/// Run the `kcl` lsp server.
//
// NOTE: we don't use web_sys::ReadableStream for input here because on the
// browser side we need to use a ReadableByteStreamController to construct it
// and so far only Chromium-based browsers support that functionality.
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen]
pub async fn lsp_run_kcl(config: LspServerConfig, token: String, baseurl: String) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let LspServerConfig {
into_server,
from_server,
fs,
} = config;
let executor_ctx = None;
let mut zoo_client = kittycad::Client::new(token);
zoo_client.set_base_url(baseurl.as_str());
// Check if we can send telemetry for this user.
let can_send_telemetry = match zoo_client.users().get_privacy_settings().await {
Ok(privacy_settings) => privacy_settings.can_train_on_data,
Err(err) => {
// In the case of dev we don't always have a sub set, but prod we should.
if err
.to_string()
.contains("The modeling app subscription type is missing.")
{
true
} else {
web_sys::console::warn_1(&format!("Failed to get privacy settings: {err:?}").into());
false
}
}
};
let (service, socket) = LspService::build(|client| {
kcl_lib::KclLspBackend::new_wasm(client, executor_ctx, fs, zoo_client, can_send_telemetry).unwrap()
})
.custom_method("kcl/updateUnits", kcl_lib::KclLspBackend::update_units)
.custom_method("kcl/updateCanExecute", kcl_lib::KclLspBackend::update_can_execute)
.finish();
let input = wasm_bindgen_futures::stream::JsStream::from(into_server);
let input = input
.map_ok(|value| {
value
.dyn_into::<js_sys::Uint8Array>()
.expect("could not cast stream item to Uint8Array")
.to_vec()
})
.map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
.into_async_read();
let output = wasm_bindgen::JsCast::unchecked_into::<wasm_streams::writable::sys::WritableStream>(from_server);
let output = wasm_streams::WritableStream::from_raw(output);
let output = output.try_into_async_write().map_err(|err| err.0)?;
Server::new(input, output, socket).serve(service).await;
Ok(())
}
/// Run the `copilot` lsp server.
//
// NOTE: we don't use web_sys::ReadableStream for input here because on the
// browser side we need to use a ReadableByteStreamController to construct it
// and so far only Chromium-based browsers support that functionality.
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen]
pub async fn lsp_run_copilot(config: LspServerConfig, token: String, baseurl: String) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let LspServerConfig {
into_server,
from_server,
fs,
} = config;
let mut zoo_client = kittycad::Client::new(token);
zoo_client.set_base_url(baseurl.as_str());
let dev_mode = baseurl == "https://api.dev.zoo.dev";
let (service, socket) =
LspService::build(|client| kcl_lib::CopilotLspBackend::new_wasm(client, fs, zoo_client, dev_mode))
.custom_method("copilot/setEditorInfo", kcl_lib::CopilotLspBackend::set_editor_info)
.custom_method(
"copilot/getCompletions",
kcl_lib::CopilotLspBackend::get_completions_cycling,
)
.custom_method("copilot/notifyAccepted", kcl_lib::CopilotLspBackend::accept_completion)
.custom_method("copilot/notifyRejected", kcl_lib::CopilotLspBackend::reject_completions)
.finish();
let input = wasm_bindgen_futures::stream::JsStream::from(into_server);
let input = input
.map_ok(|value| {
value
.dyn_into::<js_sys::Uint8Array>()
.expect("could not cast stream item to Uint8Array")
.to_vec()
})
.map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
.into_async_read();
let output = wasm_bindgen::JsCast::unchecked_into::<wasm_streams::writable::sys::WritableStream>(from_server);
let output = wasm_streams::WritableStream::from_raw(output);
let output = output.try_into_async_write().map_err(|err| err.0)?;
Server::new(input, output, socket).serve(service).await;
Ok(())
}

View File

@ -1,38 +1,9 @@
//! Wasm bindings for `kcl`.
use futures::stream::TryStreamExt;
use gloo_utils::format::JsValueSerdeExt;
use kcl_lib::{pretty::NumericSuffix, CoreDump, Point2d, Program};
use tower_lsp::{LspService, Server};
use wasm_bindgen::prelude::*;
// wasm_bindgen wrapper for mock execute
#[wasm_bindgen]
pub async fn execute_mock(
program_ast_json: &str,
path: Option<String>,
settings: &str,
use_prev_memory: bool,
fs_manager: kcl_lib::wasm_engine::FileSystemManager,
) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?;
let config: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?;
let mut settings: kcl_lib::ExecutorSettings = config.into();
if let Some(path) = path {
settings.with_current_file(std::path::PathBuf::from(path));
}
let ctx = kcl_lib::ExecutorContext::new_mock(fs_manager, settings.into()).await?;
match ctx.run_mock(program, use_prev_memory).await {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()),
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
}
}
// wasm_bindgen wrapper for execute
#[wasm_bindgen]
pub async fn kcl_lint(program_ast_json: &str) -> Result<JsValue, JsValue> {
@ -94,148 +65,6 @@ pub fn format_number(value: f64, suffix_json: &str) -> Result<String, JsError> {
Ok(kcl_lib::pretty::format_number(value, suffix))
}
#[wasm_bindgen]
pub struct ServerConfig {
into_server: js_sys::AsyncIterator,
from_server: web_sys::WritableStream,
fs: kcl_lib::wasm_engine::FileSystemManager,
}
#[wasm_bindgen]
impl ServerConfig {
#[wasm_bindgen(constructor)]
pub fn new(
into_server: js_sys::AsyncIterator,
from_server: web_sys::WritableStream,
fs: kcl_lib::wasm_engine::FileSystemManager,
) -> Self {
Self {
into_server,
from_server,
fs,
}
}
}
/// Run the `kcl` lsp server.
//
// NOTE: we don't use web_sys::ReadableStream for input here because on the
// browser side we need to use a ReadableByteStreamController to construct it
// and so far only Chromium-based browsers support that functionality.
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen]
pub async fn kcl_lsp_run(config: ServerConfig, token: String, baseurl: String) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let ServerConfig {
into_server,
from_server,
fs,
} = config;
let executor_ctx = None;
let mut zoo_client = kittycad::Client::new(token);
zoo_client.set_base_url(baseurl.as_str());
// Check if we can send telemetry for this user.
let can_send_telemetry = match zoo_client.users().get_privacy_settings().await {
Ok(privacy_settings) => privacy_settings.can_train_on_data,
Err(err) => {
// In the case of dev we don't always have a sub set, but prod we should.
if err
.to_string()
.contains("The modeling app subscription type is missing.")
{
true
} else {
web_sys::console::warn_1(&format!("Failed to get privacy settings: {err:?}").into());
false
}
}
};
let (service, socket) = LspService::build(|client| {
kcl_lib::KclLspBackend::new_wasm(client, executor_ctx, fs, zoo_client, can_send_telemetry).unwrap()
})
.custom_method("kcl/updateUnits", kcl_lib::KclLspBackend::update_units)
.custom_method("kcl/updateCanExecute", kcl_lib::KclLspBackend::update_can_execute)
.finish();
let input = wasm_bindgen_futures::stream::JsStream::from(into_server);
let input = input
.map_ok(|value| {
value
.dyn_into::<js_sys::Uint8Array>()
.expect("could not cast stream item to Uint8Array")
.to_vec()
})
.map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
.into_async_read();
let output = wasm_bindgen::JsCast::unchecked_into::<wasm_streams::writable::sys::WritableStream>(from_server);
let output = wasm_streams::WritableStream::from_raw(output);
let output = output.try_into_async_write().map_err(|err| err.0)?;
Server::new(input, output, socket).serve(service).await;
Ok(())
}
/// Run the `copilot` lsp server.
//
// NOTE: we don't use web_sys::ReadableStream for input here because on the
// browser side we need to use a ReadableByteStreamController to construct it
// and so far only Chromium-based browsers support that functionality.
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen]
pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: String) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let ServerConfig {
into_server,
from_server,
fs,
} = config;
let mut zoo_client = kittycad::Client::new(token);
zoo_client.set_base_url(baseurl.as_str());
let dev_mode = baseurl == "https://api.dev.zoo.dev";
let (service, socket) =
LspService::build(|client| kcl_lib::CopilotLspBackend::new_wasm(client, fs, zoo_client, dev_mode))
.custom_method("copilot/setEditorInfo", kcl_lib::CopilotLspBackend::set_editor_info)
.custom_method(
"copilot/getCompletions",
kcl_lib::CopilotLspBackend::get_completions_cycling,
)
.custom_method("copilot/notifyAccepted", kcl_lib::CopilotLspBackend::accept_completion)
.custom_method("copilot/notifyRejected", kcl_lib::CopilotLspBackend::reject_completions)
.finish();
let input = wasm_bindgen_futures::stream::JsStream::from(into_server);
let input = input
.map_ok(|value| {
value
.dyn_into::<js_sys::Uint8Array>()
.expect("could not cast stream item to Uint8Array")
.to_vec()
})
.map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
.into_async_read();
let output = wasm_bindgen::JsCast::unchecked_into::<wasm_streams::writable::sys::WritableStream>(from_server);
let output = wasm_streams::WritableStream::from_raw(output);
let output = output.try_into_async_write().map_err(|err| err.0)?;
Server::new(input, output, socket).serve(service).await;
Ok(())
}
#[wasm_bindgen]
pub fn is_points_ccw(points: &[f64]) -> i32 {
console_error_panic_hook::set_once();

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import { AppHeader } from './components/AppHeader'
@ -60,7 +60,6 @@ export function App() {
const projectPath = project?.path || null
const [commands] = useEngineCommands()
const [capturedCanvas, setCapturedCanvas] = useState(false)
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const lastCommandType = commands[commands.length - 1]?.type
@ -109,11 +108,7 @@ export function App() {
// Generate thumbnail.png when loading the app
useEffect(() => {
if (
isDesktop() &&
!capturedCanvas &&
lastCommandType === 'execution-done'
) {
if (isDesktop() && lastCommandType === 'execution-done') {
setTimeout(() => {
const projectDirectoryWithoutEndingSlash = loaderData?.project?.path
if (!projectDirectoryWithoutEndingSlash) {

View File

@ -168,7 +168,7 @@ export function Toolbar({
return (
<menu
data-currentMode={currentMode}
data-current-mode={currentMode}
className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-30 dark:border-chalkboard-80 border-t-0 shadow-sm"
>
<ul

View File

@ -39,7 +39,7 @@ import { getConstraintInfo, getConstraintInfoKw } from 'lang/std/sketch'
import { Dialog, Popover, Transition } from '@headlessui/react'
import toast from 'react-hot-toast'
import { InstanceProps, create } from 'react-modal-promise'
import { executeAst } from 'lang/langHelpers'
import { executeAstMock } from 'lang/langHelpers'
import {
deleteSegmentFromPipeExpression,
removeSingleConstraintInfo,
@ -147,7 +147,8 @@ export const ClientSideScene = ({
state.matches({ Sketch: 'Tangential arc to' }) ||
state.matches({ Sketch: 'Rectangle tool' }) ||
state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'Circle three point tool' })
state.matches({ Sketch: 'Circle three point tool' }) ||
state.matches({ Sketch: 'Arc three point tool' })
) {
cursor = 'crosshair'
} else {
@ -436,12 +437,10 @@ export async function deleteSegment({
if (err(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
modifiedAst = pResult.program
const testExecute = await executeAst({
const testExecute = await executeAstMock({
ast: modifiedAst,
engineCommandManager: engineCommandManager,
isMock: true,
rustContext,
usePrevMemory: false,
rustContext: rustContext,
})
if (testExecute.errors.length) {
toast.error('Segment tag used outside of current Sketch. Could not delete.')
@ -490,14 +489,19 @@ const SegmentMenu = ({
verticalPosition === 'top' ? 'bottom-full' : 'top-full'
} z-10 w-36 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50`}
>
<button
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
onClick={() => {
send({ type: 'Constrain remove constraints', data: pathToNode })
}}
>
Remove constraints
</button>
{stdLibFnName !== 'arcTo' && (
<button
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
onClick={() => {
send({
type: 'Constrain remove constraints',
data: pathToNode,
})
}}
>
Remove constraints
</button>
)}
<button
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
title={

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,19 @@ import {
TANGENTIAL_ARC_TO_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT_BODY,
TANGENTIAL_ARC_TO__SEGMENT_DASH,
ARC_SEGMENT,
ARC_SEGMENT_BODY,
ARC_SEGMENT_DASH,
ARC_ANGLE_END,
getParentGroup,
ARC_CENTER_TO_FROM,
ARC_CENTER_TO_TO,
ARC_ANGLE_REFERENCE_LINE,
THREE_POINT_ARC_SEGMENT,
THREE_POINT_ARC_SEGMENT_BODY,
THREE_POINT_ARC_SEGMENT_DASH,
THREE_POINT_ARC_HANDLE2,
THREE_POINT_ARC_HANDLE3,
} from './sceneEntities'
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import {
@ -61,7 +73,7 @@ import {
SEGMENT_LENGTH_LABEL_TEXT,
} from './sceneInfra'
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
import { normaliseAngle, roundOff } from 'lib/utils'
import { isClockwise, normaliseAngle, roundOff } from 'lib/utils'
import {
SegmentOverlay,
SegmentOverlayPayload,
@ -74,6 +86,7 @@ import { Selections } from 'lib/selections'
import { calculate_circle_from_3_points } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
import { commandBarActor } from 'machines/commandBarMachine'
const ANGLE_INDICATOR_RADIUS = 30 // in px
interface CreateSegmentArgs {
input: SegmentInputs
prevSegment: Sketch['paths'][number]
@ -412,14 +425,28 @@ class TangentialArcToSegment implements SegmentUtils {
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
const previousPoint =
prevSegment?.type === 'TangentialArcTo'
? getTangentPointFromPreviousArc(
prevSegment.center,
prevSegment.ccw,
prevSegment.to
)
: prevSegment.from
let previousPoint = prevSegment.from
if (prevSegment?.type === 'TangentialArcTo') {
previousPoint = getTangentPointFromPreviousArc(
prevSegment.center,
prevSegment.ccw,
prevSegment.to
)
} else if (prevSegment?.type === 'ArcThreePoint') {
const arcDetails = calculate_circle_from_3_points(
prevSegment.p1[0],
prevSegment.p1[1],
prevSegment.p2[0],
prevSegment.p2[1],
prevSegment.p3[0],
prevSegment.p3[1]
)
previousPoint = getTangentPointFromPreviousArc(
[arcDetails.center_x, arcDetails.center_y],
!isClockwise([prevSegment.p1, prevSegment.p2, prevSegment.p3]),
prevSegment.p3
)
}
const arcInfo = getTangentialArcToInfo({
arcStartPoint: from,
@ -591,7 +618,6 @@ class CircleSegment implements SegmentUtils {
}
const { from, center, radius } = input
group.userData.from = from
// group.userData.to = to
group.userData.center = center
group.userData.radius = radius
group.userData.prevSegment = prevSegment
@ -635,8 +661,7 @@ class CircleSegment implements SegmentUtils {
}
if (radiusLengthIndicator) {
// The radius indicator is placed at the midpoint of the radius,
// at a 45 degree CCW angle from the positive X-axis
// The radius indicator is placed halfway between the center and the start angle point
const indicatorPoint = {
x: center[0] + (Math.cos(Math.PI / 4) * radius) / 2,
y: center[1] + (Math.sin(Math.PI / 4) * radius) / 2,
@ -648,6 +673,8 @@ class CircleSegment implements SegmentUtils {
const label = labelWrapperElem.children[0] as HTMLParagraphElement
label.innerText = `${roundOff(radius)}`
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
// Calculate the angle for the label
const isPlaneBackFace = center[0] > indicatorPoint.x
label.style.setProperty(
'--degree',
@ -925,6 +952,585 @@ class CircleThreePointSegment implements SegmentUtils {
}
}
class ArcSegment implements SegmentUtils {
init: SegmentUtils['init'] = ({
prevSegment,
input,
id,
pathToNode,
isDraftSegment,
scale = 1,
theme,
isSelected,
sceneInfra,
}) => {
if (input.type !== 'arc-segment') {
return new Error('Invalid segment type')
}
const { from, to, center, radius, ccw } = input
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor
// Calculate start and end angles
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
const group = new Group()
const geometry = createArcGeometry({
center,
radius,
startAngle,
endAngle,
ccw,
isDashed: isDraftSegment,
scale,
})
const mat = new MeshBasicMaterial({ color })
const arcMesh = new Mesh(geometry, mat)
const meshType = isDraftSegment ? ARC_SEGMENT_DASH : ARC_SEGMENT_BODY
// Create handles for the arc
const endAngleHandle = createArrowhead(scale, theme, color)
endAngleHandle.name = ARC_ANGLE_END
endAngleHandle.userData.type = ARC_ANGLE_END
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
// A radius indicator that appears from the center to the perimeter
const radiusIndicatorGroup = createLengthIndicator({
from: center,
to: from,
scale,
})
const grey = 0xaaaaaa
// Create a line from the center to the 'to' point
const centerToFromLine = createLine({
from: center,
to: from,
scale,
color: grey, // Light gray color for the line
})
centerToFromLine.name = ARC_CENTER_TO_FROM
const centerToToLine = createLine({
from: center,
to,
scale,
color: grey, // Light gray color for the line
})
centerToToLine.name = ARC_CENTER_TO_TO
const angleReferenceLine = createLine({
from: [center[0] + (ANGLE_INDICATOR_RADIUS - 2) * scale, center[1]],
to: [center[0] + (ANGLE_INDICATOR_RADIUS + 2) * scale, center[1]],
scale,
color: grey, // Light gray color for the line
})
angleReferenceLine.name = ARC_ANGLE_REFERENCE_LINE
// Create a curved line with an arrow to indicate the angle
const angleIndicator = createAngleIndicator({
center,
radius: ANGLE_INDICATOR_RADIUS, // Half the radius for the indicator
startAngle: 0,
endAngle,
scale,
color: grey, // Red color for the angle indicator
}) as Line
angleIndicator.name = 'angleIndicator'
// Create a new angle indicator for the end angle
const endAngleIndicator = createAngleIndicator({
center,
radius: ANGLE_INDICATOR_RADIUS, // Half the radius for the indicator
startAngle: 0,
endAngle: (endAngle * Math.PI) / 180,
scale,
color: grey, // Green color for the end angle indicator
}) as Line
endAngleIndicator.name = 'endAngleIndicator'
// Create a length indicator for the end angle
const endAngleLengthIndicator = createLengthIndicator({
from: center,
to: [
center[0] + Math.cos(endAngle) * radius,
center[1] + Math.sin(endAngle) * radius,
],
scale,
})
endAngleLengthIndicator.name = 'endAngleLengthIndicator'
arcMesh.userData.type = meshType
arcMesh.name = meshType
group.userData = {
type: ARC_SEGMENT,
draft: isDraftSegment,
id,
from,
to,
radius,
center,
ccw,
prevSegment,
pathToNode,
isSelected,
baseColor,
}
group.name = ARC_SEGMENT
group.add(
arcMesh,
endAngleHandle,
circleCenterGroup,
radiusIndicatorGroup,
centerToFromLine,
centerToToLine,
angleReferenceLine,
angleIndicator,
endAngleIndicator,
endAngleLengthIndicator
)
const updateOverlaysCallback = this.update({
prevSegment,
input,
group,
scale,
sceneInfra,
})
if (err(updateOverlaysCallback)) return updateOverlaysCallback
return {
group,
updateOverlaysCallback,
}
}
update: SegmentUtils['update'] = ({
prevSegment,
input,
group,
scale = 1,
sceneInfra,
}) => {
if (input.type !== 'arc-segment') {
return new Error('Invalid segment type')
}
const { from, to, center, radius, ccw } = input
group.userData.from = from
group.userData.to = to
group.userData.center = center
group.userData.radius = radius
group.userData.ccw = ccw
group.userData.prevSegment = prevSegment
// Calculate start and end angles
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
// Normalize the angle to -180 to 180 degrees
// const normalizedStartAngle = ((startAngle * 180 / Math.PI) + 180) % 360 - 180
const normalizedStartAngle = normaliseAngle((startAngle * 180) / Math.PI)
const normalizedEndAngle = (((endAngle * 180) / Math.PI + 180) % 360) - 180
const endAngleHandle = group.getObjectByName(ARC_ANGLE_END) as Group
const radiusLengthIndicator = group.getObjectByName(
SEGMENT_LENGTH_LABEL
) as Group
const circleCenterHandle = group.getObjectByName(
CIRCLE_CENTER_HANDLE
) as Group
// Calculate arc length
let arcAngle = endAngle - startAngle
if (ccw && arcAngle > 0) arcAngle = arcAngle - 2 * Math.PI
if (!ccw && arcAngle < 0) arcAngle = arcAngle + 2 * Math.PI
const arcLength = Math.abs(arcAngle) * radius
const pxLength = arcLength / scale
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
const hoveredParent =
sceneInfra.hoveredObject &&
getParentGroup(sceneInfra.hoveredObject, [ARC_SEGMENT])
let isHandlesVisible = !shouldHideIdle
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
isHandlesVisible = !shouldHideHover
}
if (endAngleHandle) {
endAngleHandle.position.set(to[0], to[1], 0)
const tangentAngle = endAngle + (Math.PI / 2) * (ccw ? 1 : -1)
endAngleHandle.quaternion.setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(Math.cos(tangentAngle), Math.sin(tangentAngle), 0)
)
endAngleHandle.scale.set(scale, scale, scale)
endAngleHandle.visible = isHandlesVisible
}
if (radiusLengthIndicator) {
// The radius indicator is placed halfway between the center and the start angle point
const indicatorPoint = {
x: center[0] + (Math.cos(startAngle) * radius) / 2,
y: center[1] + (Math.sin(startAngle) * radius) / 2,
}
const labelWrapper = radiusLengthIndicator.getObjectByName(
SEGMENT_LENGTH_LABEL_TEXT
) as CSS2DObject
const labelWrapperElem = labelWrapper.element as HTMLDivElement
const label = labelWrapperElem.children[0] as HTMLParagraphElement
label.innerText = `R:${roundOff(radius)}${'\n'}A:${roundOff(
roundOff((startAngle * 180) / Math.PI)
)}`
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
// Calculate the angle for the label
label.style.setProperty('--degree', `-${startAngle}rad`)
label.style.setProperty('--x', `0px`)
label.style.setProperty('--y', `0px`)
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
radiusLengthIndicator.visible = isHandlesVisible
}
if (circleCenterHandle) {
circleCenterHandle.position.set(center[0], center[1], 0)
circleCenterHandle.scale.set(scale, scale, scale)
circleCenterHandle.visible = isHandlesVisible
}
const arcSegmentBody = group.children.find(
(child) => child.userData.type === ARC_SEGMENT_BODY
) as Mesh
if (arcSegmentBody) {
const newGeo = createArcGeometry({
radius,
center,
startAngle,
endAngle,
ccw,
scale,
})
arcSegmentBody.geometry = newGeo
}
const arcSegmentBodyDashed = group.getObjectByName(ARC_SEGMENT_DASH)
if (arcSegmentBodyDashed instanceof Mesh) {
arcSegmentBodyDashed.geometry = createArcGeometry({
center,
radius,
startAngle,
endAngle,
ccw,
isDashed: true,
scale,
})
}
const centerToFromLine = group.getObjectByName(ARC_CENTER_TO_FROM) as Line
if (centerToFromLine) {
updateLine(centerToFromLine, { from: center, to: from, scale })
centerToFromLine.visible = isHandlesVisible
}
const centerToToLine = group.getObjectByName(ARC_CENTER_TO_TO) as Line
if (centerToToLine) {
updateLine(centerToToLine, { from: center, to, scale })
centerToToLine.visible = isHandlesVisible
}
const angleReferenceLine = group.getObjectByName(
ARC_ANGLE_REFERENCE_LINE
) as Line
if (angleReferenceLine) {
updateLine(angleReferenceLine, {
from: center,
to: [center[0] + 34 * scale, center[1]],
scale,
})
angleReferenceLine.visible = isHandlesVisible
}
const angleIndicator = group.getObjectByName('angleIndicator') as Line
if (angleIndicator) {
updateAngleIndicator(angleIndicator, {
center,
radiusPx: ANGLE_INDICATOR_RADIUS - 10,
startAngle: 0,
endAngle: (normalizedStartAngle * Math.PI) / 180,
scale,
})
angleIndicator.visible = isHandlesVisible
}
const endAngleIndicator = group.getObjectByName('endAngleIndicator') as Line
if (endAngleIndicator) {
updateAngleIndicator(endAngleIndicator, {
center,
radiusPx: ANGLE_INDICATOR_RADIUS,
startAngle: 0,
endAngle: (normalizedEndAngle * Math.PI) / 180,
scale,
})
endAngleIndicator.visible = isHandlesVisible
}
const endAngleLengthIndicator = group.getObjectByName(
'endAngleLengthIndicator'
) as Group
if (endAngleLengthIndicator) {
const labelWrapper = endAngleLengthIndicator.getObjectByName(
SEGMENT_LENGTH_LABEL_TEXT
) as CSS2DObject
const labelWrapperElem = labelWrapper.element as HTMLDivElement
const label = labelWrapperElem.children[0] as HTMLParagraphElement
label.innerText = `A:${roundOff(normalizedEndAngle)}`
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
// Position the label
const indicatorPoint = {
x: center[0] + (Math.cos(endAngle) * radius) / 2,
y: center[1] + (Math.sin(endAngle) * radius) / 2,
}
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
endAngleLengthIndicator.visible = isHandlesVisible
}
return () =>
sceneInfra.updateOverlayDetails({
handle: endAngleHandle,
group,
isHandlesVisible,
from,
to,
angle: endAngle + (Math.PI / 2) * (ccw ? 1 : -1),
hasThreeDotMenu: true,
})
}
}
class ThreePointArcSegment implements SegmentUtils {
init: SegmentUtils['init'] = ({
input,
id,
pathToNode,
isDraftSegment,
scale = 1,
theme,
isSelected = false,
sceneInfra,
prevSegment,
}) => {
if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type')
}
const { p1, p2, p3 } = input
const { center_x, center_y, radius } = calculate_circle_from_3_points(
p1[0],
p1[1],
p2[0],
p2[1],
p3[0],
p3[1]
)
const center: [number, number] = [center_x, center_y]
const baseColor = getThemeColorForThreeJs(theme)
const color = isSelected ? 0x0000ff : baseColor
// Calculate start and end angles
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
const group = new Group()
const geometry = createArcGeometry({
center,
radius,
startAngle,
endAngle,
ccw: !isClockwise([p1, p2, p3]),
isDashed: isDraftSegment,
scale,
})
const mat = new MeshBasicMaterial({ color })
const arcMesh = new Mesh(geometry, mat)
const meshType = isDraftSegment
? THREE_POINT_ARC_SEGMENT_DASH
: THREE_POINT_ARC_SEGMENT_BODY
// Create handles for p2 and p3 using createCircleThreePointHandle
const p2Handle = createCircleThreePointHandle(
scale,
theme,
THREE_POINT_ARC_HANDLE2,
color
)
p2Handle.position.set(p2[0], p2[1], 0)
const p3Handle = createCircleThreePointHandle(
scale,
theme,
THREE_POINT_ARC_HANDLE3,
color
)
p3Handle.position.set(p3[0], p3[1], 0)
arcMesh.userData.type = meshType
arcMesh.name = meshType
group.userData = {
type: THREE_POINT_ARC_SEGMENT,
draft: isDraftSegment,
id,
from: p1,
to: p3,
p1,
p2,
p3,
radius,
center,
ccw: false,
prevSegment,
pathToNode,
isSelected,
baseColor,
}
group.name = THREE_POINT_ARC_SEGMENT
group.add(arcMesh, p2Handle, p3Handle)
const updateOverlaysCallback = this.update({
prevSegment,
input,
group,
scale,
sceneInfra,
})
if (err(updateOverlaysCallback)) return updateOverlaysCallback
return {
group,
updateOverlaysCallback,
}
}
update: SegmentUtils['update'] = ({
prevSegment,
input,
group,
scale = 1,
sceneInfra,
}) => {
if (input.type !== 'circle-three-point-segment') {
return new Error('Invalid segment type')
}
const { p1, p2, p3 } = input
const { center_x, center_y, radius } = calculate_circle_from_3_points(
p1[0],
p1[1],
p2[0],
p2[1],
p3[0],
p3[1]
)
const center: [number, number] = [center_x, center_y]
group.userData.from = p1
group.userData.to = p3
group.userData.p1 = p1
group.userData.p2 = p2
group.userData.p3 = p3
group.userData.center = center
group.userData.radius = radius
group.userData.prevSegment = prevSegment
// Calculate start and end angles
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
const p2Handle = group.getObjectByName(THREE_POINT_ARC_HANDLE2) as Group
const p3Handle = group.getObjectByName(THREE_POINT_ARC_HANDLE3) as Group
const arcSegmentBody = group.children.find(
(child) => child.userData.type === THREE_POINT_ARC_SEGMENT_BODY
) as Mesh
if (arcSegmentBody) {
const newGeo = createArcGeometry({
radius,
center,
startAngle,
endAngle,
ccw: !isClockwise([p1, p2, p3]),
scale,
})
arcSegmentBody.geometry = newGeo
}
const arcSegmentBodyDashed = group.getObjectByName(
THREE_POINT_ARC_SEGMENT_DASH
)
if (arcSegmentBodyDashed instanceof Mesh) {
arcSegmentBodyDashed.geometry = createArcGeometry({
center,
radius,
startAngle,
endAngle,
ccw: !isClockwise([p1, p2, p3]),
isDashed: true,
scale,
})
}
if (p2Handle) {
p2Handle.position.set(p2[0], p2[1], 0)
p2Handle.scale.set(scale, scale, scale)
p2Handle.visible = true
}
if (p3Handle) {
p3Handle.position.set(p3[0], p3[1], 0)
p3Handle.scale.set(scale, scale, scale)
p3Handle.visible = true
}
return () => {
const overlays: SegmentOverlays = {}
const overlayDetails = [p2Handle, p3Handle].map((handle, index) =>
sceneInfra.updateOverlayDetails({
handle: handle,
group,
isHandlesVisible: true,
from: p1,
to: p3,
angle: endAngle + Math.PI / 2,
hasThreeDotMenu: true,
})
)
const segmentOverlays: SegmentOverlay[] = []
overlayDetails.forEach((payload, index) => {
if (payload?.type === 'set-one') {
overlays[payload.pathToNodeString] = payload.seg
// Add filterValue: 'interior' for p2 and 'end' for p3
segmentOverlays.push({
...payload.seg[0],
filterValue: index === 0 ? 'interior' : 'end',
})
}
})
const segmentOverlayPayload: SegmentOverlayPayload = {
type: 'set-one',
pathToNodeString:
overlayDetails[0]?.type === 'set-one'
? overlayDetails[0].pathToNodeString
: '',
seg: segmentOverlays,
}
return segmentOverlayPayload
}
}
}
export function createProfileStartHandle({
from,
isDraft = false,
@ -1010,7 +1616,7 @@ function createCircleCenterHandle(
function createCircleThreePointHandle(
scale = 1,
theme: Themes,
name: `circle-three-point-handle${'1' | '2' | '3'}`,
name: string,
color?: number
): Group {
const circleCenterGroup = new Group()
@ -1249,9 +1855,11 @@ export function createArcGeometry({
)
)
const remainingArcGeometry = new ExtrudeGeometry(shape, {
steps: 50,
steps: 1,
bevelEnabled: false,
extrudePath: remainingArcPath,
extrudePath: new CatmullRomCurve3(
remainingArcPoints.map((p) => new Vector3(p.x, p.y, 0))
),
})
dashGeometries.push(remainingArcGeometry)
}
@ -1351,10 +1959,111 @@ export function dashedStraight(
geo.userData.type = 'dashed'
return geo
}
function createLine({
from,
to,
scale,
color,
}: {
from: [number, number]
to: [number, number]
scale: number
color: number
}): Line {
// Implementation for creating a line
const lineGeometry = new BufferGeometry().setFromPoints([
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0),
])
const lineMaterial = new LineBasicMaterial({ color })
return new Line(lineGeometry, lineMaterial)
}
function updateLine(
line: Line,
{
from,
to,
scale,
}: { from: [number, number]; to: [number, number]; scale: number }
) {
// Implementation for updating a line
const points = [
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0),
]
line.geometry.setFromPoints(points)
}
function createAngleIndicator({
center,
radius,
startAngle,
endAngle,
scale,
color,
}: {
center: [number, number]
radius: number
startAngle: number
endAngle: number
scale: number
color: number
}): Line {
// Implementation for creating an angle indicator
const curve = new EllipseCurve(
center[0],
center[1],
radius,
radius,
startAngle,
endAngle,
false,
0
)
const points = curve.getPoints(50)
const geometry = new BufferGeometry().setFromPoints(points)
const material = new LineBasicMaterial({ color })
return new Line(geometry, material)
}
function updateAngleIndicator(
angleIndicator: Line,
{
center,
radiusPx,
startAngle,
endAngle,
scale,
}: {
center: [number, number]
radiusPx: number
startAngle: number
endAngle: number
scale: number
}
) {
// Implementation for updating an angle indicator
const curve = new EllipseCurve(
center[0],
center[1],
radiusPx * scale,
radiusPx * scale,
startAngle,
endAngle,
endAngle < startAngle,
0
)
const points = curve.getPoints(50)
angleIndicator.geometry.setFromPoints(points)
}
export const segmentUtils = {
straight: new StraightSegment(),
tangentialArcTo: new TangentialArcToSegment(),
circle: new CircleSegment(),
circleThreePoint: new CircleThreePointSegment(),
arc: new ArcSegment(),
threePointArc: new ThreePointArcSegment(),
} as const

View File

@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from 'react'
import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
import { defaultSourceRange, topLevelRange } from 'lang/wasm'
import { isArray } from 'lib/utils'
export function AstExplorer() {

View File

@ -4,7 +4,6 @@ import {
Selections,
canSubmitSelectionArg,
getSelectionCountByType,
getSelectionTypeDisplayText,
} from 'lib/selections'
import { useSelector } from '@xstate/react'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'

View File

@ -1,13 +1,9 @@
import { Dialog } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
import { useSettings } from 'machines/appMachine'
const DownloadAppBanner = () => {
const [searchParams] = useSearchParams()
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
const settings = useSettings()
const [isBannerDismissed, setIsBannerDismissed] = useState(
settings.app.dismissWebBanner.current

View File

@ -70,6 +70,7 @@ import {
import {
KclValue,
PathToNode,
PipeExpression,
Program,
VariableDeclaration,
parse,
@ -110,7 +111,6 @@ import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { useSettings } from 'machines/appMachine'
import { isDesktop } from 'lib/isDesktop'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -1497,6 +1497,48 @@ export const ModelingMachineProvider = ({
return result
}
),
'set-up-draft-arc-three-point': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftArcThreePoint(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'set-up-draft-arc': fromPromise(
async ({ input: { sketchDetails, data } }) => {
if (!sketchDetails || !data)
return reject('No sketch details or data')
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
const result = await sceneEntitiesManager.setupDraftArc(
sketchDetails.sketchEntryNodePath,
sketchDetails.sketchNodePaths,
sketchDetails.planeNodePath,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
if (err(result)) return reject(result)
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
return result
}
),
'setup-client-side-sketch-segments': fromPromise(
async ({ input: { sketchDetails, selectionRanges } }) => {
if (!sketchDetails) return
@ -1568,24 +1610,53 @@ export const ModelingMachineProvider = ({
}
const indexToDelete = sketchDetails?.expressionIndexToDelete || -1
let isLastInPipeThreePointArc = false
if (indexToDelete >= 0) {
// this is the expression that was added when as sketch tool was used but not completed
// i.e first click for the center of the circle, but not the second click for the radius
// we added a circle to editor, but they bailed out early so we should remove it
moddedAst.body.splice(indexToDelete, 1)
// make sure the deleted expression is removed from the sketchNodePaths
updatedSketchNodePaths = updatedSketchNodePaths.filter(
(path) => path[1][0] !== indexToDelete
const pipe = getNodeFromPath<PipeExpression>(
moddedAst,
pathToProfile,
'PipeExpression'
)
// if the deleted expression was the entryNodePath, we should just make it the first sketchNodePath
// as a safe default
pathToProfile =
pathToProfile[1][0] !== indexToDelete
? pathToProfile
: updatedSketchNodePaths[0]
if (err(pipe)) {
isLastInPipeThreePointArc = false
} else {
const lastInPipe = pipe?.node?.body?.[pipe.node.body.length - 1]
if (
lastInPipe &&
Number(pathToProfile[1][0]) === indexToDelete &&
lastInPipe.type === 'CallExpression' &&
lastInPipe.callee.type === 'Identifier' &&
lastInPipe.callee.name === 'arcTo'
) {
isLastInPipeThreePointArc = true
pipe.node.body = pipe.node.body.slice(0, -1)
}
}
if (!isLastInPipeThreePointArc) {
moddedAst.body.splice(indexToDelete, 1)
// make sure the deleted expression is removed from the sketchNodePaths
updatedSketchNodePaths = updatedSketchNodePaths.filter(
(path) => path[1][0] !== indexToDelete
)
// if the deleted expression was the entryNodePath, we should just make it the first sketchNodePath
// as a safe default
pathToProfile =
pathToProfile[1][0] !== indexToDelete
? pathToProfile
: updatedSketchNodePaths[0]
}
}
if (doesNeedSplitting || indexToDelete >= 0) {
if (
doesNeedSplitting ||
indexToDelete >= 0 ||
isLastInPipeThreePointArc
) {
await kclManager.executeAstMock(moddedAst)
await codeManager.updateEditorWithAstAndWriteToFile(moddedAst)
}

View File

@ -4,12 +4,11 @@ import {
useLocation,
useNavigate,
useRouteLoaderData,
redirect,
} from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { markOnce } from 'lib/performance'
import { useAuthNavigation } from 'hooks/useAuthNavigation'
import { useAuthState, useSettings } from 'machines/appMachine'
import { useSettings } from 'machines/appMachine'
import { IndexLoaderData } from 'lib/types'
import { getAppSettingsFilePath } from 'lib/desktop'
import { isDesktop } from 'lib/isDesktop'
@ -17,9 +16,7 @@ import { trap } from 'lib/trap'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { loadAndValidateSettings } from 'lib/settings/settingsUtils'
import { settingsActor } from 'machines/appMachine'
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
import { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
import { SnapshotFrom } from 'xstate'
export const RouteProviderContext = createContext({})
@ -35,7 +32,6 @@ export function RouteProvider({ children }: { children: ReactNode }) {
const location = useLocation()
const settings = useSettings()
const authState = useAuthState()
useEffect(() => {
// On initialization, the react-router-dom does not send a 'loading' state event.
// it sends an idle event first.

View File

@ -3,7 +3,6 @@ import { CustomIcon } from 'components/CustomIcon'
import decamelize from 'decamelize'
import Fuse from 'fuse.js'
import { interactionMap } from 'lib/settings/initialKeybindings'
import { Setting } from 'lib/settings/initialSettings'
import { SettingsLevel } from 'lib/settings/settingsTypes'
import { useSettings } from 'machines/appMachine'
import { useEffect, useMemo, useRef, useState } from 'react'

View File

@ -28,7 +28,7 @@ import { base64Decode } from 'lang/wasm'
import { sendTelemetry } from 'lib/textToCad'
import { Themes } from 'lib/theme'
import { ActionButton } from './ActionButton'
import { commandBarActor, commandBarMachine } from 'machines/commandBarMachine'
import { commandBarActor } from 'machines/commandBarMachine'
import { EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine'
import { reportRejection } from 'lib/trap'

View File

@ -6,9 +6,9 @@ import {
} from '@kittycad/codemirror-lsp-client'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import init, {
ServerConfig,
copilot_lsp_run,
kcl_lsp_run,
LspServerConfig,
lsp_run_copilot,
lsp_run_kcl,
} from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
import * as jsrpc from 'json-rpc-2.0'
import {
@ -30,13 +30,13 @@ const initialise = async (wasmUrl: string) => {
}
export async function copilotLspRun(
config: ServerConfig,
config: LspServerConfig,
token: string,
baseUrl: string
) {
try {
console.log('starting copilot lsp')
await copilot_lsp_run(config, token, baseUrl)
await lsp_run_copilot(config, token, baseUrl)
} catch (e: any) {
console.log('copilot lsp failed', e)
// We can't restart here because a moved value, we should do this another way.
@ -44,13 +44,13 @@ export async function copilotLspRun(
}
export async function kclLspRun(
config: ServerConfig,
config: LspServerConfig,
token: string,
baseUrl: string
) {
try {
console.log('start kcl lsp')
await kcl_lsp_run(config, token, baseUrl)
await lsp_run_kcl(config, token, baseUrl)
} catch (e: any) {
console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way.
@ -70,7 +70,7 @@ onmessage = function (event: MessageEvent) {
initialise(wasmUrl)
.then(async (instantiatedModule) => {
console.log('Worker: WASM module loaded', worker, instantiatedModule)
const config = new ServerConfig(
const config = new LspServerConfig(
intoServer,
fromServer,
fileSystemManager

View File

@ -1,5 +1,5 @@
import { useLayoutEffect, useEffect, useRef } from 'react'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { engineCommandManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
import { useModelingContext } from './useModelingContext'
@ -93,10 +93,6 @@ export function useSetupEngineManager(
engineCommandManager.settings = settings
const handleResize = deferExecution(() => {
const { width, height } = getDimensions(
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
engineCommandManager.handleResize(engineCommandManager.streamDimensions)
}, 500)

View File

@ -1,4 +1,4 @@
import { executeAst, lintAst } from 'lang/langHelpers'
import { executeAst, executeAstMock, lintAst } from 'lang/langHelpers'
import { handleSelectionBatch, Selections } from 'lib/selections'
import {
KCLError,
@ -324,6 +324,7 @@ export class KclManager {
if (this.wasmInitFailed) {
this.wasmInitFailed = false
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.wasmInitFailed = true
}
@ -338,7 +339,7 @@ export class KclManager {
if (this.isExecuting) {
this.executeIsStale = args
// The previous execteAst will be rejected and cleaned up. The execution will be marked as stale.
// The previous executeAst will be rejected and cleaned up. The execution will be marked as stale.
// A new executeAst will start.
this.engineCommandManager.rejectAllModelingCommands(
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE
@ -358,9 +359,7 @@ export class KclManager {
const { logs, errors, execState, isInterrupted } = await executeAst({
ast,
path: codeManager.currentFilePath || undefined,
engineCommandManager: this.engineCommandManager,
rustContext,
isMock: false,
})
// Program was not interrupted, setup the scene
@ -476,11 +475,9 @@ export class KclManager {
}
this._ast = { ...newAst }
const { logs, errors, execState } = await executeAst({
const { logs, errors, execState } = await executeAstMock({
ast: newAst,
engineCommandManager: this.engineCommandManager,
rustContext,
isMock: true,
})
this._logs = logs

View File

@ -7,7 +7,6 @@ import {
sketchFromKclValue,
defaultArtifactGraph,
topLevelRange,
VariableMap,
} from './wasm'
import { enginelessExecutor } from '../lib/testHelpers'
import { KCLError } from './errors'

View File

@ -5,7 +5,6 @@ import {
assertParse,
initPromise,
Parameter,
SourceRange,
topLevelRange,
} from './wasm'
import { err } from 'lib/trap'

View File

@ -38,6 +38,7 @@ afterAll(async () => {
try {
process.chdir('..')
await fs.rm(DIR_KCL_SAMPLES, { recursive: true })
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {}
})

View File

@ -1,12 +1,10 @@
import {
Program,
executeMock,
kclLint,
emptyExecState,
ExecState,
jsAppSettings,
} from 'lang/wasm'
import { EngineCommandManager } from 'lang/std/engineConnection'
import { KCLError } from 'lang/errors'
import { Diagnostic } from '@codemirror/lint'
import { Node } from '@rust/kcl-lib/bindings/Node'
@ -28,6 +26,8 @@ export type ToolTip =
| 'tangentialArcTo'
| 'circle'
| 'circleThreePoint'
| 'arcTo'
| 'arc'
export const toolTips: Array<ToolTip> = [
'line',
@ -44,35 +44,31 @@ export const toolTips: Array<ToolTip> = [
'angledLineThatIntersects',
'tangentialArcTo',
'circleThreePoint',
'arc',
'arcTo',
]
export async function executeAst({
ast,
path,
engineCommandManager,
isMock,
usePrevMemory,
rustContext,
}: {
ast: Node<Program>
path?: string
engineCommandManager: EngineCommandManager
rustContext: RustContext
isMock: boolean
usePrevMemory?: boolean
isInterrupted?: boolean
}): Promise<{
interface ExecutionResult {
logs: string[]
errors: KCLError[]
execState: ExecState
isInterrupted: boolean
}> {
try {
const execState = await (isMock
? executeMock(ast, usePrevMemory, path)
: rustContext.execute(ast, { settings: await jsAppSettings() }, path))
}
await engineCommandManager.waitForAllCommands()
export async function executeAst({
ast,
rustContext,
path,
}: {
ast: Node<Program>
rustContext: RustContext
path?: string
}): Promise<ExecutionResult> {
try {
const settings = { settings: await jsAppSettings() }
const execState = await rustContext.execute(ast, settings, path)
await rustContext.waitForAllEngineCommands()
return {
logs: [],
errors: [],
@ -80,29 +76,65 @@ export async function executeAst({
isInterrupted: false,
}
} catch (e: any) {
let isInterrupted = false
if (e instanceof KCLError) {
// Detect if it is a force interrupt error which is not a KCL processing error.
if (
e.msg ===
'Failed to wait for promise from engine: JsValue("Force interrupt, executionIsStale, new AST requested")'
) {
isInterrupted = true
}
return {
errors: [e],
logs: [],
execState: emptyExecState(),
isInterrupted,
}
} else {
console.log(e)
return {
logs: [e],
errors: [],
execState: emptyExecState(),
isInterrupted,
}
return handleExecuteError(e)
}
}
export async function executeAstMock({
ast,
rustContext,
path,
usePrevMemory,
}: {
ast: Node<Program>
rustContext: RustContext
path?: string
usePrevMemory?: boolean
}): Promise<ExecutionResult> {
try {
const settings = { settings: await jsAppSettings() }
const execState = await rustContext.executeMock(
ast,
settings,
path,
usePrevMemory
)
await rustContext.waitForAllEngineCommands()
return {
logs: [],
errors: [],
execState,
isInterrupted: false,
}
} catch (e: any) {
return handleExecuteError(e)
}
}
function handleExecuteError(e: any): ExecutionResult {
let isInterrupted = false
if (e instanceof KCLError) {
// Detect if it is a force interrupt error which is not a KCL processing error.
if (
e.msg ===
'Failed to wait for promise from engine: JsValue("Force interrupt, executionIsStale, new AST requested")'
) {
isInterrupted = true
}
return {
errors: [e],
logs: [],
execState: emptyExecState(),
isInterrupted,
}
} else {
console.log(e)
return {
logs: [e],
errors: [],
execState: emptyExecState(),
isInterrupted,
}
}
}

View File

@ -19,7 +19,6 @@ import {
BinaryExpression,
PathToNode,
SourceRange,
sketchFromKclValue,
isPathToNodeNumber,
parse,
formatNumber,
@ -75,7 +74,6 @@ import {
import { BodyItem } from '@rust/kcl-lib/bindings/BodyItem'
import { findKwArg } from './util'
import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment'
import { codeManager } from 'lib/singletons'
export function startSketchOnDefault(
node: Node<Program>,

View File

@ -13,8 +13,6 @@ import {
} from '../wasm'
import {
createCallExpressionStdLib,
createPipeSubstitution,
createObjectExpression,
createArrayExpression,
createIdentifier,
createPipeExpression,

View File

@ -8,7 +8,7 @@ import {
rustContext,
} from 'lib/singletons'
import { err } from 'lib/trap'
import { executeAst } from 'lang/langHelpers'
import { executeAstMock } from 'lang/langHelpers'
export const deletionErrorMessage =
'Unable to delete selection. Please edit manually in code pane.'
@ -29,11 +29,9 @@ export async function deleteSelectionPromise(
return new Error(deletionErrorMessage)
}
const testExecute = await executeAst({
const testExecute = await executeAstMock({
ast: modifiedAst,
engineCommandManager,
rustContext,
isMock: true,
rustContext: rustContext,
})
if (testExecute.errors.length) {
return new Error(deletionErrorMessage)

View File

@ -5,9 +5,6 @@ import {
PathToNode,
Identifier,
topLevelRange,
PipeExpression,
CallExpression,
VariableDeclarator,
} from './wasm'
import {
findAllPreviousVariables,

View File

@ -39,7 +39,7 @@ import {
import { err, Reason } from 'lib/trap'
import { Node } from '@rust/kcl-lib/bindings/Node'
import { findKwArg } from './util'
import { codeRefFromRange, getPlaneFromArtifact } from './std/artifactGraph'
import { codeRefFromRange } from './std/artifactGraph'
import { FunctionExpression } from '@rust/kcl-lib/bindings/FunctionExpression'
import { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'
@ -595,6 +595,7 @@ export function isLinesParallelAndConstrained(
artifact: artifactGraph.get(prevSegment.__geoMeta.id),
},
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return {
isParallelAndConstrained: false,

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