Merge remote-tracking branch 'origin/main' into paultag/import

This commit is contained in:
Paul R. Tagliamonte
2025-03-13 11:17:34 -04:00
122 changed files with 1813 additions and 1666 deletions

View File

@ -33,26 +33,63 @@ jobs:
- run: yarn install - run: yarn install
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'rust/**'
- name: Download Wasm Cache
id: download-wasm
if: ${{ github.event_name == 'pull_request' && steps.filter.outputs.rust == 'false' }}
uses: dawidd6/action-download-artifact@v7
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: rust/kcl-wasm-lib/pkg
- name: Build WASM condition
id: wasm
run: |
set -euox pipefail
# Build wasm if this is a push to main or tag, there are Rust changes, or
# downloading from the wasm cache failed.
if [[ ${{github.event_name}} == 'push' || ${{steps.filter.outputs.rust}} == 'true' || ${{steps.download-wasm.outcome}} == 'failure' ]]; then
echo "should-build-wasm=true" >> $GITHUB_OUTPUT
else
echo "should-build-wasm=false" >> $GITHUB_OUTPUT
fi
- name: Use correct Rust toolchain - name: Use correct Rust toolchain
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
shell: bash shell: bash
run: | run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./ [ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust - name: Install rust
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
cache: false # Configured below. cache: false # Configured below.
# TODO: see if we can fetch from main instead if no diff at rust
- uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3 - uses: taiki-e/install-action@955a6ff1416eae278c9f833008a9beb4b7f9afe3
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
with: with:
tool: wasm-pack tool: wasm-pack
- name: Rust Cache - name: Rust Cache
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: rust workspaces: rust
- name: Run build:wasm - name: Run build:wasm
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
run: "yarn build:wasm" run: "yarn build:wasm"
- name: Set nightly version, product name, release notes, and icons - name: Set nightly version, product name, release notes, and icons

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { import {
getUtils, getUtils,
TEST_COLORS, TEST_COLORS,

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { HomePageFixture } from './fixtures/homePageFixture' import { HomePageFixture } from './fixtures/homePageFixture'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'

View File

@ -10,7 +10,11 @@ import fsp from 'fs/promises'
test( test(
'export works on the first try', 'export works on the first try',
{ tag: ['@electron', '@skipLocalEngine'] }, { tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context, scene }, testInfo) => { async ({ page, context, scene, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
@ -86,7 +90,7 @@ test(
await expect(exportingToastMessage).not.toBeVisible() await expect(exportingToastMessage).not.toBeVisible()
const firstFileFullPath = path.resolve( const firstFileFullPath = path.resolve(
getPlaywrightDownloadDir(page), getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName exportFileName
) )
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {
@ -165,7 +169,7 @@ test(
])) ]))
const secondFileFullPath = path.resolve( const secondFileFullPath = path.resolve(
getPlaywrightDownloadDir(page), getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName exportFileName
) )
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {

View File

@ -158,11 +158,14 @@ test.describe('when using the file tree to', () => {
await createNewFile('lee') await createNewFile('lee')
await test.step('Postcondition: there are 5 new lee-*.kcl files', async () => { await test.step('Postcondition: there are 5 new lee-*.kcl files', async () => {
await expect( await expect
page .poll(() =>
.locator('[data-testid="file-pane-scroll-container"] button') page
.filter({ hasText: /lee[-]?[0-5]?/ }) .locator('[data-testid="file-pane-scroll-container"] button')
).toHaveCount(5) .filter({ hasText: /lee[-]?[0-5]?/ })
.count()
)
.toEqual(5)
}) })
} }
) )

View File

@ -27,28 +27,19 @@ type CmdBarSerialised =
export class CmdBarFixture { export class CmdBarFixture {
public page: Page public page: Page
public cmdBarOpenBtn!: Locator
get cmdBarOpenBtn() { public cmdBarElement!: Locator
return this.page.getByTestId('command-bar-open-button')
}
get cmdBarElement() {
return this.page.getByTestId('command-bar')
}
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.cmdBarOpenBtn = this.page.getByTestId('command-bar-open-button')
this.cmdBarElement = this.page.getByTestId('command-bar')
} }
get currentArgumentInput() { get currentArgumentInput() {
return this.page.getByTestId('cmd-bar-arg-value') return this.page.getByTestId('cmd-bar-arg-value')
} }
// Put all selectors here because this method is re-run on fixture creation.
reConstruct = (page: Page) => {
this.page = page
}
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => { private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) { if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
return { stage: 'commandBarClosed' } return { stage: 'commandBarClosed' }

View File

@ -24,11 +24,6 @@ export class EditorFixture {
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.codeContent = page.locator('.cm-content[data-language="kcl"]') this.codeContent = page.locator('.cm-content[data-language="kcl"]')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint') this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error') this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')

View File

@ -1,13 +1,31 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type { import type {
BrowserContext, BrowserContext,
ElectronApplication, ElectronApplication,
Fixtures as PlaywrightFixtures,
TestInfo, TestInfo,
Page, Page,
} from '@playwright/test' } from '@playwright/test'
import { getUtils, setup, setupElectron } from '../test-utils' import {
_electron as electron,
PlaywrightTestArgs,
PlaywrightWorkerArgs,
} 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 { getUtils, setup } from '../test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { join } from 'path' import fs from 'node:fs'
import path from 'path'
import { CmdBarFixture } from './cmdBarFixture' import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture' import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture' import { ToolbarFixture } from './toolbarFixture'
@ -23,7 +41,7 @@ export class AuthenticatedApp {
public readonly testInfo: TestInfo public readonly testInfo: TestInfo
public readonly viewPortSize = { width: 1200, height: 500 } public readonly viewPortSize = { width: 1200, height: 500 }
public electronApp: undefined | ElectronApplication public electronApp: undefined | ElectronApplication
public dir: string = '' public projectDirName: string = ''
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.context = context this.context = context
@ -46,7 +64,7 @@ export class AuthenticatedApp {
} }
getInputFile = (fileName: string) => { getInputFile = (fileName: string) => {
return fsp.readFile( return fsp.readFile(
join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName), path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
'utf-8' 'utf-8'
) )
} }
@ -59,101 +77,300 @@ export interface Fixtures {
scene: SceneFixture scene: SceneFixture
homePage: HomePageFixture homePage: HomePageFixture
} }
export class AuthenticatedTronApp {
public originalPage: Page
public page: Page
public browserContext: BrowserContext
public context: BrowserContext
public readonly testInfo: TestInfo
public electronApp: ElectronApplication | undefined
public readonly viewPortSize = { width: 1200, height: 500 }
public dir: string = ''
constructor( export class ElectronZoo {
browserContext: BrowserContext, public available: boolean = true
originalPage: Page, public electron!: ElectronApplication
testInfo: TestInfo public firstUrl = ''
) { public viewPortSize = { width: 1200, height: 500 }
this.page = originalPage public projectDirName = ''
this.originalPage = originalPage
this.browserContext = browserContext public page!: Page
// Will be overwritten in the initializer public context!: BrowserContext
this.context = browserContext
this.testInfo = testInfo constructor() {}
}
async initialise( async makeAvailableAgain() {
arg: { // Help remote end by signaling we're done with the connection.
fixtures: Partial<Fixtures> await this.page.evaluate(async () => {
folderSetupFn?: (projectDirName: string) => Promise<void> return new Promise((resolve) => {
cleanProjectDir?: boolean if (!window.engineCommandManager.engineConnection?.state?.type) {
appSettings?: DeepPartial<Settings> return resolve(undefined)
} = { fixtures: {} } }
) {
const { electronApp, page, context, dir } = await setupElectron({ window.engineCommandManager.tearDown()
testInfo: this.testInfo, // Keep polling (per js event tick) until state is Disconnected.
folderSetupFn: arg.folderSetupFn, const checkDisconnected = () => {
cleanProjectDir: arg.cleanProjectDir, // It's possible we never even created an engineConnection
appSettings: arg.appSettings, // e.g. never left Projects view.
viewport: this.viewPortSize, if (
window.engineCommandManager?.engineConnection?.state.type ===
'disconnected'
) {
return resolve(undefined)
}
setTimeout(checkDisconnected, 0)
}
checkDisconnected()
})
}) })
this.page = page
// These assignments "fix" some brokenness in the Playwright Workbench when await this.context.tracing.stopChunk({ path: 'trace.zip' })
// running against electron applications.
// The timeline is still broken but failure screenshots work again.
this.context = context
// TODO: try to get this to work again for screenshots, but it messed with test ends when enabled
// Object.assign(this.browserContext, this.context)
this.electronApp = electronApp // Only after cleanup we're ready.
this.dir = dir this.available = true
}
// Easier to access throughout utils async createInstanceIfMissing(testInfo: TestInfo) {
this.page.dir = dir // Create or otherwise clear the folder.
this.projectDirName = testInfo.outputPath('electron-test-projects-dir')
// Setup localStorage, addCookies, reload // We need to expose this in order for some tests that require folder
await setup(this.context, this.page, this.testInfo) // creation and some code below.
const that = this
for (const key of unsafeTypedKeys(arg.fixtures)) { const options = {
const fixture = arg.fixtures[key] args: ['.', '--no-sandbox'],
if ( env: {
!fixture || ...process.env,
fixture instanceof AuthenticatedApp || TEST_SETTINGS_FILE_KEY: this.projectDirName,
fixture instanceof AuthenticatedTronApp IS_PLAYWRIGHT: 'true',
) },
continue ...(process.env.ELECTRON_OVERRIDE_DIST_PATH
fixture.reConstruct(page) ? {
executablePath:
process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron',
}
: {}),
...(process.env.PLAYWRIGHT_RECORD_VIDEO
? {
recordVideo: {
dir: testInfo.snapshotPath(),
size: this.viewPortSize,
},
}
: {}),
} }
// Do this once and then reuse window on subsequent calls.
if (!this.electron) {
this.electron = await electron.launch(options)
this.context = this.electron.context()
this.page = await this.electron.firstWindow()
await this.context.tracing.start({ screenshots: true, snapshots: true })
}
await this.context.tracing.startChunk()
await setup(this.context, this.page, testInfo)
await this.cleanProjectDir()
// Create a consistent way to resize the page across electron and web.
// (lee) I had to do everything in the book to make electron change its
// damn window size. I succeeded in making it consistently and reliably
// do it after a whole afternoon.
this.page.setBodyDimensions = async function (dims: {
width: number
height: number
}) {
await this.setViewportSize(dims)
await that.electron?.evaluateHandle(async ({ app }, dims) => {
// @ts-ignore sorry jon but see comment in main.ts why this is ignored
await app.resizeWindow(dims.width, dims.height)
}, dims)
return this.evaluate(async (dims: { width: number; height: number }) => {
await window.electron.resizeWindow(dims.width, dims.height)
window.document.body.style.width = dims.width + 'px'
window.document.body.style.height = dims.height + 'px'
window.document.documentElement.style.width = dims.width + 'px'
window.document.documentElement.style.height = dims.height + 'px'
}, dims)
}
await this.page.setBodyDimensions(this.viewPortSize)
this.context.folderSetupFn = async function (fn) {
return fn(that.projectDirName)
.then(() => that.page.reload())
.then(() => ({
dir: that.projectDirName,
}))
}
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = this.context.addInitScript
this.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await that.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = this.page.addInitScript
this.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await that.page.reload()
}
if (!this.firstUrl) {
await this.page.getByText('Your Projects').count()
this.firstUrl = this.page.url()
}
// Due to the app controlling its own window context we need to inject new
// options and context here.
// NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
// await tronApp.electronApp.evaluate(({ app }) => {
// return app.reuseWindowForTest();
// });
await this.electron?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, this.projectDirName)
// Always start at the root view
await this.page.goto(this.firstUrl)
// Force a hard reload, destroying the stream and other state
await this.page.reload()
} }
close = async () => { async cleanProjectDir(appSettings?: DeepPartial<Settings>) {
await this.electronApp?.close?.() try {
if (fs.existsSync(this.projectDirName)) {
await fsp.rm(this.projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
try {
await fsp.mkdir(this.projectDirName)
} catch (e) {
// Not a problem if it already exists.
}
const tempSettingsFilePath = path.join(
this.projectDirName,
SETTINGS_FILE_NAME
)
let settingsOverridesToml = ''
if (appSettings) {
settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
project_directory: this.projectDirName,
...appSettings.app,
},
},
})
} else {
settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
project_directory: this.projectDirName,
},
},
})
}
await fsp.writeFile(tempSettingsFilePath, settingsOverridesToml)
} }
debugPause = () =>
new Promise(() => {
console.log('UN-RESOLVING PROMISE')
})
} }
export const fixtures = { // If yee encounter this, please try to type it.
cmdBar: async ({ page }: { page: Page }, use: any) => { type FnUse = any
// eslint-disable-next-line react-hooks/rules-of-hooks
const fixturesForElectron = {
page: async (
{ tronApp }: { tronApp: ElectronZoo },
use: FnUse,
testInfo: TestInfo
) => {
await tronApp.createInstanceIfMissing(testInfo)
await use(tronApp.page)
await tronApp?.makeAvailableAgain()
},
context: async (
{ tronApp }: { tronApp: ElectronZoo },
use: FnUse,
testInfo: TestInfo
) => {
await tronApp.createInstanceIfMissing(testInfo)
await use(tronApp.context)
},
}
const fixturesForWeb = {
page: async (
{ page, context }: { page: Page; context: BrowserContext },
use: FnUse,
testInfo: TestInfo
) => {
page.setBodyDimensions = page.setViewportSize
// We do the same thing in ElectronZoo. addInitScript simply doesn't fire
// at the correct time, so we reload the page and it fires appropriately.
const oldPageAddInitScript = page.addInitScript
page.addInitScript = async function (...args) {
// @ts-expect-error
await oldPageAddInitScript.apply(this, args)
await page.reload()
}
const oldContextAddInitScript = context.addInitScript
context.addInitScript = async function (...args) {
// @ts-expect-error
await oldContextAddInitScript.apply(this, args)
await page.reload()
}
const webApp = new AuthenticatedApp(context, page, testInfo)
await webApp.initialise()
await use(page)
},
}
const fixturesBasedOnProcessEnvPlatform = {
cmdBar: async ({ page }: { page: Page }, use: FnUse) => {
await use(new CmdBarFixture(page)) await use(new CmdBarFixture(page))
}, },
editor: async ({ page }: { page: Page }, use: any) => { editor: async ({ page }: { page: Page }, use: FnUse) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new EditorFixture(page)) await use(new EditorFixture(page))
}, },
toolbar: async ({ page }: { page: Page }, use: any) => { toolbar: async ({ page }: { page: Page }, use: FnUse) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new ToolbarFixture(page)) await use(new ToolbarFixture(page))
}, },
scene: async ({ page }: { page: Page }, use: any) => { scene: async ({ page }: { page: Page }, use: FnUse) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new SceneFixture(page)) await use(new SceneFixture(page))
}, },
homePage: async ({ page }: { page: Page }, use: any) => { homePage: async ({ page }: { page: Page }, use: FnUse) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new HomePageFixture(page)) await use(new HomePageFixture(page))
}, },
} }
if (process.env.PLATFORM === 'web') {
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForWeb)
} else {
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForElectron)
}
export { fixturesBasedOnProcessEnvPlatform }

View File

@ -27,10 +27,6 @@ export class HomePageFixture {
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.projectSection = this.page.getByTestId('home-section') this.projectSection = this.page.getByTestId('home-section')
@ -96,8 +92,12 @@ export class HomePageFixture {
} }
} }
createAndGoToProject = async (projectTitle = 'project-$nnn') => { projectsLoaded = async () => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...') await expect(this.projectSection).not.toHaveText('Loading your Projects...')
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
await this.projectsLoaded()
await this.projectButtonNew.click() await this.projectButtonNew.click()
await this.projectTextName.click() await this.projectTextName.click()
await this.projectTextName.fill(projectTitle) await this.projectTextName.fill(projectTitle)

View File

@ -53,7 +53,12 @@ export class SceneFixture {
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page) this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
} }
private _serialiseScene = async (): Promise<SceneSerialised> => { private _serialiseScene = async (): Promise<SceneSerialised> => {
const camera = await this.getCameraInfo() const camera = await this.getCameraInfo()
@ -72,17 +77,6 @@ export class SceneFixture {
.toEqual(expected) .toEqual(expected)
} }
reConstruct = (page: Page) => {
this.page = page
this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
}
makeMouseHelpers = ( makeMouseHelpers = (
x: number, x: number,
y: number, y: number,
@ -253,7 +247,7 @@ export class SceneFixture {
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() await u.closeDebugPanel()
await this.waitForExecutionDone() await this.waitForExecutionDone()
await expect(this.startEditSketchBtn).not.toBeDisabled() await expect(this.startEditSketchBtn).not.toBeDisabled()

View File

@ -37,13 +37,12 @@ export class ToolbarFixture {
featureTreeId = 'feature-tree' as const featureTreeId = 'feature-tree' as const
/** The pane element for the Feature Tree */ /** The pane element for the Feature Tree */
featureTreePane!: Locator featureTreePane!: Locator
gizmo!: Locator
gizmoDisabled!: Locator
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft') this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep') this.sweepButton = page.getByTestId('sweep')
@ -67,6 +66,13 @@ export class ToolbarFixture {
this.filePane = page.locator('#files-pane') this.filePane = page.locator('#files-pane')
this.featureTreePane = page.locator('#feature-tree-pane') this.featureTreePane = page.locator('#feature-tree-pane')
this.fileCreateToast = page.getByText('Successfully created') this.fileCreateToast = page.getByText('Successfully created')
// Note to test writers: having two locators like this is preferable to one
// which changes another el property because it means our test "signal" is
// completely decoupled from the elements themselves. It means the same
// element or two different elements can represent these states.
this.gizmo = page.getByTestId('gizmo')
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
} }
get editSketchBtn() { get editSketchBtn() {
@ -86,6 +92,18 @@ export class ToolbarFixture {
startSketchPlaneSelection = async () => startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
waitUntilSketchingReady = async () => {
await expect(this.gizmoDisabled).toBeVisible()
}
startSketchThenCallbackThenWaitUntilReady = async (
cb: () => Promise<void>
) => {
await this.startSketchBtn.click()
await cb()
await this.waitUntilSketchingReady()
}
exitSketch = async () => { exitSketch = async () => {
await this.exitSketchBtn.click() await this.exitSketchBtn.click()
await expect( await expect(

View File

@ -21,58 +21,54 @@ import { expectPixelColor } from './fixtures/sceneFixture'
// we must set it to empty for the tests where we want to see the onboarding immediately. // we must set it to empty for the tests where we want to see the onboarding immediately.
test.describe('Onboarding tests', () => { test.describe('Onboarding tests', () => {
test( test('Onboarding code is shown in the editor', async ({
'Onboarding code is shown in the editor', page,
{ homePage,
appSettings: { tronApp,
app: { }) => {
onboarding_status: '', if (!tronApp) {
}, fail()
},
cleanProjectDir: true,
},
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(
8
)
} }
) await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
})
test( test(
'Desktop: fresh onboarding executes and loads', 'Desktop: fresh onboarding executes and loads',
{ {
tag: '@electron', tag: '@electron',
appSettings: { },
async ({ page, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '', onboarding_status: '',
}, },
}, })
cleanProjectDir: true,
},
async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
@ -107,223 +103,235 @@ test.describe('Onboarding tests', () => {
} }
) )
test( test('Code resets after confirmation', async ({
'Code resets after confirmation', context,
{ page,
cleanProjectDir: true, homePage,
}, tronApp,
async ({ context, page, homePage }) => { scene,
const initialCode = `sketch001 = startSketchOn('XZ')` cmdBar,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir()
// Load the page up with some code so we see the confirmation warning const initialCode = `sketch001 = startSketchOn('XZ')`
// when we go to replay onboarding
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
await page.setBodyDimensions({ width: 1200, height: 500 }) // Load the page up with some code so we see the confirmation warning
await homePage.goToModelingScene() // when we go to replay onboarding
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
// Replay the onboarding await page.setBodyDimensions({ width: 1200, height: 500 })
await page.getByRole('link', { name: 'Settings' }).last().click() await homePage.goToModelingScene()
const replayButton = page.getByRole('button', { await scene.connectionEstablished()
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible()
await replayButton.click()
// Ensure we see the warning, and that the code has not yet updated // Replay the onboarding
await expect(page.getByText('Would you like to create')).toBeVisible() await page.getByRole('link', { name: 'Settings' }).last().click()
await expect(page.locator('.cm-content')).toHaveText(initialCode) const replayButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible()
await replayButton.click()
const nextButton = page.getByTestId('onboarding-next') // Ensure we see the warning, and that the code has not yet updated
await expect(page.getByText('Would you like to create')).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode)
const nextButton = page.getByTestId('onboarding-next')
await nextButton.hover()
await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// There used to be old code here that checked if we stored the reset
// code into localStorage but that isn't the case on desktop. It gets
// saved to the file system, which we have other tests for.
})
test('Click through each onboarding step and back', async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover() await nextButton.hover()
await nextButton.click() await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// There used to be old code here that checked if we stored the reset
// code into localStorage but that isn't the case on desktop. It gets
// saved to the file system, which we have other tests for.
} }
)
test( while ((await prevButton.innerText()) !== 'Dismiss') {
'Click through each onboarding step and back',
{
appSettings: {
app: {
onboarding_status: '',
},
},
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
}
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
}
// Dismiss the onboarding
await prevButton.hover() await prevButton.hover()
await prevButton.click() await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
} }
)
test( // Dismiss the onboarding
'Onboarding redirects and code updating', await prevButton.hover()
{ await prevButton.click()
appSettings: {
app: { // Test that the onboarding pane is gone
onboarding_status: '/export', await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
}, await expect.poll(() => page.url()).not.toContain('/onboarding')
})
test('Onboarding redirects and code updating', async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '/export',
}, },
cleanProjectDir: true, })
},
async ({ context, page, homePage }) => {
const originalCode = 'sigmaAllow = 15000'
// Override beforeEach test setup const originalCode = 'sigmaAllow = 15000'
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 500 }) // Override beforeEach test setup
await homePage.goToModelingScene() await context.addInitScript(
async ({ settingsKey, settings }) => {
// Test that the redirect happened // Give some initial code, so we can test that it's cleared
await expect.poll(() => page.url()).toContain('/onboarding/export') localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
}
)
test(
'Onboarding code gets reset to demo on Interactive Numbers step',
{
appSettings: {
app: {
onboarding_status: '/parametric-modeling',
},
}, },
cleanProjectDir: true, {
}, settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
async ({ page, homePage }) => { await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page) await homePage.goToModelingScene()
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 1080 }) // Test that the redirect happened
await homePage.goToModelingScene() await expect.poll(() => page.url()).toContain('/onboarding/export')
await expect // Test that you come back to this page when you refresh
.poll(() => page.url()) await page.reload()
.toContain(onboardingPaths.PARAMETRIC_MODELING) await expect.poll(() => page.url()).toContain('/onboarding/export')
const bracketNoNewLines = bracket.replace(/\n/g, '') // Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Check the code got reset on load // Test that the onboarding pane loaded
await expect(page.locator('#code-pane')).toBeVisible() const title = page.locator('[data-testid="onboarding-content"]')
await expect(u.codeLocator).toHaveText(bracketNoNewLines, { await expect(title).toBeAttached()
timeout: 10_000,
})
// Mess with the code again await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
// Click to the next step // Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover() await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click() await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, { await expect(page.locator('.cm-content')).toHaveText(/.+/)
waitUntil: 'domcontentloaded', })
})
// Check that the code has been reset test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
await expect(u.codeLocator).toHaveText(bracketNoNewLines) page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
} }
) await tronApp.cleanProjectDir({
app: {
onboarding_status: '/parametric-modeling',
},
})
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
})
// (lee) The two avatar tests are weird because even on main, we don't have // (lee) The two avatar tests are weird because even on main, we don't have
// anything to do with the avatar inside the onboarding test. Due to the // anything to do with the avatar inside the onboarding test. Due to the
// low impact of an avatar not showing I'm changing this to fixme. // low impact of an avatar not showing I'm changing this to fixme.
test.fixme( test.fixme(
'Avatar text updates depending on image load success', 'Avatar text updates depending on image load success',
{ async ({ context, page, homePage, tronApp }) => {
appSettings: { if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '', onboarding_status: '',
}, },
}, })
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup // Override beforeEach test setup
await context.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
@ -388,15 +396,16 @@ test.describe('Onboarding tests', () => {
test.fixme( test.fixme(
"Avatar text doesn't mention avatar when no avatar", "Avatar text doesn't mention avatar when no avatar",
{ async ({ context, page, homePage, tronApp }) => {
appSettings: { if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '', onboarding_status: '',
}, },
}, })
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup // Override beforeEach test setup
await context.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
@ -444,15 +453,17 @@ test.describe('Onboarding tests', () => {
test.fixme( test.fixme(
'Restarting onboarding on desktop takes one attempt', 'Restarting onboarding on desktop takes one attempt',
{ async ({ context, page, tronApp }) => {
appSettings: { if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: 'dismissed', onboarding_status: 'dismissed',
}, },
}, })
cleanProjectDir: true,
},
async ({ context, page }) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate') const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true }) await fsp.mkdir(routerTemplateDir, { recursive: true })

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { EditorFixture } from './fixtures/editorFixture' import { EditorFixture } from './fixtures/editorFixture'
import { SceneFixture } from './fixtures/sceneFixture' import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture' import { ToolbarFixture } from './fixtures/toolbarFixture'

View File

@ -163,7 +163,7 @@ test(
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(20)
}) })
await test.step('Clicking the logo takes us back to the projects page / home', async () => { await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -464,7 +464,11 @@ test.describe('Can export from electron app', () => {
test( test(
`Can export using ${method}`, `Can export using ${method}`,
{ tag: ['@electron', '@skipLocalEngine'] }, { tag: ['@electron', '@skipLocalEngine'] },
async ({ context, page }, testInfo) => { async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
@ -516,6 +520,7 @@ test.describe('Can export from electron app', () => {
storage: 'embedded', storage: 'embedded',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp.projectDirName,
page, page,
method method
) )
@ -523,7 +528,7 @@ test.describe('Can export from electron app', () => {
}) })
const filepath = path.resolve( const filepath = path.resolve(
getPlaywrightDownloadDir(page), getPlaywrightDownloadDir(tronApp.projectDirName),
'main.gltf' 'main.gltf'
) )
@ -781,6 +786,7 @@ test(
page.on('console', console.log) page.on('console', console.log)
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Loading your Projects...')).not.toBeVisible()
await expect(page.getByText('Your Projects')).toBeVisible() await expect(page.getByText('Your Projects')).toBeVisible()
await page.keyboard.press('Delete') await page.keyboard.press('Delete')
@ -858,7 +864,7 @@ test.describe(`Project management commands`, () => {
test( test(
`Delete from project page`, `Delete from project page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page, scene, cmdBar }, testInfo) => {
const projectName = `my_project_to_delete` const projectName = `my_project_to_delete`
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -887,6 +893,8 @@ test.describe(`Project management commands`, () => {
await projectHomeLink.click() await projectHomeLink.click()
await u.waitForPageLoad() await u.waitForPageLoad()
await scene.connectionEstablished()
await scene.settled(cmdBar)
}) })
await test.step(`Run delete command via command palette`, async () => { await test.step(`Run delete command via command palette`, async () => {
@ -909,7 +917,7 @@ test.describe(`Project management commands`, () => {
test( test(
`Rename from home page`, `Rename from home page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page, homePage }, testInfo) => {
const projectName = `my_project_to_rename` const projectName = `my_project_to_rename`
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -936,6 +944,7 @@ test.describe(`Project management commands`, () => {
await test.step(`Setup`, async () => { await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await homePage.projectsLoaded()
await expect(projectHomeLink).toBeVisible() await expect(projectHomeLink).toBeVisible()
}) })
@ -1682,7 +1691,11 @@ test(
test( test(
'You can change the root projects directory and nothing is lost', 'You can change the root projects directory and nothing is lost',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, electronApp }, testInfo) => { async ({ context, page, tronApp, homePage }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await Promise.all([ await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }), fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
@ -1712,6 +1725,8 @@ test(
await fsp.rm(newProjectDirName, { recursive: true }) await fsp.rm(newProjectDirName, { recursive: true })
} }
await homePage.projectsLoaded()
await test.step('We can change the root project directory', async () => { await test.step('We can change the root project directory', async () => {
// expect to see the project directory settings link // expect to see the project directory settings link
await expect( await expect(
@ -1725,7 +1740,7 @@ test(
.locator('section#projectDirectory input') .locator('section#projectDirectory input')
.inputValue() .inputValue()
const handleFile = electronApp?.evaluate( const handleFile = tronApp.electron.evaluate(
async ({ dialog }, filePaths) => { async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () => dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths }) Promise.resolve({ canceled: false, filePaths })
@ -1741,6 +1756,8 @@ test(
await page.getByTestId('settings-close-button').click() await page.getByTestId('settings-close-button').click()
await homePage.projectsLoaded()
await expect(page.getByText('No Projects found')).toBeVisible() await expect(page.getByText('No Projects found')).toBeVisible()
await createProject({ name: 'project-000', page, returnHome: true }) await createProject({ name: 'project-000', page, returnHome: true })
await expect( await expect(
@ -1755,7 +1772,7 @@ test(
await page.getByTestId('project-directory-settings-link').click() await page.getByTestId('project-directory-settings-link').click()
const handleFile = electronApp?.evaluate( const handleFile = tronApp.electron.evaluate(
async ({ dialog }, filePaths) => { async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () => dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths }) Promise.resolve({ canceled: false, filePaths })
@ -1767,6 +1784,7 @@ test(
await page.getByTestId('project-directory-button').click() await page.getByTestId('project-directory-button').click()
await handleFile await handleFile
await homePage.projectsLoaded()
await expect(page.locator('section#projectDirectory input')).toHaveValue( await expect(page.locator('section#projectDirectory input')).toHaveValue(
originalProjectDirName originalProjectDirName
) )
@ -2000,8 +2018,8 @@ test(
test( test(
'Settings persist across restarts', 'Settings persist across restarts',
{ tag: '@electron', cleanProjectDir: true }, { tag: '@electron' },
async ({ page }, testInfo) => { async ({ page, scene, cmdBar }, testInfo) => {
await test.step('We can change a user setting like theme', async () => { await test.step('We can change a user setting like theme', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
@ -2014,6 +2032,10 @@ test(
await expect(page.getByTestId('app-theme')).toHaveValue('dark') await expect(page.getByTestId('app-theme')).toHaveValue('dark')
await page.getByTestId('app-theme').selectOption('light') await page.getByTestId('app-theme').selectOption('light')
await expect(page.getByTestId('app-theme')).toHaveValue('light')
// Give time to system for writing to a persistent store
await page.waitForTimeout(1000)
}) })
await test.step('Starting the app again and we can see the same theme', async () => { await test.step('Starting the app again and we can see the same theme', async () => {

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import path from 'path' import path from 'path'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { getUtils, executorInputPath } from './test-utils' import { getUtils, executorInputPath } from './test-utils'

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { HomePageFixture } from './fixtures/homePageFixture' import { HomePageFixture } from './fixtures/homePageFixture'
@ -2153,6 +2154,8 @@ extrude001 = extrude(profile003, length = 5)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await page.waitForTimeout(5000)
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -2165,7 +2168,7 @@ extrude001 = extrude(profile003, length = 5)
await page.waitForTimeout(600) await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`) await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await toolbar.exitSketchBtn.click() await toolbar.exitSketch()
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`) await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`)
@ -2181,6 +2184,8 @@ extrude001 = extrude(profile003, length = 5)
)` )`
) )
await scene.settled(cmdBar)
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15) await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
}) })
}) })

View File

@ -31,8 +31,7 @@ test.beforeEach(async ({ page, context }) => {
// Help engine-manager: tear shit down. // Help engine-manager: tear shit down.
test.afterEach(async ({ page }) => { test.afterEach(async ({ page }) => {
await page.evaluate(() => { await page.evaluate(() => {
// @ts-expect-error window.engineCommandManager.tearDown()
window.tearDown()
}) })
}) })
@ -45,7 +44,11 @@ test.setTimeout(60_000)
test.skip( test.skip(
'exports of each format should work', 'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] }, { tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context, scene, cmdBar }) => { async ({ page, context, scene, cmdBar, tronApp }) => {
if (!tronApp) {
fail()
}
// FYI this test doesn't work with only engine running locally // FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed // And you will need to have the KittyCAD CLI installed
const u = await getUtils(page) const u = await getUtils(page)
@ -134,6 +137,7 @@ part001 = startSketchOn('-XZ')
storage: 'ascii', storage: 'ascii',
units: 'in', units: 'in',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -146,6 +150,7 @@ part001 = startSketchOn('-XZ')
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
units: 'in', units: 'in',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -158,6 +163,7 @@ part001 = startSketchOn('-XZ')
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
units: 'in', units: 'in',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -170,6 +176,7 @@ part001 = startSketchOn('-XZ')
units: 'in', units: 'in',
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
}, },
tronApp.projectDirName,
page page
) )
) )
@ -182,6 +189,7 @@ part001 = startSketchOn('-XZ')
units: 'in', units: 'in',
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
}, },
tronApp.projectDirName,
page page
) )
) )
@ -193,6 +201,7 @@ part001 = startSketchOn('-XZ')
coords: sysType, coords: sysType,
units: 'in', units: 'in',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -203,6 +212,7 @@ part001 = startSketchOn('-XZ')
storage: 'embedded', storage: 'embedded',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -213,6 +223,7 @@ part001 = startSketchOn('-XZ')
storage: 'binary', storage: 'binary',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -223,6 +234,7 @@ part001 = startSketchOn('-XZ')
storage: 'standard', storage: 'standard',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp.projectDirName,
page page
) )
) )

View File

@ -84,7 +84,7 @@ test.describe('Test network and connection issues', () => {
'Engine disconnect & reconnect in sketch mode', 'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' }, { tag: '@skipLocalEngine' },
async ({ page, homePage }) => { async ({ page, homePage }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit // TODO: Don't skip Mac for these. After `window.engineCommandManager.tearDown` is working in Safari, these should work on webkit
const networkToggle = page.getByTestId('network-toggle') const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page) const u = await getUtils(page)

View File

@ -5,8 +5,9 @@ import {
_electron as electron, _electron as electron,
ElectronApplication, ElectronApplication,
Locator, Locator,
Page,
} from '@playwright/test' } from '@playwright/test'
import { test, Page } from './zoo-test' import { test } from './zoo-test'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fsSync from 'fs' import fsSync from 'fs'
@ -337,7 +338,7 @@ export const getMovementUtils = (opts: any) => {
async function waitForAuthAndLsp(page: Page) { async function waitForAuthAndLsp(page: Page) {
const waitForLspPromise = page.waitForEvent('console', { const waitForLspPromise = page.waitForEvent('console', {
predicate: async (message) => { predicate: async (message: any) => {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :( // but that doesn't seem to make it to the console for macos/safari :(
if (message.text().includes('start kcl lsp')) { if (message.text().includes('start kcl lsp')) {
@ -420,7 +421,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
const overlay = page.locator(locator) const overlay = page.locator(locator)
const bbox = await overlay const bbox = await overlay
.boundingBox({ timeout: 5_000 }) .boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })) .then((box: any) => ({ ...box, x: box?.x || 0, y: box?.y || 0 }))
const angle = Number(await overlay.getAttribute('data-overlay-angle')) const angle = Number(await overlay.getAttribute('data-overlay-angle'))
const angleXOffset = Math.cos(((angle - 180) * Math.PI) / 180) * px const angleXOffset = Math.cos(((angle - 180) * Math.PI) / 180) * px
const angleYOffset = Math.sin(((angle - 180) * Math.PI) / 180) * px const angleYOffset = Math.sin(((angle - 180) * Math.PI) / 180) * px
@ -437,7 +438,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
page page
.locator(locator) .locator(locator)
.boundingBox({ timeout: 5_000 }) .boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })), .then((box: any) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
codeLocator: page.locator('.cm-content'), codeLocator: page.locator('.cm-content'),
crushKclCodeIntoOneLineAndThenMaybeSome: async () => { crushKclCodeIntoOneLineAndThenMaybeSome: async () => {
const code = await page.locator('.cm-content').innerText() const code = await page.locator('.cm-content').innerText()
@ -504,7 +505,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
) => { ) => {
if (cdpSession === null) { if (cdpSession === null) {
// Use a fail safe if we can't simulate disconnect (on Safari) // Use a fail safe if we can't simulate disconnect (on Safari)
return page.evaluate('window.tearDown()') return page.evaluate('window.engineCommandManager.tearDown()')
} }
return cdpSession?.send( return cdpSession?.send(
@ -631,7 +632,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
panesOpen: async (paneIds: PaneId[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(
({ PERSIST_MODELING_CONTEXT, paneIds }) => { ({ PERSIST_MODELING_CONTEXT, paneIds }: any) => {
localStorage.setItem( localStorage.setItem(
PERSIST_MODELING_CONTEXT, PERSIST_MODELING_CONTEXT,
JSON.stringify({ openPanes: paneIds }) JSON.stringify({ openPanes: paneIds })
@ -722,14 +723,14 @@ export const makeTemplate: (
const PLAYWRIGHT_DOWNLOAD_DIR = 'downloads-during-playwright' const PLAYWRIGHT_DOWNLOAD_DIR = 'downloads-during-playwright'
export const getPlaywrightDownloadDir = (page: Page) => { export const getPlaywrightDownloadDir = (rootDir: string) => {
return path.resolve(page.dir, PLAYWRIGHT_DOWNLOAD_DIR) return path.resolve(rootDir, PLAYWRIGHT_DOWNLOAD_DIR)
} }
const moveDownloadedFileTo = async (page: Page, toLocation: string) => { const moveDownloadedFileTo = async (rootDir: string, toLocation: string) => {
await fsp.mkdir(path.dirname(toLocation), { recursive: true }) await fsp.mkdir(path.dirname(toLocation), { recursive: true })
const downloadDir = getPlaywrightDownloadDir(page) const downloadDir = getPlaywrightDownloadDir(rootDir)
// Expect there to be at least one file // Expect there to be at least one file
await expect await expect
@ -756,6 +757,7 @@ export interface Paths {
export const doExport = async ( export const doExport = async (
output: Models['OutputFormat_type'], output: Models['OutputFormat_type'],
rootDir: string,
page: Page, page: Page,
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown' exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => { ): Promise<Paths> => {
@ -836,7 +838,7 @@ export const doExport = async (
// (declared in src/lib/exportSave) // (declared in src/lib/exportSave)
// To remain consistent with our old web tests, we want to move some downloads // To remain consistent with our old web tests, we want to move some downloads
// (images) to another directory. // (images) to another directory.
await moveDownloadedFileTo(page, downloadLocation) await moveDownloadedFileTo(rootDir, downloadLocation)
} }
return { return {
@ -859,12 +861,6 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
downloadThroughput: -1, downloadThroughput: -1,
uploadThroughput: -1, uploadThroughput: -1,
}) })
// It seems it's best to give the browser about 3s to close things
// It's not super reliable but we have no real other choice for now
await page.waitForTimeout(3000)
await testInfo.tronApp?.close()
} }
// settingsOverrides may need to be augmented to take more generic items, // settingsOverrides may need to be augmented to take more generic items,
@ -936,107 +932,11 @@ let electronApp: ElectronApplication | undefined = undefined
let context: BrowserContext | undefined = undefined let context: BrowserContext | undefined = undefined
let page: Page | undefined = undefined let page: Page | undefined = undefined
export async function setupElectron({
testInfo,
cleanProjectDir = true,
appSettings,
viewport,
}: {
testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
viewport: {
width: number
height: number
}
}): Promise<{
electronApp: ElectronApplication
context: BrowserContext
page: Page
dir: string
}> {
// create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
try {
if (fsSync.existsSync(projectDirName) && cleanProjectDir) {
await fsp.rm(projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
if (cleanProjectDir) {
await fsp.mkdir(projectDirName)
}
const options = {
args: ['.', '--no-sandbox'],
env: {
...process.env,
TEST_SETTINGS_FILE_KEY: projectDirName,
IS_PLAYWRIGHT: 'true',
},
...(process.env.ELECTRON_OVERRIDE_DIST_PATH
? { executablePath: process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron' }
: {}),
...(process.env.PLAYWRIGHT_RECORD_VIDEO
? {
recordVideo: {
dir: testInfo.snapshotPath(),
size: viewport,
},
}
: {}),
}
// Do this once and then reuse window on subsequent calls.
if (!electronApp) {
electronApp = await electron.launch(options)
}
if (!context || !page) {
context = electronApp.context()
page = await electronApp.firstWindow()
context.on('console', console.log)
page.on('console', console.log)
}
if (cleanProjectDir) {
const tempSettingsFilePath = path.join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = settingsToToml(
appSettings
? {
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
project_directory: projectDirName,
...appSettings.app,
},
},
}
: {
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
project_directory: projectDirName,
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
}
return { electronApp, page, context, dir: projectDirName }
}
function failOnConsoleErrors(page: Page, testInfo?: TestInfo) { function failOnConsoleErrors(page: Page, testInfo?: TestInfo) {
// enabled for chrome for now // enabled for chrome for now
if (page.context().browser()?.browserType().name() === 'chromium') { if (page.context().browser()?.browserType().name() === 'chromium') {
page.on('pageerror', (exception) => { // No idea wtf exception is
page.on('pageerror', (exception: any) => {
if (isErrorWhitelisted(exception)) { if (isErrorWhitelisted(exception)) {
return return
} }

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { deg, getUtils, wiggleMove } from './test-utils' import { deg, getUtils, wiggleMove } from './test-utils'
import { LineInputsType } from 'lang/std/sketchcombos' import { LineInputsType } from 'lang/std/sketchcombos'

View File

@ -20,35 +20,40 @@ import { DeepPartial } from 'lib/types'
import { Settings } from '@rust/kcl-lib/bindings/Settings' import { Settings } from '@rust/kcl-lib/bindings/Settings'
test.describe('Testing settings', () => { test.describe('Testing settings', () => {
test( test('Stored settings are validated and fall back to defaults', async ({
'Stored settings are validated and fall back to defaults', page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
// Override beforeEach test setup // Override beforeEach test setup
// with corrupted settings // with corrupted settings
{ await tronApp.cleanProjectDir(
appSettings: TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>, TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>
}, )
async ({ page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
// Check the settings were reset await page.setBodyDimensions({ width: 1200, height: 500 })
const storedSettings = tomlToSettings(
await page.evaluate( // Check the settings were reset
({ settingsKey }) => localStorage.getItem(settingsKey) || '', const storedSettings = tomlToSettings(
{ settingsKey: TEST_SETTINGS_KEY } await page.evaluate(
) ({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
) )
)
expect(storedSettings.settings?.app?.theme).toBe('dark') expect(storedSettings.settings?.app?.theme).toBe('dark')
// Check that the invalid settings were changed to good defaults // Check that the invalid settings were changed to good defaults
expect(storedSettings.settings?.modeling?.base_unit).toBe('in') expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
expect(storedSettings.settings?.modeling?.mouse_controls).toBe('zoo') expect(storedSettings.settings?.modeling?.mouse_controls).toBe('zoo')
expect(storedSettings.settings?.app?.project_directory).toBe('') expect(storedSettings.settings?.app?.project_directory).toBe('')
expect(storedSettings.settings?.project?.default_project_name).toBe( expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn' 'project-$nnn'
) )
} })
)
// The behavior is actually broken. Parent always takes precedence // The behavior is actually broken. Parent always takes precedence
test.fixme( test.fixme(
@ -357,8 +362,6 @@ test.describe('Testing settings', () => {
`Load desktop app with no settings file`, `Load desktop app with no settings file`,
{ {
tag: '@electron', tag: '@electron',
// This is what makes no settings file get created
cleanProjectDir: false,
}, },
async ({ page }, testInfo) => { async ({ page }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
@ -379,13 +382,17 @@ test.describe('Testing settings', () => {
`Load desktop app with a settings file, but no project directory setting`, `Load desktop app with a settings file, but no project directory setting`,
{ {
tag: '@electron', tag: '@electron',
appSettings: { },
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
theme_color: '259', theme_color: '259',
}, },
}, })
},
async ({ context, page }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
// Selectors and constants // Selectors and constants
@ -405,15 +412,20 @@ test.describe('Testing settings', () => {
'user settings reload on external change, on project and modeling view', 'user settings reload on external change, on project and modeling view',
{ {
tag: '@electron', tag: '@electron',
appSettings: { },
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
// Doesn't matter what you set it to. It will // Doesn't matter what you set it to. It will
// default to 264.5 // default to 264.5
theme_color: '0', theme_color: '0',
}, },
}, })
},
async ({ context, page }, testInfo) => {
const { dir: projectDirName } = await context.folderSetupFn( const { dir: projectDirName } = await context.folderSetupFn(
async () => {} async () => {}
) )
@ -783,128 +795,136 @@ test.describe('Testing settings', () => {
}) })
}) })
test( test(`Changing system theme preferences (via media query) should update UI and stream`, async ({
`Changing system theme preferences (via media query) should update UI and stream`, page,
{ homePage,
// Override the settings so that the theme is set to `system` tronApp,
appSettings: TEST_SETTINGS_DEFAULT_THEME, }) => {
}, if (!tronApp) {
async ({ page, homePage }) => { fail()
const u = await getUtils(page)
// Selectors and constants
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
) => {
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
}
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await expect(toolbar).toBeVisible()
})
await test.step(`Check the background color is light before`, async () => {
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
.toBeLessThan(15)
})
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
await page.emulateMedia({ colorScheme: 'dark' })
})
await test.step(`Check the background color is dark after`, async () => {
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15)
})
} }
)
test( await tronApp.cleanProjectDir({
`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, // Override the settings so that the theme is set to `system`
{ ...TEST_SETTINGS_DEFAULT_THEME,
})
const u = await getUtils(page)
// Selectors and constants
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
) => {
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
}
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await expect(toolbar).toBeVisible()
})
await test.step(`Check the background color is light before`, async () => {
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
.toBeLessThan(15)
})
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
await page.emulateMedia({ colorScheme: 'dark' })
})
await test.step(`Check the background color is dark after`, async () => {
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15)
})
})
test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
// Override beforeEach test setup // Override beforeEach test setup
// with debug panel open // with debug panel open
// but "show debug panel" set to false // but "show debug panel" set to false
appSettings: { ...TEST_SETTINGS,
...TEST_SETTINGS, app: { ...TEST_SETTINGS.app, show_debug_panel: false },
app: { ...TEST_SETTINGS.app, show_debug_panel: false }, modeling: { ...TEST_SETTINGS.modeling },
modeling: { ...TEST_SETTINGS.modeling }, })
},
},
async ({ context, page, homePage }) => {
const u = await getUtils(page)
await context.addInitScript(async () => { const u = await getUtils(page)
localStorage.setItem(
'persistModelingContext', await context.addInitScript(async () => {
'{"openPanes":["debug"]}' localStorage.setItem('persistModelingContext', '{"openPanes":["debug"]}')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · app · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
) )
}) ).toBeVisible()
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · app · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
)
).toBeVisible()
}
await test.step(`Initial load with corrupted settings`, async () => {
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
} }
)
await test.step(`Initial load with corrupted settings`, async () => {
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
})
test(`Change inline units setting`, async ({ test(`Change inline units setting`, async ({
page, page,

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { getUtils, createProject } from './test-utils' import { getUtils, createProject } from './test-utils'
import { join } from 'path' import { join } from 'path'
import fs from 'fs' import fs from 'fs'

View File

@ -35,7 +35,7 @@ test.fixme('Units menu', async ({ page, homePage }) => {
test( test(
'Successful export shows a success toast', 'Successful export shows a success toast',
{ tag: '@skipLocalEngine' }, { tag: '@skipLocalEngine' },
async ({ page, homePage }) => { async ({ page, homePage, tronApp }) => {
// FYI this test doesn't work with only engine running locally // FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed // And you will need to have the KittyCAD CLI installed
const u = await getUtils(page) const u = await getUtils(page)
@ -92,12 +92,17 @@ part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
if (!tronApp?.projectDirName) {
fail()
}
await doExport( await doExport(
{ {
type: 'gltf', type: 'gltf',
storage: 'embedded', storage: 'embedded',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp?.projectDirName,
page page
) )
} }
@ -465,7 +470,7 @@ test('Delete key does not navigate back', async ({ page, homePage }) => {
await expect.poll(() => page.url()).not.toContain('/settings') await expect.poll(() => page.url()).not.toContain('/settings')
}) })
test('Sketch on face', async ({ page, homePage, scene, cmdBar }) => { test('Sketch on face', async ({ page, homePage, scene, cmdBar, toolbar }) => {
test.setTimeout(90_000) test.setTimeout(90_000)
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -491,25 +496,22 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone() await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(300)
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = await page.locator('.cm-content').innerText()
await u.openAndClearDebugPanel() await toolbar.startSketchThenCallbackThenWaitUntilReady(async () => {
await u.doAndWaitForCmd( await u.openAndClearDebugPanel()
() => page.mouse.click(625, 165), await u.doAndWaitForCmd(
'default_camera_get_settings', () => page.mouse.click(625, 165),
true 'default_camera_get_settings',
) true
await page.waitForTimeout(150) )
await u.closeDebugPanel() await page.waitForTimeout(150)
await u.closeDebugPanel()
})
await page.waitForTimeout(300)
const firstClickPosition = [612, 238] const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242] const secondClickPosition = [661, 242]

View File

@ -1,21 +1,11 @@
import { /* eslint-disable react-hooks/rules-of-hooks */
test as playwrightTestFn,
TestInfo as TestInfoPlaywright, import { test as playwrightTestFn, ElectronApplication } from '@playwright/test'
BrowserContext as BrowserContextPlaywright,
Page as PagePlaywright,
TestDetails as TestDetailsPlaywright,
PlaywrightTestArgs,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
ElectronApplication,
} from '@playwright/test'
import { import {
fixtures, fixturesBasedOnProcessEnvPlatform,
Fixtures, Fixtures,
AuthenticatedTronApp, ElectronZoo,
AuthenticatedApp,
} from './fixtures/fixtureSetup' } from './fixtures/fixtureSetup'
import { Settings } from '@rust/kcl-lib/bindings/Settings' import { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -23,9 +13,6 @@ import { DeepPartial } from 'lib/types'
export { expect } from '@playwright/test' export { expect } from '@playwright/test'
declare module '@playwright/test' { declare module '@playwright/test' {
interface TestInfo {
tronApp?: AuthenticatedTronApp
}
interface BrowserContext { interface BrowserContext {
folderSetupFn: ( folderSetupFn: (
cb: (dir: string) => Promise<void> cb: (dir: string) => Promise<void>
@ -41,288 +28,29 @@ declare module '@playwright/test' {
} }
} }
export type TestInfo = TestInfoPlaywright // Each worker spawns a new thread, which will spawn its own ElectronZoo.
export type BrowserContext = BrowserContextPlaywright // So in some sense there is an implicit pool.
export type Page = PagePlaywright // For example, the variable just beneath this text is reused many times
export type TestDetails = TestDetailsPlaywright & { // *for one worker*.
cleanProjectDir?: boolean const electronZooInstance = new ElectronZoo()
appSettings?: DeepPartial<Settings>
}
// Our custom decorated Zoo test object. Makes it easier to add fixtures, and // Our custom decorated Zoo test object. Makes it easier to add fixtures, and
// switch between web and electron if needed. // switch between web and electron if needed.
const pwTestFnWithFixtures = playwrightTestFn.extend<Fixtures>(fixtures) const playwrightTestFnWithFixtures_ = playwrightTestFn.extend<{
tronApp?: ElectronZoo
// In JavaScript you cannot replace a function's body only (despite functions }>({
// are themselves objects, which you'd expect a body property or something...) tronApp: async ({}, use, testInfo) => {
// So we must redefine the function and then re-attach properties. if (process.env.PLATFORM === 'web') {
type PWFunction = ( await use(undefined)
args: PlaywrightTestArgs & return
Fixtures &
PlaywrightWorkerArgs &
PlaywrightTestOptions &
PlaywrightWorkerOptions & {
electronApp?: ElectronApplication
},
testInfo: TestInfo
) => void | Promise<void>
let firstUrl = ''
export const test = (
desc: string,
objOrFn: PWFunction | TestDetails,
fnMaybe?: PWFunction
) => {
const hasTestConf = typeof objOrFn === 'object'
const fn = hasTestConf ? fnMaybe : objOrFn
return pwTestFnWithFixtures(
desc,
hasTestConf ? objOrFn : {},
async (
{
page,
context,
cmdBar,
editor,
toolbar,
scene,
homePage,
request,
playwright,
browser,
acceptDownloads,
bypassCSP,
colorScheme,
clientCertificates,
deviceScaleFactor,
extraHTTPHeaders,
geolocation,
hasTouch,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
timezoneId,
userAgent,
viewport,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout,
serviceWorkers,
testIdAttribute,
browserName,
defaultBrowserType,
headless,
channel,
launchOptions,
connectOptions,
screenshot,
trace,
video,
},
testInfo
) => {
// To switch to web, use PLATFORM=web environment variable.
// Only use this for debugging, since the playwright tracer is busted
// for electron.
let tronApp
if (process.env.PLATFORM === 'web') {
tronApp = new AuthenticatedApp(context, page, testInfo)
} else {
tronApp = new AuthenticatedTronApp(context, page, testInfo)
}
const fixtures: Fixtures = { cmdBar, editor, toolbar, scene, homePage }
if (tronApp instanceof AuthenticatedTronApp) {
const options = {
fixtures,
}
if (hasTestConf) {
Object.assign(options, {
appSettings: objOrFn?.appSettings,
cleanProjectDir: objOrFn?.cleanProjectDir,
})
}
await tronApp.initialise(options)
} else {
await tronApp.initialise('')
}
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = tronApp.context.addInitScript
tronApp.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await tronApp.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = tronApp.page.addInitScript
tronApp.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await tronApp.page.reload()
}
// Create a consistent way to resize the page across electron and web.
// (lee) I had to do everything in the book to make electron change its
// damn window size. I succeeded in making it consistently and reliably
// do it after a whole afternoon.
tronApp.page.setBodyDimensions = async function (dims: {
width: number
height: number
}) {
await tronApp.page.setViewportSize(dims)
if (!(tronApp instanceof AuthenticatedTronApp)) {
return
}
await tronApp.electronApp?.evaluateHandle(async ({ app }, dims) => {
// @ts-ignore sorry jon but see comment in main.ts why this is ignored
await app.resizeWindow(dims.width, dims.height)
}, dims)
return tronApp.page.evaluate(
async (dims: { width: number; height: number }) => {
await window.electron.resizeWindow(dims.width, dims.height)
window.document.body.style.width = dims.width + 'px'
window.document.body.style.height = dims.height + 'px'
window.document.documentElement.style.width = dims.width + 'px'
window.document.documentElement.style.height = dims.height + 'px'
},
dims
)
}
await tronApp.page.setBodyDimensions(tronApp.viewPortSize)
// We need to expose this in order for some tests that require folder
// creation. Before they used to do this by their own electronSetup({...})
// calls.
if (tronApp instanceof AuthenticatedTronApp) {
tronApp.context.folderSetupFn = async function (fn) {
return fn(tronApp.dir)
.then(() => tronApp.page.reload())
.then(() => ({
dir: tronApp.dir,
}))
}
}
if (!firstUrl) {
await tronApp.page.getByText('Your Projects').count()
firstUrl = tronApp.page.url()
}
// Due to the app controlling its own window context we need to inject new
// options and context here.
// NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
// await tronApp.electronApp.evaluate(({ app }) => {
// return app.reuseWindowForTest();
// });
await tronApp.electronApp?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, tronApp.dir)
// Always start at the root view
await tronApp.page.goto(firstUrl)
// Force a hard reload, destroying the stream and other state
await tronApp.page.reload()
// tsc aint smart enough to know this'll never be undefined
// but I dont blame it, the logic to know is complex
if (fn) {
await fn(
{
context: tronApp.context,
page: tronApp.page,
electronApp:
tronApp instanceof AuthenticatedTronApp
? tronApp.electronApp
: undefined,
...fixtures,
request,
playwright,
browser,
acceptDownloads,
bypassCSP,
colorScheme,
clientCertificates,
deviceScaleFactor,
extraHTTPHeaders,
geolocation,
hasTouch,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
timezoneId,
userAgent,
viewport,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout,
serviceWorkers,
testIdAttribute,
browserName,
defaultBrowserType,
headless,
channel,
launchOptions,
connectOptions,
screenshot,
trace,
video,
},
testInfo
)
}
testInfo.tronApp =
tronApp instanceof AuthenticatedTronApp ? tronApp : undefined
} }
)
}
type ZooTest = typeof test await use(electronZooInstance)
},
})
test.describe = pwTestFnWithFixtures.describe const test = playwrightTestFnWithFixtures_.extend<Fixtures>(
test.beforeEach = pwTestFnWithFixtures.beforeEach fixturesBasedOnProcessEnvPlatform
test.afterEach = pwTestFnWithFixtures.afterEach )
test.step = pwTestFnWithFixtures.step
test.skip = pwTestFnWithFixtures.skip export { test }
test.setTimeout = pwTestFnWithFixtures.setTimeout
test.fixme = pwTestFnWithFixtures.fixme as unknown as ZooTest
test.only = pwTestFnWithFixtures.only
test.fail = pwTestFnWithFixtures.fail
test.slow = pwTestFnWithFixtures.slow
test.beforeAll = pwTestFnWithFixtures.beforeAll
test.afterAll = pwTestFnWithFixtures.afterAll
test.use = pwTestFnWithFixtures.use
test.expect = pwTestFnWithFixtures.expect
test.extend = pwTestFnWithFixtures.extend
test.info = pwTestFnWithFixtures.info

View File

@ -106,7 +106,7 @@
"files:flip-to-nightly:windows": "./scripts/flip-files-to-nightly.ps1", "files:flip-to-nightly:windows": "./scripts/flip-files-to-nightly.ps1",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh", "files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly", "files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"postinstall": "./node_modules/.bin/electron-rebuild", "postinstall": "yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install && ./node_modules/.bin/electron-rebuild",
"make:dev": "make dev", "make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"generate:samples-manifest": "cd public/kcl-samples && node generate-manifest.js", "generate:samples-manifest": "cd public/kcl-samples && node generate-manifest.js",

31
rust/Cargo.lock generated
View File

@ -1783,7 +1783,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.48" version = "0.1.49"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1794,7 +1794,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-derive-docs" name = "kcl-derive-docs"
version = "0.1.48" version = "0.1.49"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -1813,7 +1813,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
version = "0.1.48" version = "0.1.49"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1822,7 +1822,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server" name = "kcl-language-server"
version = "0.2.48" version = "0.2.49"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1843,7 +1843,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.48" version = "0.1.49"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1863,7 +1863,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.48" version = "0.2.49"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1931,7 +1931,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-python-bindings" name = "kcl-python-bindings"
version = "0.3.48" version = "0.3.49"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"kcl-lib", "kcl-lib",
@ -1946,7 +1946,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.48" version = "0.1.49"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper 0.14.32", "hyper 0.14.32",
@ -1959,7 +1959,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-to-core" name = "kcl-to-core"
version = "0.1.48" version = "0.1.49"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1973,7 +1973,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-wasm-lib" name = "kcl-wasm-lib"
version = "0.1.48" version = "0.1.49"
dependencies = [ dependencies = [
"bson", "bson",
"console_error_panic_hook", "console_error_panic_hook",
@ -3097,15 +3097,14 @@ checksum = "e6cd655523701785087f69900df39892fb7b9b0721aa67682f571c38c32ac58a"
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom 0.2.15", "getrandom 0.2.15",
"libc", "libc",
"spin",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -3560,12 +3559,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.48" version = "0.1.49"
edition = "2021" edition = "2021"
repository = "https://github.com/KittyCAD/modeling-api" repository = "https://github.com/KittyCAD/modeling-api"
rust-version = "1.76" rust-version = "1.76"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-derive-docs" name = "kcl-derive-docs"
description = "A tool for generating documentation from Rust derive macros" description = "A tool for generating documentation from Rust derive macros"
version = "0.1.48" version = "0.1.49"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files" description = "A tool for generating tests from a directory of kcl files"
version = "0.1.48" version = "0.1.49"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.48" version = "0.1.49"
edition = "2021" edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"] authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false publish = false

View File

@ -72,7 +72,7 @@ impl Build {
} }
fn build_client(sh: &Shell, version: &str, release_tag: &str, target: &Target) -> anyhow::Result<()> { fn build_client(sh: &Shell, version: &str, release_tag: &str, target: &Target) -> anyhow::Result<()> {
let bundle_path = Path::new("server"); let bundle_path = Path::new("kcl-language-server/server");
sh.create_dir(bundle_path)?; sh.create_dir(bundle_path)?;
sh.copy_file(&target.server_path, bundle_path)?; sh.copy_file(&target.server_path, bundle_path)?;
if let Some(symbols_path) = &target.symbols_path { if let Some(symbols_path) = &target.symbols_path {

View File

@ -2,7 +2,7 @@
name = "kcl-language-server" name = "kcl-language-server"
description = "A language server for KCL." description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"] authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.48" version = "0.2.49"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -97,7 +97,7 @@ async function getServer(
'You need to manually clone the kcl-lsp repository and ' + 'You need to manually clone the kcl-lsp repository and ' +
'run `cargo install` to build the language server from sources. ' + 'run `cargo install` to build the language server from sources. ' +
'If you feel that your platform should be supported, please create an issue ' + 'If you feel that your platform should be supported, please create an issue ' +
'about that [here](https://github.com/kittycad/kcl-lsp/issues) and we ' + 'about that [here](https://github.com/kittycad/modeling-app/issues) and we ' +
'will consider it.' 'will consider it.'
) )
return undefined return undefined

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.48" version = "0.2.49"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,8 +1,15 @@
const part001 = startSketchOn('XY') startProfileAt([0, 0], startSketchOn("XY"))
|> startProfileAt([0,0], %) |> xLine(length = 10, tag = $line000)
|> line(end = [0, 10], tag = $thing) |> yLine(length = 10, tag = $line001)
|> line(end = [10, 0]) |> xLine(endAbsolute = profileStartX(%), tag = $line002)
|> line(end = [0, -10], tag = $thing2) |> close(tag = $line003)
|> close() |> extrude(length = 10)
|> extrude(length = 10) |> fillet(
|> fillet(radius = 0.5, tags = [thing, thing]) radius = 1,
tags = [
line003,
getNextAdjacentEdge(line000),
getPreviousAdjacentEdge(line001)
],
)

View File

@ -27,11 +27,14 @@ async fn kcl_test_fillet_duplicate_tags() {
let code = kcl_input!("fillet_duplicate_tags"); let code = kcl_input!("fillet_duplicate_tags");
let result = execute_and_snapshot(code, UnitLength::Mm, None).await; let result = execute_and_snapshot(code, UnitLength::Mm, None).await;
assert!(result.is_err()); let err = result.expect_err("Code should have failed due to the duplicate edges being filletted");
let err = err.as_kcl_error().unwrap();
assert_eq!( assert_eq!(
result.err().unwrap().to_string(), err.message(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([229, 272, 0])], message: "Duplicate tags are not allowed." }"#, "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge"
); );
assert_eq!(err.source_ranges().len(), 2);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -857,7 +860,7 @@ part = rectShape([0, 0], 20, 20)
}; };
assert_eq!( assert_eq!(
err.error.message(), err.error.message(),
"This function expected this argument to be of type SketchOrSurface but it's actually of type string (text)" "This function expected the input argument to be of type SketchOrSurface but it's actually of type string (text)"
); );
} }
@ -2103,7 +2106,7 @@ async fn kcl_test_better_type_names() {
}, },
None => todo!(), None => todo!(),
}; };
assert_eq!(err, "This function expected this argument to be of type SolidSet but it's actually of type Sketch. You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`"); assert_eq!(err, "This function expected the input argument to be of type SolidSet but it's actually of type Sketch. You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`");
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]

View File

@ -48,6 +48,15 @@ impl ExecErrorWithState {
} }
} }
impl ExecError {
pub fn as_kcl_error(&self) -> Option<&crate::KclError> {
let ExecError::Kcl(k) = &self else {
return None;
};
Some(&k.error)
}
}
impl From<ExecError> for ExecErrorWithState { impl From<ExecError> for ExecErrorWithState {
fn from(error: ExecError) -> Self { fn from(error: ExecError) -> Self {
Self { Self {

View File

@ -596,12 +596,11 @@ impl ExecutorContext {
self.exec_module_for_result(module_id, exec_state, ExecutionKind::Normal, metadata.source_range) self.exec_module_for_result(module_id, exec_state, ExecutionKind::Normal, metadata.source_range)
.await? .await?
.unwrap_or_else(|| { .unwrap_or_else(|| {
// The module didn't have a return value. Currently, exec_state.warn(CompilationError::err(
// the only way to have a return value is with the final metadata.source_range,
// statement being an expression statement. "Imported module has no return value. The last statement of the module must be an expression, usually the Solid.",
// ));
// TODO: Make a warning when we support them in the
// execution phase.
let mut new_meta = vec![metadata.to_owned()]; let mut new_meta = vec![metadata.to_owned()];
new_meta.extend(meta); new_meta.extend(meta);
KclValue::KclNone { KclValue::KclNone {
@ -1187,7 +1186,7 @@ impl Node<CallExpressionKw> {
}, },
self.into(), self.into(),
ctx.clone(), ctx.clone(),
exec_state.mod_local.pipe_value.clone().map(Arg::synthetic), exec_state.mod_local.pipe_value.clone().map(|v| Arg::new(v, callsite)),
); );
match ctx.stdlib.get_either(fn_name) { match ctx.stdlib.get_either(fn_name) {
FunctionKind::Core(func) => { FunctionKind::Core(func) => {
@ -1349,7 +1348,7 @@ impl Node<CallExpression> {
fn_args, fn_args,
self.into(), self.into(),
ctx.clone(), ctx.clone(),
exec_state.mod_local.pipe_value.clone().map(Arg::synthetic), exec_state.mod_local.pipe_value.clone().map(|v| Arg::new(v, callsite)),
); );
let mut return_value = { let mut return_value = {
// Don't early-return in this block. // Don't early-return in this block.
@ -2000,7 +1999,11 @@ impl FunctionSource {
args, args,
source_range, source_range,
ctx.clone(), ctx.clone(),
exec_state.mod_local.pipe_value.clone().map(Arg::synthetic), exec_state
.mod_local
.pipe_value
.clone()
.map(|v| Arg::new(v, source_range)),
); );
func(exec_state, args).await.map(Some) func(exec_state, args).await.map(Some)

View File

@ -659,7 +659,11 @@ impl KclValue {
args, args,
source_range, source_range,
ctx.clone(), ctx.clone(),
exec_state.mod_local.pipe_value.clone().map(Arg::synthetic), exec_state
.mod_local
.pipe_value
.clone()
.map(|v| Arg::new(v, source_range)),
); );
let result = func(exec_state, args).await.map(Some); let result = func(exec_state, args).await.map(Some);
exec_state.mut_stack().pop_env(); exec_state.mut_stack().pop_env();

View File

@ -844,11 +844,23 @@ fn object_property(i: &mut TokenSlice) -> PResult<Node<ObjectProperty>> {
)) ))
.parse_next(i)?; .parse_next(i)?;
ignore_whitespace(i); ignore_whitespace(i);
let expr = expression let expr = match expression
.context(expected( .context(expected(
"the value which you're setting the property to, e.g. in 'height: 4', the value is 4", "the value which you're setting the property to, e.g. in 'height: 4', the value is 4",
)) ))
.parse_next(i)?; .parse_next(i)
{
Ok(expr) => expr,
Err(_) => {
return Err(ErrMode::Cut(
CompilationError::fatal(
SourceRange::from(sep),
"This property has a label, but no value. Put some value after the equals sign",
)
.into(),
));
}
};
let result = Node { let result = Node {
start: key.start, start: key.start,
@ -2810,7 +2822,7 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
ignore_whitespace(i); ignore_whitespace(i);
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum ArgPlace { enum ArgPlace {
NonCode(Node<NonCodeNode>), NonCode(Node<NonCodeNode>),
LabeledArg(LabeledArg), LabeledArg(LabeledArg),
UnlabeledArg(Expr), UnlabeledArg(Expr),
@ -2827,22 +2839,34 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
.parse_next(i)?; .parse_next(i)?;
let (args, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = args.into_iter().enumerate().try_fold( let (args, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = args.into_iter().enumerate().try_fold(
(Vec::new(), BTreeMap::new()), (Vec::new(), BTreeMap::new()),
|(mut args, mut non_code_nodes), (i, e)| { |(mut args, mut non_code_nodes), (index, e)| {
match e { match e {
ArgPlace::NonCode(x) => { ArgPlace::NonCode(x) => {
non_code_nodes.insert(i, vec![x]); non_code_nodes.insert(index, vec![x]);
} }
ArgPlace::LabeledArg(x) => { ArgPlace::LabeledArg(x) => {
args.push(x); args.push(x);
} }
ArgPlace::UnlabeledArg(arg) => { ArgPlace::UnlabeledArg(arg) => {
return Err(ErrMode::Cut( let followed_by_equals = peek((opt(whitespace), equals)).parse_next(i).is_ok();
CompilationError::fatal( let err = if followed_by_equals {
SourceRange::from(arg), ErrMode::Cut(
"This argument needs a label, but it doesn't have one", CompilationError::fatal(
SourceRange::from(arg),
"This argument has a label, but no value. Put some value after the equals sign",
)
.into(),
) )
.into(), } else {
)); ErrMode::Cut(
CompilationError::fatal(
SourceRange::from(arg),
"This argument needs a label, but it doesn't have one",
)
.into(),
)
};
return Err(err);
} }
} }
Ok((args, non_code_nodes)) Ok((args, non_code_nodes))
@ -4678,6 +4702,42 @@ baz = 2
); );
} }
} }
#[test]
fn test_sensible_error_when_missing_rhs_of_kw_arg() {
for (i, program) in ["f(x, y=)"].into_iter().enumerate() {
let tokens = crate::parsing::token::lex(program, ModuleId::default()).unwrap();
let err = fn_call_kw.parse(tokens.as_slice()).unwrap_err();
let cause = err.inner().cause.as_ref().unwrap();
assert_eq!(
cause.message, "This argument has a label, but no value. Put some value after the equals sign",
"failed test {i}: {program}"
);
assert_eq!(
cause.source_range.start(),
program.find("y").unwrap(),
"failed test {i}: {program}"
);
}
}
#[test]
fn test_sensible_error_when_missing_rhs_of_obj_property() {
for (i, program) in ["{x = 1, y =}"].into_iter().enumerate() {
let tokens = crate::parsing::token::lex(program, ModuleId::default()).unwrap();
let err = object.parse(tokens.as_slice()).unwrap_err();
let cause = err.inner().cause.as_ref().unwrap();
assert_eq!(
cause.message, "This property has a label, but no value. Put some value after the equals sign",
"failed test {i}: {program}"
);
assert_eq!(
cause.source_range.start(),
program.rfind('=').unwrap(),
"failed test {i}: {program}"
);
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -159,6 +159,49 @@ impl Args {
}) })
} }
/// Get a labelled keyword arg, check it's an array, and return all items in the array
/// plus their source range.
pub(crate) fn kw_arg_array_and_source<'a, T>(&'a self, label: &str) -> Result<Vec<(T, SourceRange)>, KclError>
where
T: FromKclValue<'a>,
{
let Some(arg) = self.kw_args.labeled.get(label) else {
let err = KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.source_range],
message: format!("This function requires a keyword argument '{label}'"),
});
return Err(err);
};
let Some(array) = arg.value.as_array() else {
let err = KclError::Semantic(KclErrorDetails {
source_ranges: vec![arg.source_range],
message: format!(
"Expected an array of {} but found {}",
type_name::<T>(),
arg.value.human_friendly_type()
),
});
return Err(err);
};
array
.iter()
.map(|item| {
let source = SourceRange::from(item);
let val = FromKclValue::from_kcl_val(item).ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
source_ranges: arg.source_ranges(),
message: format!(
"Expected a {} but found {}",
type_name::<T>(),
arg.value.human_friendly_type()
),
})
})?;
Ok((val, source))
})
.collect::<Result<Vec<_>, _>>()
}
/// Get the unlabeled keyword argument. If not set, returns None. /// Get the unlabeled keyword argument. If not set, returns None.
pub(crate) fn unlabeled_kw_arg_unconverted(&self) -> Option<&Arg> { pub(crate) fn unlabeled_kw_arg_unconverted(&self) -> Option<&Arg> {
self.kw_args self.kw_args
@ -184,7 +227,7 @@ impl Args {
T::from_kcl_val(&arg.value).ok_or_else(|| { T::from_kcl_val(&arg.value).ok_or_else(|| {
let expected_type_name = tynm::type_name::<T>(); let expected_type_name = tynm::type_name::<T>();
let actual_type_name = arg.value.human_friendly_type(); let actual_type_name = arg.value.human_friendly_type();
let msg_base = format!("This function expected this argument to be of type {expected_type_name} but it's actually of type {actual_type_name}"); let msg_base = format!("This function expected the input argument to be of type {expected_type_name} but it's actually of type {actual_type_name}");
let suggestion = match (expected_type_name.as_str(), actual_type_name) { let suggestion = match (expected_type_name.as_str(), actual_type_name) {
("SolidSet", "Sketch") => Some( ("SolidSet", "Sketch") => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`", "You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",

View File

@ -5,7 +5,6 @@ use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd}; use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds as kcmc;
use super::utils::unique_count;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid}, execution::{ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid},
@ -19,9 +18,11 @@ pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?; let solid = args.get_unlabeled_kw_arg("solid")?;
let length = args.get_kw_arg("length")?; let length = args.get_kw_arg("length")?;
let tags = args.get_kw_arg("tags")?; let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
let tag = args.get_kw_arg_opt("tag")?; let tag = args.get_kw_arg_opt("tag")?;
super::fillet::validate_unique(&tags)?;
let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
let value = inner_chamfer(solid, length, tags, tag, exec_state, args).await?; let value = inner_chamfer(solid, length, tags, tag, exec_state, args).await?;
Ok(KclValue::Solid { value }) Ok(KclValue::Solid { value })
} }
@ -109,15 +110,6 @@ async fn inner_chamfer(
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Box<Solid>, KclError> { ) -> Result<Box<Solid>, KclError> {
// Check if tags contains any duplicate values.
let unique_tags = unique_count(tags.clone());
if unique_tags != tags.len() {
return Err(KclError::Type(KclErrorDetails {
message: "Duplicate tags are not allowed.".to_string(),
source_ranges: vec![args.source_range],
}));
}
// If you try and tag multiple edges with a tagged chamfer, we want to return an // If you try and tag multiple edges with a tagged chamfer, we want to return an
// error to the user that they can only tag one edge at a time. // error to the user that they can only tag one edge at a time.
if tag.is_some() && tags.len() > 1 { if tag.is_some() && tags.len() > 1 {

View File

@ -1,6 +1,7 @@
//! Standard library fillets. //! Standard library fillets.
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap;
use kcl_derive_docs::stdlib; use kcl_derive_docs::stdlib;
use kcmc::{ use kcmc::{
each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::CutType, each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::CutType,
@ -11,13 +12,13 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use super::utils::unique_count;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier}, execution::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
settings::types::UnitLength, settings::types::UnitLength,
std::Args, std::Args,
SourceRange,
}; };
/// A tag or a uuid of an edge. /// A tag or a uuid of an edge.
@ -40,13 +41,39 @@ impl EdgeReference {
} }
} }
pub(super) fn validate_unique<T: Eq + std::hash::Hash>(tags: &[(T, SourceRange)]) -> Result<(), KclError> {
// Check if tags contains any duplicate values.
let mut tag_counts: IndexMap<&T, Vec<SourceRange>> = Default::default();
for tag in tags {
tag_counts.entry(&tag.0).or_insert(Vec::new()).push(tag.1);
}
let mut duplicate_tags_source = Vec::new();
for (_tag, count) in tag_counts {
if count.len() > 1 {
duplicate_tags_source.extend(count)
}
}
if !duplicate_tags_source.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge".to_string(),
source_ranges: duplicate_tags_source,
}));
}
Ok(())
}
/// Create fillets on tagged paths. /// Create fillets on tagged paths.
pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
// Get all args:
let solid = args.get_unlabeled_kw_arg("solid")?; let solid = args.get_unlabeled_kw_arg("solid")?;
let radius = args.get_kw_arg("radius")?; let radius = args.get_kw_arg("radius")?;
let tolerance = args.get_kw_arg_opt("tolerance")?; let tolerance = args.get_kw_arg_opt("tolerance")?;
let tags = args.get_kw_arg("tags")?; let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
let tag = args.get_kw_arg_opt("tag")?; let tag = args.get_kw_arg_opt("tag")?;
// Run the function.
validate_unique(&tags)?;
let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?; let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
Ok(KclValue::Solid { value }) Ok(KclValue::Solid { value })
} }
@ -129,15 +156,6 @@ async fn inner_fillet(
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Box<Solid>, KclError> { ) -> Result<Box<Solid>, KclError> {
// Check if tags contains any duplicate values.
let unique_tags = unique_count(tags.clone());
if unique_tags != tags.len() {
return Err(KclError::Type(KclErrorDetails {
message: "Duplicate tags are not allowed.".to_string(),
source_ranges: vec![args.source_range],
}));
}
let mut solid = solid.clone(); let mut solid = solid.clone();
for edge_tag in tags { for edge_tag in tags {
let edge_id = edge_tag.get_engine_id(exec_state, &args)?; let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
@ -432,3 +450,22 @@ pub(crate) fn default_tolerance(units: &UnitLength) -> f64 {
UnitLength::M => 0.001, UnitLength::M => 0.001,
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_unique() {
let dup_a = SourceRange::from([1, 3, 0]);
let dup_b = SourceRange::from([10, 30, 0]);
// Two entries are duplicates (abc) with different source ranges.
let tags = vec![("abc", dup_a), ("abc", dup_b), ("def", SourceRange::from([2, 4, 0]))];
let actual = validate_unique(&tags);
// Both the duplicates should show up as errors, with both of the
// source ranges they correspond to.
// But the unique source range 'def' should not.
let expected = vec![dup_a, dup_b];
assert_eq!(actual.err().unwrap().source_ranges(), expected);
}
}

View File

@ -1,4 +1,4 @@
use std::{collections::HashSet, f64::consts::PI}; use std::f64::consts::PI;
use kittycad_modeling_cmds::shared::Angle; use kittycad_modeling_cmds::shared::Angle;
@ -8,16 +8,6 @@ use crate::{
source_range::SourceRange, source_range::SourceRange,
}; };
/// Count the number of unique items in a `Vec` in O(n) time.
pub(crate) fn unique_count<T: Eq + std::hash::Hash>(vec: Vec<T>) -> usize {
// Add to a set.
let mut set = HashSet::with_capacity(vec.len());
for item in vec {
set.insert(item);
}
set.len()
}
/// Get the distance between two points. /// Get the distance between two points.
pub fn distance(a: Point2d, b: Point2d) -> f64 { pub fn distance(a: Point2d, b: Point2d) -> f64 {
((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt() ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt()
@ -686,11 +676,6 @@ mod get_tangential_arc_to_info_tests {
(num * 1000.0).round() / 1000.0 (num * 1000.0).round() / 1000.0
} }
#[test]
fn test_unique_count() {
assert_eq!(unique_count(vec![1, 2, 2, 3, 2]), 3);
}
#[test] #[test]
fn test_basic_case() { fn test_basic_case() {
let result = get_tangential_arc_to_info(TangentialArcInfoInput { let result = get_tangential_arc_to_info(TangentialArcInfoInput {

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed angled_line.kcl description: Operations executed angled_line.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed angled_line.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 270,
0, 289,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed artifact_graph_example_code1.kcl description: Operations executed artifact_graph_example_code1.kcl
--- ---
[ [
@ -125,8 +125,8 @@ description: Operations executed artifact_graph_example_code1.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 298,
0, 332,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed basic_fillet_cube_close_opposite.kcl description: Operations executed basic_fillet_cube_close_opposite.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed basic_fillet_cube_close_opposite.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 197,
0, 217,
0 0
] ]
} }
@ -129,8 +129,8 @@ description: Operations executed basic_fillet_cube_close_opposite.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 223,
0, 283,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed basic_fillet_cube_end.kcl description: Operations executed basic_fillet_cube_end.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed basic_fillet_cube_end.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 185,
0, 205,
0 0
] ]
} }
@ -129,8 +129,8 @@ description: Operations executed basic_fillet_cube_end.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 211,
0, 269,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed basic_fillet_cube_next_adjacent.kcl description: Operations executed basic_fillet_cube_next_adjacent.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed basic_fillet_cube_next_adjacent.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 212,
0, 232,
0 0
] ]
} }
@ -124,8 +124,8 @@ description: Operations executed basic_fillet_cube_next_adjacent.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 238,
0, 294,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed basic_fillet_cube_previous_adjacent.kcl description: Operations executed basic_fillet_cube_previous_adjacent.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed basic_fillet_cube_previous_adjacent.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 212,
0, 232,
0 0
] ]
} }
@ -124,8 +124,8 @@ description: Operations executed basic_fillet_cube_previous_adjacent.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 238,
0, 298,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed basic_fillet_cube_start.kcl description: Operations executed basic_fillet_cube_start.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed basic_fillet_cube_start.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 185,
0, 205,
0 0
] ]
} }
@ -130,8 +130,8 @@ description: Operations executed basic_fillet_cube_start.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 211,
0, 253,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed big_number_angle_to_match_length_x.kcl description: Operations executed big_number_angle_to_match_length_x.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed big_number_angle_to_match_length_x.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 183,
0, 203,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed big_number_angle_to_match_length_y.kcl description: Operations executed big_number_angle_to_match_length_y.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed big_number_angle_to_match_length_y.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 183,
0, 203,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed circle_three_point.kcl description: Operations executed circle_three_point.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed circle_three_point.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 104,
0, 124,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed circular_pattern3d_a_pattern.kcl description: Operations executed circular_pattern3d_a_pattern.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed circular_pattern3d_a_pattern.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 159,
0, 178,
0 0
] ]
} }

View File

@ -139,8 +139,8 @@ description: Operations executed cube.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 374,
0, 402,
0 0
] ]
} }

View File

@ -80,8 +80,8 @@ description: Operations executed cube_with_error.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 366,
0, 390,
0 0
] ]
} }

View File

@ -87,8 +87,8 @@ description: Operations executed fillet-and-shell.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1057,
0, 1085,
0 0
] ]
} }
@ -159,8 +159,8 @@ description: Operations executed fillet-and-shell.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1091,
0, 1297,
0 0
] ]
} }
@ -280,8 +280,8 @@ description: Operations executed fillet-and-shell.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1497,
0, 1521,
0 0
] ]
} }
@ -404,8 +404,8 @@ description: Operations executed fillet-and-shell.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1497,
0, 1521,
0 0
] ]
} }
@ -528,8 +528,8 @@ description: Operations executed fillet-and-shell.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1497,
0, 1521,
0 0
] ]
} }
@ -652,8 +652,8 @@ description: Operations executed fillet-and-shell.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1497,
0, 1521,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed function_sketch.kcl description: Operations executed function_sketch.kcl
--- ---
[ [
@ -80,8 +80,8 @@ description: Operations executed function_sketch.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 183,
0, 202,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed function_sketch_with_position.kcl description: Operations executed function_sketch_with_position.kcl
--- ---
[ [
@ -80,8 +80,8 @@ description: Operations executed function_sketch_with_position.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 181,
0, 200,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed helix_ccw.kcl description: Operations executed helix_ccw.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed helix_ccw.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 77,
0, 97,
0 0
] ]
} }

View File

@ -125,8 +125,8 @@ description: Operations executed i_shape.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2510,
0, 2531,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed import_whole.kcl description: Operations executed import_whole.kcl
--- ---
[ [
@ -64,9 +64,9 @@ description: Operations executed import_whole.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 103,
0, 123,
0 3
] ]
} }
}, },
@ -124,8 +124,8 @@ description: Operations executed import_whole.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 83,
0, 123,
0 0
] ]
} }

View File

@ -93,9 +93,9 @@ description: Operations executed 3d-boaty.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1379,
0, 1417,
0 3
] ]
} }
}, },
@ -173,9 +173,9 @@ description: Operations executed 3d-boaty.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1455,
0, 1494,
0 3
] ]
} }
}, },
@ -428,9 +428,9 @@ description: Operations executed 3d-boaty.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1379,
0, 1417,
0 3
] ]
} }
}, },
@ -508,9 +508,9 @@ description: Operations executed 3d-boaty.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1455,
0, 1494,
0 3
] ]
} }
}, },
@ -763,9 +763,9 @@ description: Operations executed 3d-boaty.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1379,
0, 1417,
0 3
] ]
} }
}, },
@ -843,9 +843,9 @@ description: Operations executed 3d-boaty.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1455,
0, 1494,
0 3
] ]
} }
}, },
@ -1104,9 +1104,9 @@ description: Operations executed 3d-boaty.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1949,
0, 1973,
0 3
] ]
} }
}, },
@ -1190,9 +1190,9 @@ description: Operations executed 3d-boaty.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2015,
0, 2039,
0 3
] ]
} }
}, },
@ -1339,9 +1339,9 @@ description: Operations executed 3d-boaty.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 2523,
0, 2547,
0 3
] ]
} }
}, },
@ -1485,9 +1485,9 @@ description: Operations executed 3d-boaty.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 3047,
0, 3071,
0 3
] ]
} }
}, },

View File

@ -118,8 +118,8 @@ description: Operations executed 80-20-rail.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 6006,
0, 6034,
0 0
] ]
} }
@ -238,8 +238,8 @@ description: Operations executed 80-20-rail.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 6042,
0, 6746,
0 0
] ]
} }
@ -358,8 +358,8 @@ description: Operations executed 80-20-rail.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 6754,
0, 7457,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed a-parametric-bearing-pillow-block.kcl description: Operations executed a-parametric-bearing-pillow-block.kcl
--- ---
[ [
@ -254,8 +254,8 @@ description: Operations executed a-parametric-bearing-pillow-block.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1902,
0, 1936,
0 0
] ]
} }
@ -693,8 +693,8 @@ description: Operations executed a-parametric-bearing-pillow-block.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3383,
0, 3408,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed ball-bearing.kcl description: Operations executed ball-bearing.kcl
--- ---
[ [
@ -519,8 +519,8 @@ description: Operations executed ball-bearing.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1561,
0, 1721,
0 0
] ]
} }
@ -779,8 +779,8 @@ description: Operations executed ball-bearing.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2214,
0, 2374,
0 0
] ]
} }
@ -1027,8 +1027,8 @@ description: Operations executed ball-bearing.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2718,
0, 2878,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed bracket.kcl description: Operations executed bracket.kcl
--- ---
[ [
@ -274,8 +274,8 @@ description: Operations executed bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1903,
0, 2052,
0 0
] ]
} }
@ -724,8 +724,8 @@ description: Operations executed bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3306,
0, 3455,
0 0
] ]
} }

View File

@ -1510,9 +1510,9 @@ description: Operations executed car-wheel-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 529,
0, 562,
0 3
] ]
} }
}, },
@ -1622,9 +1622,9 @@ description: Operations executed car-wheel-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 859,
0, 892,
0 3
] ]
} }
}, },
@ -1710,9 +1710,9 @@ description: Operations executed car-wheel-assembly.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 1214,
0, 1248,
0 3
] ]
} }
}, },
@ -1798,9 +1798,9 @@ description: Operations executed car-wheel-assembly.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 1572,
0, 1606,
0 3
] ]
} }
}, },
@ -2362,9 +2362,9 @@ description: Operations executed car-wheel-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4067,
0, 4247,
0 3
] ]
} }
}, },
@ -2801,9 +2801,9 @@ description: Operations executed car-wheel-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4067,
0, 4247,
0 3
] ]
} }
}, },
@ -3298,8 +3298,8 @@ description: Operations executed car-wheel-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 373,
0, 524,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed enclosure.kcl description: Operations executed enclosure.kcl
--- ---
[ [
@ -130,8 +130,8 @@ description: Operations executed enclosure.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 740,
0, 1021,
0 0
] ]
} }
@ -190,8 +190,8 @@ description: Operations executed enclosure.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1100,
0, 1170,
0 0
] ]
} }
@ -1699,8 +1699,8 @@ description: Operations executed enclosure.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3601,
0, 3882,
0 0
] ]
} }
@ -1997,8 +1997,8 @@ description: Operations executed enclosure.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 5327,
0, 5608,
0 0
] ]
} }

View File

@ -338,8 +338,8 @@ description: Operations executed exhaust-manifold.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1547,
0, 1570,
0 0
] ]
} }
@ -682,8 +682,8 @@ description: Operations executed exhaust-manifold.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1547,
0, 1570,
0 0
] ]
} }
@ -1026,8 +1026,8 @@ description: Operations executed exhaust-manifold.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1547,
0, 1570,
0 0
] ]
} }
@ -1370,8 +1370,8 @@ description: Operations executed exhaust-manifold.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1547,
0, 1570,
0 0
] ]
} }
@ -1744,8 +1744,8 @@ description: Operations executed exhaust-manifold.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3933,
0, 3962,
0 0
] ]
} }
@ -1808,8 +1808,8 @@ description: Operations executed exhaust-manifold.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3968,
0, 4101,
0 0
] ]
} }
@ -1872,8 +1872,8 @@ description: Operations executed exhaust-manifold.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4107,
0, 4240,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed flange-with-patterns.kcl description: Operations executed flange-with-patterns.kcl
--- ---
[ [
@ -174,8 +174,8 @@ description: Operations executed flange-with-patterns.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1413,
0, 1444,
0 0
] ]
} }
@ -461,8 +461,8 @@ description: Operations executed flange-with-patterns.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1928,
0, 1963,
0 0
] ]
} }
@ -566,8 +566,8 @@ description: Operations executed flange-with-patterns.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2230,
0, 2264,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed flange-xy.kcl description: Operations executed flange-xy.kcl
--- ---
[ [
@ -254,8 +254,8 @@ description: Operations executed flange-xy.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1555,
0, 1586,
0 0
] ]
} }
@ -552,8 +552,8 @@ description: Operations executed flange-xy.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2077,
0, 2112,
0 0
] ]
} }
@ -657,8 +657,8 @@ description: Operations executed flange-xy.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2379,
0, 2413,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed focusrite-scarlett-mounting-bracket.kcl description: Operations executed focusrite-scarlett-mounting-bracket.kcl
--- ---
[ [
@ -253,8 +253,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1831,
0, 1865,
0 0
] ]
} }
@ -325,8 +325,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1871,
0, 2129,
0 0
] ]
} }
@ -612,8 +612,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2875,
0, 2900,
0 0
] ]
} }
@ -670,8 +670,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2906,
0, 3050,
0 0
] ]
} }
@ -779,8 +779,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3056,
0, 3184,
0 0
] ]
} }
@ -1066,8 +1066,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3719,
0, 3744,
0 0
] ]
} }
@ -1124,8 +1124,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3750,
0, 3894,
0 0
] ]
} }
@ -1233,8 +1233,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3900,
0, 4028,
0 0
] ]
} }
@ -1476,8 +1476,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4458,
0, 4486,
0 0
] ]
} }
@ -1719,8 +1719,8 @@ description: Operations executed focusrite-scarlett-mounting-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4706,
0, 4734,
0 0
] ]
} }

View File

@ -323,8 +323,8 @@ description: Operations executed french-press.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2157,
0, 2179,
0 0
] ]
} }
@ -500,8 +500,8 @@ description: Operations executed french-press.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2185,
0, 2340,
0 0
] ]
} }
@ -1293,8 +1293,8 @@ description: Operations executed french-press.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 5053,
0, 5092,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed gear-rack.kcl description: Operations executed gear-rack.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed gear-rack.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 731,
0, 754,
0 0
] ]
} }
@ -147,8 +147,8 @@ description: Operations executed gear-rack.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1279,
0, 1302,
0 0
] ]
} }
@ -265,8 +265,8 @@ description: Operations executed gear-rack.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1409,
0, 1508,
0 0
] ]
} }
@ -332,8 +332,8 @@ description: Operations executed gear-rack.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1820,
0, 1843,
0 0
] ]
} }
@ -399,8 +399,8 @@ description: Operations executed gear-rack.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2157,
0, 2180,
0 0
] ]
} }

View File

@ -5897,8 +5897,8 @@ description: Operations executed gear.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1405,
0, 1433,
0 0
] ]
} }
@ -9802,8 +9802,8 @@ description: Operations executed gear.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2128,
0, 2156,
0 0
] ]
} }
@ -9979,8 +9979,8 @@ description: Operations executed gear.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2162,
0, 2322,
0 0
] ]
} }
@ -10097,8 +10097,8 @@ description: Operations executed gear.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3058,
0, 3087,
0 0
] ]
} }

View File

@ -919,8 +919,8 @@ description: Operations executed gridfinity-baseplate-magnets.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 2243,
0, 2360,
0 0
] ]
} }
@ -1183,8 +1183,8 @@ description: Operations executed gridfinity-baseplate-magnets.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 2584,
0, 2701,
0 0
] ]
} }
@ -2008,8 +2008,8 @@ description: Operations executed gridfinity-baseplate-magnets.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 6613,
0, 6730,
0 0
] ]
} }
@ -2243,8 +2243,8 @@ description: Operations executed gridfinity-baseplate-magnets.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 6933,
0, 7050,
0 0
] ]
} }

View File

@ -919,8 +919,8 @@ description: Operations executed gridfinity-baseplate.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 2118,
0, 2235,
0 0
] ]
} }
@ -1183,8 +1183,8 @@ description: Operations executed gridfinity-baseplate.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 2459,
0, 2576,
0 0
] ]
} }

View File

@ -722,8 +722,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2875,
0, 2899,
0 0
] ]
} }
@ -794,8 +794,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2905,
0, 3134,
0 0
] ]
} }
@ -885,8 +885,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 3580,
0, 3607,
0 0
] ]
} }
@ -1137,8 +1137,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 3813,
0, 3943,
0 0
] ]
} }
@ -1389,8 +1389,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 4174,
0, 4304,
0 0
] ]
} }
@ -1612,8 +1612,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 4529,
0, 4659,
0 0
] ]
} }
@ -1715,8 +1715,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 5002,
0, 5046,
0 0
] ]
} }
@ -1787,8 +1787,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 5052,
0, 5284,
0 0
] ]
} }
@ -1847,8 +1847,8 @@ description: Operations executed gridfinity-bins-stacking-lip.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 5290,
0, 5332,
0 0
] ]
} }

View File

@ -722,8 +722,8 @@ description: Operations executed gridfinity-bins.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2618,
0, 2642,
0 0
] ]
} }
@ -794,8 +794,8 @@ description: Operations executed gridfinity-bins.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2648,
0, 2877,
0 0
] ]
} }
@ -885,8 +885,8 @@ description: Operations executed gridfinity-bins.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 3323,
0, 3350,
0 0
] ]
} }
@ -1137,8 +1137,8 @@ description: Operations executed gridfinity-bins.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 3556,
0, 3686,
0 0
] ]
} }
@ -1389,8 +1389,8 @@ description: Operations executed gridfinity-bins.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 3917,
0, 4047,
0 0
] ]
} }
@ -1612,8 +1612,8 @@ description: Operations executed gridfinity-bins.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 4272,
0, 4402,
0 0
] ]
} }
@ -1715,8 +1715,8 @@ description: Operations executed gridfinity-bins.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4771,
0, 4815,
0 0
] ]
} }
@ -1787,8 +1787,8 @@ description: Operations executed gridfinity-bins.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4821,
0, 5053,
0 0
] ]
} }
@ -1847,8 +1847,8 @@ description: Operations executed gridfinity-bins.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 5059,
0, 5101,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed hex-nut.kcl description: Operations executed hex-nut.kcl
--- ---
[ [
@ -118,8 +118,8 @@ description: Operations executed hex-nut.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1038,
0, 1059,
0 0
] ]
} }

View File

@ -66,8 +66,8 @@ description: Operations executed i-beam.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 652,
0, 680,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed kitt.kcl description: Operations executed kitt.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 900,
0, 930,
0 0
] ]
} }
@ -160,8 +160,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -243,8 +243,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2204,
0, 2235,
0 0
] ]
} }
@ -339,8 +339,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -438,8 +438,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -537,8 +537,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -636,8 +636,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -719,8 +719,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3485,
0, 3514,
0 0
] ]
} }
@ -815,8 +815,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -914,8 +914,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1013,8 +1013,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1112,8 +1112,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1211,8 +1211,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1310,8 +1310,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1409,8 +1409,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1508,8 +1508,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1607,8 +1607,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1706,8 +1706,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1805,8 +1805,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -1904,8 +1904,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -2003,8 +2003,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -2185,8 +2185,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -2370,8 +2370,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -2489,8 +2489,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -2588,8 +2588,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -2687,8 +2687,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -2786,8 +2786,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -2905,8 +2905,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3004,8 +3004,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3103,8 +3103,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3202,8 +3202,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3305,8 +3305,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3405,8 +3405,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3505,8 +3505,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3605,8 +3605,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3705,8 +3705,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3805,8 +3805,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -3905,8 +3905,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -4005,8 +4005,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -4105,8 +4105,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }
@ -4205,8 +4205,8 @@ description: Operations executed kitt.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 449,
0, 472,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed lego.kcl description: Operations executed lego.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed lego.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1780,
0, 1804,
0 0
] ]
} }
@ -138,8 +138,8 @@ description: Operations executed lego.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2221,
0, 2252,
0 0
] ]
} }
@ -262,8 +262,8 @@ description: Operations executed lego.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 2687,
0, 2715,
0 0
] ]
} }
@ -528,8 +528,8 @@ description: Operations executed lego.kcl
] ]
}, },
"sourceRange": [ "sourceRange": [
0, 3197,
0, 3226,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed mounting-plate.kcl description: Operations executed mounting-plate.kcl
--- ---
[ [
@ -273,8 +273,8 @@ description: Operations executed mounting-plate.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1825,
0, 1857,
0 0
] ]
} }
@ -345,8 +345,8 @@ description: Operations executed mounting-plate.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1863,
0, 2127,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed multi-axis-robot.kcl description: Operations executed multi-axis-robot.kcl
--- ---
[ [
@ -212,9 +212,9 @@ description: Operations executed multi-axis-robot.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 769,
0, 1045,
0 3
] ]
} }
}, },
@ -352,9 +352,9 @@ description: Operations executed multi-axis-robot.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1280,
0, 1362,
0 3
] ]
} }
}, },
@ -923,9 +923,9 @@ description: Operations executed multi-axis-robot.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 323,
0, 406,
0 4
] ]
} }
}, },
@ -1340,9 +1340,9 @@ description: Operations executed multi-axis-robot.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1143,
0, 1226,
0 4
] ]
} }
}, },
@ -1685,9 +1685,9 @@ description: Operations executed multi-axis-robot.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1996,
0, 2079,
0 4
] ]
} }
}, },
@ -2341,9 +2341,9 @@ description: Operations executed multi-axis-robot.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1033,
0, 1116,
0 5
] ]
} }
}, },
@ -2519,9 +2519,9 @@ description: Operations executed multi-axis-robot.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1415,
0, 1498,
0 5
] ]
} }
}, },
@ -3400,9 +3400,9 @@ description: Operations executed multi-axis-robot.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1299,
0, 1382,
0 6
] ]
} }
}, },

View File

@ -247,8 +247,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 5504,
0, 5535,
0 0
] ]
} }
@ -410,8 +410,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 5838,
0, 5872,
0 0
] ]
} }
@ -713,8 +713,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3177,
0, 3198,
0 0
] ]
} }
@ -893,8 +893,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 6049,
0, 6204,
0 0
] ]
} }
@ -1431,8 +1431,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 6407,
0, 6562,
0 0
] ]
} }
@ -1857,8 +1857,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 6781,
0, 6936,
0 0
] ]
} }
@ -2160,8 +2160,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3177,
0, 3198,
0 0
] ]
} }
@ -2340,8 +2340,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 7426,
0, 7581,
0 0
] ]
} }
@ -2643,8 +2643,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4236,
0, 4258,
0 0
] ]
} }
@ -2823,8 +2823,8 @@ description: Operations executed pipe-flange-assembly.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 7778,
0, 7933,
0 0
] ]
} }

View File

@ -209,8 +209,8 @@ description: Operations executed poopy-shoe.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1706,
0, 1743,
0 0
] ]
} }
@ -452,8 +452,8 @@ description: Operations executed poopy-shoe.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2178,
0, 2209,
0 0
] ]
} }
@ -532,8 +532,8 @@ description: Operations executed poopy-shoe.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2907,
0, 2938,
0 0
] ]
} }
@ -775,8 +775,8 @@ description: Operations executed poopy-shoe.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3566,
0, 3597,
0 0
] ]
} }
@ -856,8 +856,8 @@ description: Operations executed poopy-shoe.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3816,
0, 3847,
0 0
] ]
} }
@ -930,8 +930,8 @@ description: Operations executed poopy-shoe.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4043,
0, 4093,
0 0
] ]
} }
@ -1173,8 +1173,8 @@ description: Operations executed poopy-shoe.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 4597,
0, 4629,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed sheet-metal-bracket.kcl description: Operations executed sheet-metal-bracket.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1299,
0, 1325,
0 0
] ]
} }
@ -124,8 +124,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1331,
0, 1410,
0 0
] ]
} }
@ -184,8 +184,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1416,
0, 1502,
0 0
] ]
} }
@ -244,8 +244,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1508,
0, 1594,
0 0
] ]
} }
@ -304,8 +304,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1600,
0, 1679,
0 0
] ]
} }
@ -364,8 +364,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1685,
0, 1771,
0 0
] ]
} }
@ -424,8 +424,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1777,
0, 1856,
0 0
] ]
} }
@ -484,8 +484,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1862,
0, 1942,
0 0
] ]
} }
@ -544,8 +544,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1948,
0, 2035,
0 0
] ]
} }
@ -687,8 +687,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2446,
0, 2473,
0 0
] ]
} }
@ -751,8 +751,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2479,
0, 2608,
0 0
] ]
} }
@ -894,8 +894,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 2976,
0, 3003,
0 0
] ]
} }
@ -958,8 +958,8 @@ description: Operations executed sheet-metal-bracket.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 3009,
0, 3139,
0 0
] ]
} }

View File

@ -136,9 +136,9 @@ description: Operations executed walkie-talkie.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 564,
0, 794,
0 3
] ]
} }
}, },
@ -2035,9 +2035,9 @@ description: Operations executed walkie-talkie.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 745,
0, 890,
0 8
] ]
} }
}, },
@ -2227,9 +2227,9 @@ description: Operations executed walkie-talkie.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 745,
0, 890,
0 8
] ]
} }
}, },
@ -2419,9 +2419,9 @@ description: Operations executed walkie-talkie.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 745,
0, 890,
0 8
] ]
} }
}, },
@ -2611,9 +2611,9 @@ description: Operations executed walkie-talkie.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 745,
0, 890,
0 8
] ]
} }
}, },
@ -2923,9 +2923,9 @@ description: Operations executed walkie-talkie.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 891,
0, 1096,
0 6
] ]
} }
}, },

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed kittycad_svg.kcl description: Operations executed kittycad_svg.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed kittycad_svg.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 18347,
0, 18366,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed linear_pattern3d_a_pattern.kcl description: Operations executed linear_pattern3d_a_pattern.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed linear_pattern3d_a_pattern.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 159,
0, 178,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed mike_stress_test.kcl description: Operations executed mike_stress_test.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed mike_stress_test.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 77102,
0, 77121,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed neg_xz_plane.kcl description: Operations executed neg_xz_plane.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed neg_xz_plane.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 151,
0, 174,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed parametric.kcl description: Operations executed parametric.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed parametric.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 465,
0, 488,
0 0
] ]
} }

View File

@ -64,8 +64,8 @@ description: Operations executed parametric_with_tan_arc.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 622,
0, 645,
0 0
] ]
} }

View File

@ -1,5 +1,5 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl-lib/src/simulation_tests.rs
description: Operations executed pentagon_fillet_sugar.kcl description: Operations executed pentagon_fillet_sugar.kcl
--- ---
[ [
@ -64,8 +64,8 @@ description: Operations executed pentagon_fillet_sugar.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 379,
0, 411,
0 0
] ]
} }
@ -164,8 +164,8 @@ description: Operations executed pentagon_fillet_sugar.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 612,
0, 640,
0 0
] ]
} }
@ -229,8 +229,8 @@ description: Operations executed pentagon_fillet_sugar.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 646,
0, 773,
0 0
] ]
} }
@ -329,8 +329,8 @@ description: Operations executed pentagon_fillet_sugar.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 812,
0, 840,
0 0
] ]
} }
@ -394,8 +394,8 @@ description: Operations executed pentagon_fillet_sugar.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 846,
0, 973,
0 0
] ]
} }

View File

@ -93,8 +93,8 @@ description: Operations executed pipe_as_arg.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 367,
0, 391,
0 0
] ]
} }

View File

@ -209,8 +209,8 @@ description: Operations executed poop_chute.kcl
} }
}, },
"sourceRange": [ "sourceRange": [
0, 1719,
0, 1757,
0 0
] ]
} }

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