Compare commits
5 Commits
v0.21.8
...
max-unused
Author | SHA1 | Date | |
---|---|---|---|
cfde4e99f9 | |||
1bcaaec807 | |||
67e90f580d | |||
78046eceb6 | |||
502cb08a10 |
@ -1,7 +1,7 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import { makeTemplate, getUtils } from './test-utils'
|
import { makeTemplate, getUtils } from './test-utils'
|
||||||
import waitOn from 'wait-on'
|
import waitOn from 'wait-on'
|
||||||
import { roundOff, uuidv4 } from 'lib/utils'
|
import { roundOff } from 'lib/utils'
|
||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
import { secrets } from './secrets'
|
import { secrets } from './secrets'
|
||||||
import {
|
import {
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
import { Coords2d } from 'lang/std/sketch'
|
import { Coords2d } from 'lang/std/sketch'
|
||||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||||
import { EngineCommand } from 'lang/std/engineConnection'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||||
@ -60,7 +59,7 @@ test.beforeEach(async ({ context, page }) => {
|
|||||||
test.setTimeout(60000)
|
test.setTimeout(60000)
|
||||||
|
|
||||||
test('Basic sketch', async ({ page }) => {
|
test('Basic sketch', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -146,7 +145,7 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
|
|
||||||
test('Can moving camera', async ({ page, context }) => {
|
test('Can moving camera', async ({ page, context }) => {
|
||||||
test.skip(process.platform === 'darwin', 'Can moving camera')
|
test.skip(process.platform === 'darwin', 'Can moving camera')
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -166,26 +165,7 @@ test('Can moving camera', async ({ page, context }) => {
|
|||||||
// We could break them out into separate tests, but the longest past of the test is waiting
|
// We could break them out into separate tests, but the longest past of the test is waiting
|
||||||
// for the stream to start, so it can be good to bundle related things together.
|
// for the stream to start, so it can be good to bundle related things together.
|
||||||
|
|
||||||
const camCommand: EngineCommand = {
|
await u.updateCamPosition(camPos)
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const updateCamCommand: EngineCommand = {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await u.sendCustomCmd(camCommand)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd(updateCamCommand)
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// rotate
|
// rotate
|
||||||
@ -245,29 +225,9 @@ test('Can moving camera', async ({ page, context }) => {
|
|||||||
await page.mouse.move(700, 200, { steps: 2 })
|
await page.mouse.move(700, 200, { steps: 2 })
|
||||||
await page.mouse.up({ button: 'right' })
|
await page.mouse.up({ button: 'right' })
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
}, [-19, -85, -85])
|
}, [-10, -85, -85])
|
||||||
|
|
||||||
const camCommand: EngineCommand = {
|
await u.updateCamPosition(camPos)
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const updateCamCommand: EngineCommand = {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await u.sendCustomCmd(camCommand)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd(updateCamCommand)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -303,13 +263,13 @@ test('Can moving camera', async ({ page, context }) => {
|
|||||||
await bakeInRetries(async () => {
|
await bakeInRetries(async () => {
|
||||||
await page.mouse.move(700, 400)
|
await page.mouse.move(700, 400)
|
||||||
await page.mouse.wheel(0, -100)
|
await page.mouse.wheel(0, -100)
|
||||||
}, [1, -68, -68])
|
}, [1, -94, -94])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('if you click the format button it formats your code', async ({
|
test('if you click the format button it formats your code', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1000, height: 500 })
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
|
|
||||||
@ -340,7 +300,7 @@ test('if you click the format button it formats your code', async ({
|
|||||||
test('if you use the format keyboard binding it formats your code', async ({
|
test('if you use the format keyboard binding it formats your code', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -398,7 +358,7 @@ test('ensure the Zoo logo is not a link in browser app', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1000, height: 500 })
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
|
|
||||||
@ -465,7 +425,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
|
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -541,7 +501,7 @@ fn squareHole = (l, w) => {
|
|||||||
test('if your kcl gets an error from the engine it is inlined', async ({
|
test('if your kcl gets an error from the engine it is inlined', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -589,7 +549,7 @@ angle: 90
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('executes on load', async ({ page }) => {
|
test('executes on load', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -625,7 +585,7 @@ test('executes on load', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('re-executes', async ({ page }) => {
|
test('re-executes', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem('persistCode', `const myVar = 5`)
|
localStorage.setItem('persistCode', `const myVar = 5`)
|
||||||
})
|
})
|
||||||
@ -659,31 +619,17 @@ const sketchOnPlaneAndBackSideTest = async (
|
|||||||
plane: string,
|
plane: string,
|
||||||
clickCoords: { x: number; y: number }
|
clickCoords: { x: number; y: number }
|
||||||
) => {
|
) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const coord =
|
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
|
||||||
plane === '-XY' || plane === '-YZ' || plane === 'XZ' ? -100 : 100
|
let camPos: [number, number, number] = [100, 100, 100]
|
||||||
const camCommand: EngineCommand = {
|
if (plane === '-XY' || plane === '-YZ' || plane === 'XZ') {
|
||||||
type: 'modeling_cmd_req',
|
camPos = camCmdBackSide
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
vantage: { x: coord, y: coord, z: coord },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const updateCamCommand: EngineCommand = {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = `const part001 = startSketchOn('${plane}')
|
const code = `const part001 = startSketchOn('${plane}')
|
||||||
@ -693,10 +639,7 @@ const sketchOnPlaneAndBackSideTest = async (
|
|||||||
|
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await u.updateCamPosition(camPos)
|
||||||
await u.sendCustomCmd(camCommand)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd(updateCamCommand)
|
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.mouse.click(clickCoords.x, clickCoords.y)
|
await page.mouse.click(clickCoords.x, clickCoords.y)
|
||||||
@ -753,7 +696,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Auto complete works', async ({ page }) => {
|
test('Auto complete works', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const lspStartPromise = page.waitForEvent('console', async (message) => {
|
const lspStartPromise = page.waitForEvent('console', async (message) => {
|
||||||
@ -823,7 +766,7 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
test('Stored settings are validated and fall back to defaults', async ({
|
test('Stored settings are validated and fall back to defaults', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
|
|
||||||
// Override beforeEach test setup
|
// Override beforeEach test setup
|
||||||
// with corrupted settings
|
// with corrupted settings
|
||||||
@ -1031,7 +974,7 @@ test('Project and user settings can be reset', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Click through each onboarding step', async ({ page }) => {
|
test('Click through each onboarding step', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
|
|
||||||
// Override beforeEach test setup
|
// Override beforeEach test setup
|
||||||
await page.addInitScript(
|
await page.addInitScript(
|
||||||
@ -1070,7 +1013,7 @@ test('Click through each onboarding step', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Onboarding redirects and code updating', async ({ page }) => {
|
test('Onboarding redirects and code updating', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
|
|
||||||
// Override beforeEach test setup
|
// Override beforeEach test setup
|
||||||
await page.addInitScript(
|
await page.addInitScript(
|
||||||
@ -1117,7 +1060,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
// tests mapping works on fresh sketch and edited sketch
|
// tests mapping works on fresh sketch and edited sketch
|
||||||
// tests using hovers which is the same as selections, because if
|
// tests using hovers which is the same as selections, because if
|
||||||
// source ranges are wrong, hovers won't work
|
// source ranges are wrong, hovers won't work
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -1407,7 +1350,7 @@ test.describe('Command bar tests', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -1480,7 +1423,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
|
|
||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
|
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -1623,7 +1566,7 @@ const part002 = startSketchOn('${plane}')
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('ProgramMemory can be serialised', async ({ page }) => {
|
test('ProgramMemory can be serialised', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -1662,7 +1605,7 @@ test('ProgramMemory can be serialised', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Hovering over 3d features highlights code', async ({ page }) => {
|
test('Hovering over 3d features highlights code', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -1693,28 +1636,6 @@ test('Hovering over 3d features highlights code', async ({ page }) => {
|
|||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.openAndClearDebugPanel()
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
vantage: { x: 0, y: -1250, z: 580 },
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
const extrusionTop: Coords2d = [800, 240]
|
const extrusionTop: Coords2d = [800, 240]
|
||||||
const flatExtrusionFace: Coords2d = [960, 160]
|
const flatExtrusionFace: Coords2d = [960, 160]
|
||||||
const arc: Coords2d = [840, 160]
|
const arc: Coords2d = [840, 160]
|
||||||
@ -1752,7 +1673,7 @@ test('Hovering over 3d features highlights code', async ({ page }) => {
|
|||||||
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
const selectionsSnippets = {
|
const selectionsSnippets = {
|
||||||
extrudeAndEditBlocked: '|> startProfileAt([10.81, 32.99], %)',
|
extrudeAndEditBlocked: '|> startProfileAt([10.81, 32.99], %)',
|
||||||
extrudeAndEditBlockedInFunction: '|> startProfileAt(pos, %)',
|
extrudeAndEditBlockedInFunction: '|> startProfileAt(pos, %)',
|
||||||
@ -1849,9 +1770,7 @@ fn yohey = (pos) => {
|
|||||||
// selecting an editable sketch but clicking "start sktech" should start a new sketch and not edit the existing one
|
// selecting an editable sketch but clicking "start sktech" should start a new sketch and not edit the existing one
|
||||||
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
|
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
await page.getByTestId('KCL Code').click()
|
await page.mouse.click(700, 200)
|
||||||
await page.mouse.click(300, 500)
|
|
||||||
await page.getByTestId('KCL Code').click()
|
|
||||||
// expect main content to contain `part005` i.e. started a new sketch
|
// expect main content to contain `part005` i.e. started a new sketch
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
/part005 = startSketchOn\('XZ'\)/
|
/part005 = startSketchOn\('XZ'\)/
|
||||||
@ -1861,7 +1780,7 @@ fn yohey = (pos) => {
|
|||||||
test('Deselecting line tool should mean nothing happens on click', async ({
|
test('Deselecting line tool should mean nothing happens on click', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -1927,7 +1846,7 @@ test('multi-sketch file shows multiple Edit Sketch buttons', async ({
|
|||||||
page,
|
page,
|
||||||
context,
|
context,
|
||||||
}) => {
|
}) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
const selectionsSnippets = {
|
const selectionsSnippets = {
|
||||||
startProfileAt1:
|
startProfileAt1:
|
||||||
'|> startProfileAt([-width / 4 + screwRadius, height / 2], %)',
|
'|> startProfileAt([-width / 4 + screwRadius, height / 2], %)',
|
||||||
@ -2006,7 +1925,7 @@ const part002 = startSketchOn('-XZ')
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Can edit segments by dragging their handles', async ({ page }) => {
|
test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -2024,28 +1943,6 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
|||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.openAndClearDebugPanel()
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
vantage: { x: 0, y: -1250, z: 580 },
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
const startPX = [665, 458]
|
const startPX = [665, 458]
|
||||||
const lineEndPX = [842, 458]
|
const lineEndPX = [842, 458]
|
||||||
const arcEndPX = [971, 342]
|
const arcEndPX = [971, 342]
|
||||||
@ -2104,7 +2001,7 @@ const doSnapAtDifferentScales = async (
|
|||||||
scale = 1,
|
scale = 1,
|
||||||
fudge = 0
|
fudge = 0
|
||||||
) => {
|
) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -2114,7 +2011,6 @@ const doSnapAtDifferentScales = async (
|
|||||||
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|
||||||
|> line([${roundOff(scale * 175.36)}, 0], %)
|
|> line([${roundOff(scale * 175.36)}, 0], %)
|
||||||
|> line([0, -${roundOff(scale * 175.36) + fudge}], %)
|
|> line([0, -${roundOff(scale * 175.36) + fudge}], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
||||||
|> close(%)`
|
|> close(%)`
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@ -2167,11 +2063,6 @@ const doSnapAtDifferentScales = async (
|
|||||||
prevContent = await page.locator('.cm-content').innerText()
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||||
// Assert the tool was unequipped
|
|
||||||
await expect(page.getByRole('button', { name: 'Line' })).not.toHaveAttribute(
|
|
||||||
'aria-pressed',
|
|
||||||
'true'
|
|
||||||
)
|
|
||||||
|
|
||||||
// exit sketch
|
// exit sketch
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
@ -2191,7 +2082,7 @@ test.describe('Snap to close works (at any scale)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Sketch on face', async ({ page }) => {
|
test('Sketch on face', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -2226,7 +2117,7 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
() => page.mouse.click(625, 133),
|
() => page.mouse.click(793, 133),
|
||||||
'default_camera_get_settings',
|
'default_camera_get_settings',
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@ -2257,10 +2148,9 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||||
|> startProfileAt([-13.02, 6.52], %)
|
|> startProfileAt([-12.83, 6.7], %)
|
||||||
|> line([2.1, -0.18], %)
|
|> line([2.87, -0.23], %)
|
||||||
|> line([-2.23, -1.07], %)
|
|> line([-3.05, -1.47], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
||||||
|> close(%)`)
|
|> close(%)`)
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
@ -2270,7 +2160,7 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
await u.updateCamPosition([1049, 239, 686])
|
await u.updateCamPosition([1049, 239, 686])
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await page.getByText('startProfileAt([-13.02, 6.52], %)').click()
|
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
|
||||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||||
@ -2299,7 +2189,6 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|> startProfileAt([-12.83, 6.7], %)
|
|> startProfileAt([-12.83, 6.7], %)
|
||||||
|> line([${[2.28, 2.35]}, -${0.07}], %)
|
|> line([${[2.28, 2.35]}, -${0.07}], %)
|
||||||
|> line([-3.05, -1.47], %)
|
|> line([-3.05, -1.47], %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|
||||||
|> close(%)`
|
|> close(%)`
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(result.regExp)
|
await expect(page.locator('.cm-content')).toHaveText(result.regExp)
|
||||||
@ -2309,17 +2198,15 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
await page.getByText('startProfileAt([-13.02, 6.52], %)').click()
|
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
|
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await page.getByRole('button', { name: 'Extrude' }).click()
|
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||||
|
|
||||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
@ -2341,7 +2228,7 @@ test('Can code mod a line length', async ({ page }) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -2350,39 +2237,24 @@ test('Can code mod a line length', async ({ page }) => {
|
|||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// Click the line of code for line.
|
// Click the line of code for xLine.
|
||||||
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
await page.getByText(`xLine(-20, %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(350) // wait for animation
|
||||||
|
|
||||||
const startXPx = 500
|
const startXPx = 500
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||||
await page.keyboard.down('Shift')
|
await page.mouse.click(615, 102)
|
||||||
await page.mouse.click(834, 244)
|
|
||||||
await page.keyboard.up('Shift')
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Constrain', exact: true }).click()
|
await page.getByRole('button', { name: 'Constrain', exact: true }).click()
|
||||||
await page.getByRole('button', { name: 'length', exact: true }).click()
|
await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||||
await page.getByText('Add constraining value').click()
|
await page.getByText('Add constraining value').click()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
`const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make sure we didn't pop out of sketch mode.
|
|
||||||
await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible()
|
|
||||||
|
|
||||||
await page.waitForTimeout(500) // wait for animation
|
|
||||||
|
|
||||||
// Exit sketch
|
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
|
||||||
await page.keyboard.press('Escape')
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
|
||||||
).not.toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Extrude from command bar selects extrude line after', async ({
|
test('Extrude from command bar selects extrude line after', async ({
|
||||||
@ -2401,7 +2273,7 @@ test('Extrude from command bar selects extrude line after', async ({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -2424,68 +2296,6 @@ test('Extrude from command bar selects extrude line after', async ({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('First escape in tool pops you out of tool, second exits sketch mode', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
// Wait for the app to be ready for use
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
|
||||||
await u.openDebugPanel()
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
const lineButton = page.getByRole('button', { name: 'Line' })
|
|
||||||
const arcButton = page.getByRole('button', { name: 'Tangential Arc' })
|
|
||||||
|
|
||||||
// Test these hotkeys perform actions when
|
|
||||||
// focus is on the canvas
|
|
||||||
await page.mouse.move(600, 250)
|
|
||||||
await page.mouse.click(600, 250)
|
|
||||||
|
|
||||||
// Start a sketch
|
|
||||||
await page.keyboard.press('s')
|
|
||||||
await page.mouse.move(800, 300)
|
|
||||||
await page.mouse.click(800, 300)
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
|
||||||
|
|
||||||
// Draw a line
|
|
||||||
await page.mouse.move(700, 200, { steps: 5 })
|
|
||||||
await page.mouse.click(700, 200)
|
|
||||||
await page.mouse.move(800, 250, { steps: 5 })
|
|
||||||
await page.mouse.click(800, 250)
|
|
||||||
// Unequip line tool
|
|
||||||
await page.keyboard.press('Escape')
|
|
||||||
// Make sure we didn't pop out of sketch mode.
|
|
||||||
await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible()
|
|
||||||
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
|
|
||||||
// Equip arc tool
|
|
||||||
await page.keyboard.press('a')
|
|
||||||
await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
|
|
||||||
await page.mouse.move(1000, 100, { steps: 5 })
|
|
||||||
await page.mouse.click(1000, 100)
|
|
||||||
await page.keyboard.press('Escape')
|
|
||||||
await page.keyboard.press('l')
|
|
||||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
|
||||||
|
|
||||||
// Do not close the sketch.
|
|
||||||
// On close it will exit sketch mode.
|
|
||||||
|
|
||||||
// Unequip line tool
|
|
||||||
await page.keyboard.press('Escape')
|
|
||||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
|
|
||||||
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
|
|
||||||
// Make sure we didn't pop out of sketch mode.
|
|
||||||
await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible()
|
|
||||||
// Exit sketch
|
|
||||||
await page.keyboard.press('Escape')
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
|
||||||
).not.toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
||||||
// This test can run long if it takes a little too long to load
|
// This test can run long if it takes a little too long to load
|
||||||
// the engine.
|
// the engine.
|
||||||
@ -2509,7 +2319,7 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Wait for the app to be ready for use
|
// Wait for the app to be ready for use
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -2569,20 +2379,17 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
|||||||
// Close profile
|
// Close profile
|
||||||
await page.mouse.move(700, 200, { steps: 5 })
|
await page.mouse.move(700, 200, { steps: 5 })
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
// On close it will unequip the line tool.
|
// Unequip line tool
|
||||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
|
await page.keyboard.press('Escape')
|
||||||
// Exit sketch
|
// Exit sketch
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
|
||||||
).not.toBeVisible()
|
|
||||||
|
|
||||||
// Extrude
|
// Extrude
|
||||||
await page.mouse.click(750, 150)
|
await page.mouse.click(750, 150)
|
||||||
await expect(extrudeButton).not.toBeDisabled()
|
await expect(extrudeButton).not.toBeDisabled()
|
||||||
await page.keyboard.press('e')
|
await page.keyboard.press('e')
|
||||||
await page.mouse.move(730, 230, { steps: 5 })
|
await page.mouse.move(850, 180, { steps: 5 })
|
||||||
await page.mouse.click(730, 230)
|
await page.mouse.click(850, 180)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.getByRole('button', { name: 'Continue' }).click()
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||||
@ -2590,64 +2397,3 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
|||||||
await codePaneButton.click()
|
await codePaneButton.click()
|
||||||
await expect(page.locator('.cm-content')).toContainText('extrude(')
|
await expect(page.locator('.cm-content')).toContainText('extrude(')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('simulate network down and network little widget', async ({ page }) => {
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
|
||||||
|
|
||||||
const networkWidget = page.locator('[data-testid="network-toggle"]')
|
|
||||||
await expect(networkWidget).toBeVisible()
|
|
||||||
await networkWidget.hover()
|
|
||||||
|
|
||||||
const networkPopover = page.locator('[data-testid="network-popover"]')
|
|
||||||
await expect(networkPopover).not.toBeVisible()
|
|
||||||
|
|
||||||
// Expect the network to be up
|
|
||||||
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
|
||||||
|
|
||||||
// Click the network widget
|
|
||||||
await networkWidget.click()
|
|
||||||
|
|
||||||
// Check the modal opened.
|
|
||||||
await expect(networkPopover).toBeVisible()
|
|
||||||
|
|
||||||
// Click off the modal.
|
|
||||||
await page.mouse.click(100, 100)
|
|
||||||
await expect(networkPopover).not.toBeVisible()
|
|
||||||
|
|
||||||
// Turn off the network
|
|
||||||
await u.emulateNetworkConditions({
|
|
||||||
offline: true,
|
|
||||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
|
||||||
latency: 0,
|
|
||||||
downloadThroughput: -1,
|
|
||||||
uploadThroughput: -1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Expect the network to be down
|
|
||||||
await expect(page.getByText('Network Health (Offline)')).toBeVisible()
|
|
||||||
|
|
||||||
// Click the network widget
|
|
||||||
await networkWidget.click()
|
|
||||||
|
|
||||||
// Check the modal opened.
|
|
||||||
await expect(networkPopover).toBeVisible()
|
|
||||||
|
|
||||||
// Click off the modal.
|
|
||||||
await page.mouse.click(100, 100)
|
|
||||||
await expect(networkPopover).not.toBeVisible()
|
|
||||||
|
|
||||||
// Turn back on the network
|
|
||||||
await u.emulateNetworkConditions({
|
|
||||||
offline: false,
|
|
||||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
|
||||||
latency: 0,
|
|
||||||
downloadThroughput: -1,
|
|
||||||
uploadThroughput: -1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Expect the network to be up
|
|
||||||
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
|
||||||
})
|
|
||||||
|
@ -44,7 +44,7 @@ test.setTimeout(60_000)
|
|||||||
test('exports of each format should work', async ({ page, context }) => {
|
test('exports of each format should work', async ({ page, context }) => {
|
||||||
// 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 = getUtils(page)
|
||||||
await context.addInitScript(async () => {
|
await context.addInitScript(async () => {
|
||||||
;(window as any).playwrightSkipFilePicker = true
|
;(window as any).playwrightSkipFilePicker = true
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
@ -369,7 +369,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => {
|
|||||||
localStorage.setItem('persistCode', code)
|
localStorage.setItem('persistCode', code)
|
||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
@ -424,7 +424,7 @@ test.describe('extrude on default planes should be stable', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Draft segments should look right', async ({ page, context }) => {
|
test('Draft segments should look right', async ({ page, context }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -483,7 +483,7 @@ test('Draft segments should look right', async ({ page, context }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Draft rectangles should look right', async ({ page, context }) => {
|
test('Draft rectangles should look right', async ({ page, context }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -530,7 +530,7 @@ test('Draft rectangles should look right', async ({ page, context }) => {
|
|||||||
|
|
||||||
test.describe('Client side scene scale should match engine scale', () => {
|
test.describe('Client side scene scale should match engine scale', () => {
|
||||||
test('Inch scale', async ({ page }) => {
|
test('Inch scale', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -633,7 +633,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
@ -719,7 +719,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Sketch on face with none z-up', async ({ page, context }) => {
|
test('Sketch on face with none z-up', async ({ page, context }) => {
|
||||||
const u = await getUtils(page)
|
const u = getUtils(page)
|
||||||
await context.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
await context.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -773,76 +773,3 @@ const part002 = startSketchOn(part001, 'seg01')
|
|||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Zoom to fit on load - solid 2d', async ({ page, context }) => {
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await context.addInitScript(async () => {
|
|
||||||
localStorage.setItem(
|
|
||||||
'persistCode',
|
|
||||||
`const part001 = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-10, -10], %)
|
|
||||||
|> line([20, 0], %)
|
|
||||||
|> line([0, 20], %)
|
|
||||||
|> line([-20, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
}, KCL_DEFAULT_LENGTH)
|
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
|
||||||
|
|
||||||
await u.openDebugPanel()
|
|
||||||
// wait for execution done
|
|
||||||
await expect(
|
|
||||||
page.locator('[data-message-type="execution-done"]')
|
|
||||||
).toHaveCount(2)
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
// Wait for the second extrusion to appear
|
|
||||||
// TODO: Find a way to truly know that the objects have finished
|
|
||||||
// rendering, because an execution-done message is not sufficient.
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
await expect(page).toHaveScreenshot({
|
|
||||||
maxDiffPixels: 100,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await context.addInitScript(async () => {
|
|
||||||
localStorage.setItem(
|
|
||||||
'persistCode',
|
|
||||||
`const part001 = startSketchOn('XY')
|
|
||||||
|> startProfileAt([-10, -10], %)
|
|
||||||
|> line([20, 0], %)
|
|
||||||
|> line([0, 20], %)
|
|
||||||
|> line([-20, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(10, %)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
}, KCL_DEFAULT_LENGTH)
|
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
|
||||||
|
|
||||||
await u.openDebugPanel()
|
|
||||||
// wait for execution done
|
|
||||||
await expect(
|
|
||||||
page.locator('[data-message-type="execution-done"]')
|
|
||||||
).toHaveCount(2)
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
// Wait for the second extrusion to appear
|
|
||||||
// TODO: Find a way to truly know that the objects have finished
|
|
||||||
// rendering, because an execution-done message is not sufficient.
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
await expect(page).toHaveScreenshot({
|
|
||||||
maxDiffPixels: 100,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 47 KiB |
@ -1,9 +1,8 @@
|
|||||||
import { test, expect, Page } from '@playwright/test'
|
import { expect, Page } from '@playwright/test'
|
||||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import pixelMatch from 'pixelmatch'
|
import pixelMatch from 'pixelmatch'
|
||||||
import { PNG } from 'pngjs'
|
import { PNG } from 'pngjs'
|
||||||
import { Protocol } from 'playwright-core/types/protocol'
|
|
||||||
|
|
||||||
async function waitForPageLoad(page: Page) {
|
async function waitForPageLoad(page: Page) {
|
||||||
// wait for 'Loading stream...' spinner
|
// wait for 'Loading stream...' spinner
|
||||||
@ -94,12 +93,7 @@ async function waitForCmdReceive(page: Page, commandType: string) {
|
|||||||
.waitFor()
|
.waitFor()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUtils(page: Page) {
|
export function getUtils(page: Page) {
|
||||||
const cdpSession =
|
|
||||||
process.platform === 'darwin'
|
|
||||||
? null
|
|
||||||
: await page.context().newCDPSession(page)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
||||||
removeCurrentCode: () => removeCurrentCode(page),
|
removeCurrentCode: () => removeCurrentCode(page),
|
||||||
@ -186,17 +180,6 @@ export async function getUtils(page: Page) {
|
|||||||
}
|
}
|
||||||
}, 50)
|
}, 50)
|
||||||
}),
|
}),
|
||||||
emulateNetworkConditions: async (
|
|
||||||
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
|
||||||
) => {
|
|
||||||
// Skip on non-Chromium browsers, since we need to use the CDP.
|
|
||||||
test.skip(
|
|
||||||
cdpSession === null,
|
|
||||||
'Network emulation is only supported in Chromium'
|
|
||||||
)
|
|
||||||
|
|
||||||
cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.21.8",
|
"version": "0.21.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.16.0",
|
"@codemirror/autocomplete": "^6.16.0",
|
||||||
@ -10,12 +10,12 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.19",
|
"@headlessui/react": "^1.7.19",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.63",
|
"@kittycad/lib": "^0.0.60",
|
||||||
"@lezer/javascript": "^1.4.9",
|
"@lezer/javascript": "^1.4.9",
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^2.0.1",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@replit/codemirror-interact": "^6.3.1",
|
"@replit/codemirror-interact": "^6.3.1",
|
||||||
"@tauri-apps/api": "2.0.0-beta.12",
|
"@tauri-apps/api": "2.0.0-beta.8",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0-beta.3",
|
"@tauri-apps/plugin-fs": "^2.0.0-beta.3",
|
||||||
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
|
||||||
@ -61,11 +61,11 @@
|
|||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"vscode-jsonrpc": "^8.2.1",
|
"vscode-jsonrpc": "^8.1.0",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"wasm-pack": "^0.12.1",
|
"wasm-pack": "^0.12.1",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2",
|
||||||
"ws": "^8.17.0",
|
"ws": "^8.16.0",
|
||||||
"xstate": "^4.38.2",
|
"xstate": "^4.38.2",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
|
21
src-tauri/Cargo.lock
generated
@ -195,7 +195,6 @@ dependencies = [
|
|||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-persisted-scope",
|
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
@ -5371,9 +5370,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs"
|
name = "tauri-plugin-fs"
|
||||||
version = "2.0.0-beta.7"
|
version = "2.0.0-beta.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35377195c6923beda5f29482a16b492d431de964389fca9aaf81a0f7e908023f"
|
checksum = "609f53d90f08808679ecdd81455d9a4d0053291b92780695569f7400fdba27d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"glob",
|
"glob",
|
||||||
@ -5448,22 +5447,6 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tauri-plugin-persisted-scope"
|
|
||||||
version = "2.0.0-beta.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b600c870217ec082b0f3482935a8edad347d18e8cd7d422a74bc92d035c0e394"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"bincode",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tauri",
|
|
||||||
"tauri-plugin-fs",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-process"
|
name = "tauri-plugin-process"
|
||||||
version = "2.0.0-beta.3"
|
version = "2.0.0-beta.3"
|
||||||
|
@ -28,7 +28,6 @@ tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
|||||||
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
||||||
tauri-plugin-log = { version = "2.0.0-beta.4" }
|
tauri-plugin-log = { version = "2.0.0-beta.4" }
|
||||||
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-persisted-scope = { version = "2.0.0-beta.7" }
|
|
||||||
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
||||||
|
@ -32,15 +32,6 @@
|
|||||||
{
|
{
|
||||||
"identifier": "fs:scope",
|
"identifier": "fs:scope",
|
||||||
"allow": [
|
"allow": [
|
||||||
{
|
|
||||||
"path": "$TEMP"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "$TEMP/**/*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "$HOME"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "$HOME/**/*"
|
"path": "$HOME/**/*"
|
||||||
},
|
},
|
||||||
|
@ -425,7 +425,6 @@ fn main() -> Result<()> {
|
|||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_persisted_scope::init())
|
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
@ -74,5 +74,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"version": "0.21.8"
|
"version": "0.21.7"
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ import {
|
|||||||
EngineCommand,
|
EngineCommand,
|
||||||
Subscription,
|
Subscription,
|
||||||
EngineCommandManager,
|
EngineCommandManager,
|
||||||
UnreliableSubscription,
|
|
||||||
} from 'lang/std/engineConnection'
|
} from 'lang/std/engineConnection'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { deg2Rad } from 'lib/utils2d'
|
import { deg2Rad } from 'lib/utils2d'
|
||||||
@ -233,19 +232,9 @@ export class CameraControls {
|
|||||||
this.update()
|
this.update()
|
||||||
this._usePerspectiveCamera()
|
this._usePerspectiveCamera()
|
||||||
|
|
||||||
type CallBackParam = Parameters<
|
const cb: Subscription<
|
||||||
(
|
'default_camera_zoom' | 'camera_drag_end' | 'default_camera_get_settings'
|
||||||
| Subscription<
|
>['callback'] = ({ data, type }) => {
|
||||||
| 'default_camera_zoom'
|
|
||||||
| 'camera_drag_end'
|
|
||||||
| 'default_camera_get_settings'
|
|
||||||
| 'zoom_to_fit'
|
|
||||||
>
|
|
||||||
| UnreliableSubscription<'camera_drag_move'>
|
|
||||||
)['callback']
|
|
||||||
>[0]
|
|
||||||
|
|
||||||
const cb = ({ data, type }: CallBackParam) => {
|
|
||||||
const camSettings = data.settings
|
const camSettings = data.settings
|
||||||
this.camera.position.set(
|
this.camera.position.set(
|
||||||
camSettings.pos.x,
|
camSettings.pos.x,
|
||||||
@ -257,13 +246,7 @@ export class CameraControls {
|
|||||||
camSettings.center.y,
|
camSettings.center.y,
|
||||||
camSettings.center.z
|
camSettings.center.z
|
||||||
)
|
)
|
||||||
const quat = new Quaternion(
|
this.camera.up.set(camSettings.up.x, camSettings.up.y, camSettings.up.z)
|
||||||
camSettings.orientation.x,
|
|
||||||
camSettings.orientation.y,
|
|
||||||
camSettings.orientation.z,
|
|
||||||
camSettings.orientation.w
|
|
||||||
).invert()
|
|
||||||
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat))
|
|
||||||
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
||||||
this.useOrthographicCamera()
|
this.useOrthographicCamera()
|
||||||
}
|
}
|
||||||
@ -304,14 +287,6 @@ export class CameraControls {
|
|||||||
event: 'default_camera_get_settings',
|
event: 'default_camera_get_settings',
|
||||||
callback: cb,
|
callback: cb,
|
||||||
})
|
})
|
||||||
this.engineCommandManager.subscribeTo({
|
|
||||||
event: 'zoom_to_fit',
|
|
||||||
callback: cb,
|
|
||||||
})
|
|
||||||
this.engineCommandManager.subscribeToUnreliable({
|
|
||||||
event: 'camera_drag_move',
|
|
||||||
callback: cb,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,6 @@ import {
|
|||||||
tangentialArcToSegment,
|
tangentialArcToSegment,
|
||||||
} from './segments'
|
} from './segments'
|
||||||
import {
|
import {
|
||||||
addCallExpressionsToPipe,
|
|
||||||
addCloseToPipe,
|
addCloseToPipe,
|
||||||
addNewSketchLn,
|
addNewSketchLn,
|
||||||
changeSketchArguments,
|
changeSketchArguments,
|
||||||
@ -537,32 +536,8 @@ export class SceneEntities {
|
|||||||
|
|
||||||
let modifiedAst
|
let modifiedAst
|
||||||
if (profileStart) {
|
if (profileStart) {
|
||||||
const lastSegment = sketchGroup.value.slice(-1)[0]
|
|
||||||
modifiedAst = addCallExpressionsToPipe({
|
|
||||||
node: kclManager.ast,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
pathToNode: sketchPathToNode,
|
|
||||||
expressions: [
|
|
||||||
createCallExpressionStdLib(
|
|
||||||
lastSegment.type === 'TangentialArcTo'
|
|
||||||
? 'tangentialArcTo'
|
|
||||||
: 'lineTo',
|
|
||||||
[
|
|
||||||
createArrayExpression([
|
|
||||||
createCallExpressionStdLib('profileStartX', [
|
|
||||||
createPipeSubstitution(),
|
|
||||||
]),
|
|
||||||
createCallExpressionStdLib('profileStartY', [
|
|
||||||
createPipeSubstitution(),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
createPipeSubstitution(),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
modifiedAst = addCloseToPipe({
|
modifiedAst = addCloseToPipe({
|
||||||
node: modifiedAst,
|
node: kclManager.ast,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
})
|
})
|
||||||
@ -585,9 +560,6 @@ export class SceneEntities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await kclManager.executeAstMock(modifiedAst)
|
await kclManager.executeAstMock(modifiedAst)
|
||||||
if (profileStart) {
|
|
||||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
|
||||||
} else {
|
|
||||||
this.setUpDraftSegment(
|
this.setUpDraftSegment(
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
forward,
|
forward,
|
||||||
@ -595,7 +567,6 @@ export class SceneEntities {
|
|||||||
origin,
|
origin,
|
||||||
segmentName
|
segmentName
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onMove: (args) => {
|
onMove: (args) => {
|
||||||
this.onDragSegment({
|
this.onDragSegment({
|
||||||
|
@ -28,6 +28,7 @@ import { Axis } from 'lib/selections'
|
|||||||
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
||||||
import { CameraControls } from './CameraControls'
|
import { CameraControls } from './CameraControls'
|
||||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { settings } from 'lib/settings/initialSettings'
|
||||||
import { MouseState } from 'machines/modelingMachine'
|
import { MouseState } from 'machines/modelingMachine'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
|
|
||||||
@ -181,6 +182,15 @@ export class SceneInfra {
|
|||||||
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
|
||||||
|
// CAMERA
|
||||||
|
const camHeightDistanceRatio = 0.5
|
||||||
|
const baseUnit: BaseUnit = settings.modeling.defaultUnit.current
|
||||||
|
const baseRadius = 5.6
|
||||||
|
const length = baseUnitTomm(baseUnit) * baseRadius
|
||||||
|
const ang = Math.atan(camHeightDistanceRatio)
|
||||||
|
const x = Math.cos(ang) * length
|
||||||
|
const y = Math.sin(ang) * length
|
||||||
|
|
||||||
this.camControls = new CameraControls(
|
this.camControls = new CameraControls(
|
||||||
false,
|
false,
|
||||||
this.renderer.domElement,
|
this.renderer.domElement,
|
||||||
@ -188,6 +198,7 @@ export class SceneInfra {
|
|||||||
)
|
)
|
||||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||||
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
||||||
|
this.camControls.camera.position.set(0, -x, y)
|
||||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
|
@ -395,14 +395,6 @@ const CustomIconMap = {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
trash: (
|
|
||||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M8.5 6H5V8H6M8.5 6V4H11.5V6M8.5 6H11.5M11.5 6H15V8H14M6 8V15.5H8M6 8H14M14 8V15.5H12M8 15.5V10M8 15.5H10M12 15.5V10M12 15.5H10M10 15.5V12"
|
|
||||||
stroke="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
vertical: (
|
vertical: (
|
||||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
import { sceneInfra } from 'lib/singletons'
|
|
||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import {
|
|
||||||
WebGLRenderer,
|
|
||||||
Scene,
|
|
||||||
OrthographicCamera,
|
|
||||||
BoxGeometry,
|
|
||||||
SphereGeometry,
|
|
||||||
MeshBasicMaterial,
|
|
||||||
Color,
|
|
||||||
Mesh,
|
|
||||||
Clock,
|
|
||||||
Quaternion,
|
|
||||||
ColorRepresentation,
|
|
||||||
} from 'three'
|
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
|
||||||
const FRUSTUM_SIZE = 0.5
|
|
||||||
const AXIS_LENGTH = 0.35
|
|
||||||
const AXIS_WIDTH = 0.02
|
|
||||||
const AXIS_COLORS = {
|
|
||||||
x: '#fa6668',
|
|
||||||
y: '#11eb6b',
|
|
||||||
z: '#6689ef',
|
|
||||||
gray: '#c6c7c2',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Gizmo() {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canvasRef.current) return
|
|
||||||
|
|
||||||
const canvas = canvasRef.current
|
|
||||||
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
|
|
||||||
renderer.setSize(CANVAS_SIZE, CANVAS_SIZE)
|
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
||||||
|
|
||||||
const scene = new Scene()
|
|
||||||
const camera = createCamera()
|
|
||||||
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
|
|
||||||
scene.add(...gizmoAxes, ...gizmoAxisHeads)
|
|
||||||
|
|
||||||
const clock = new Clock()
|
|
||||||
const clientCamera = sceneInfra.camControls.camera
|
|
||||||
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
updateCameraOrientation(
|
|
||||||
camera,
|
|
||||||
currentQuaternion,
|
|
||||||
sceneInfra.camControls.camera.quaternion,
|
|
||||||
clock.getDelta()
|
|
||||||
)
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
}
|
|
||||||
animate()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
renderer.dispose()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-none">
|
|
||||||
<canvas ref={canvasRef} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCamera = () => {
|
|
||||||
return new OrthographicCamera(
|
|
||||||
-FRUSTUM_SIZE,
|
|
||||||
FRUSTUM_SIZE,
|
|
||||||
FRUSTUM_SIZE,
|
|
||||||
-FRUSTUM_SIZE,
|
|
||||||
0.5,
|
|
||||||
3
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createGizmo = () => {
|
|
||||||
const gizmoAxes = [
|
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.x, 0, 'z'),
|
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
|
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
|
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI, 'z'),
|
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
|
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
|
|
||||||
]
|
|
||||||
|
|
||||||
const gizmoAxisHeads = [
|
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.x, 0, 'z'),
|
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
|
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
|
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI, 'z'),
|
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
|
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
|
|
||||||
]
|
|
||||||
|
|
||||||
return { gizmoAxes, gizmoAxisHeads }
|
|
||||||
}
|
|
||||||
|
|
||||||
const createAxis = (
|
|
||||||
length: number,
|
|
||||||
width: number,
|
|
||||||
color: ColorRepresentation,
|
|
||||||
rotation = 0,
|
|
||||||
axis = 'x'
|
|
||||||
) => {
|
|
||||||
const geometry = new BoxGeometry(length, width, width).translate(
|
|
||||||
length / 2,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
|
||||||
const mesh = new Mesh(geometry, material)
|
|
||||||
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
|
||||||
return mesh
|
|
||||||
}
|
|
||||||
|
|
||||||
const createAxisHead = (
|
|
||||||
length: number,
|
|
||||||
color: ColorRepresentation,
|
|
||||||
rotation = 0,
|
|
||||||
axis = 'x'
|
|
||||||
) => {
|
|
||||||
const geometry = new SphereGeometry(0.065, 16, 8).translate(length, 0, 0)
|
|
||||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
|
||||||
const mesh = new Mesh(geometry, material)
|
|
||||||
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
|
||||||
return mesh
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCameraOrientation = (
|
|
||||||
camera: OrthographicCamera,
|
|
||||||
currentQuaternion: Quaternion,
|
|
||||||
targetQuaternion: Quaternion,
|
|
||||||
deltaTime: number
|
|
||||||
) => {
|
|
||||||
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
|
|
||||||
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
|
|
||||||
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
|
|
||||||
camera.quaternion.copy(currentQuaternion)
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
import { APP_VERSION } from 'routes/Settings'
|
import { APP_VERSION } from 'routes/Settings'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import Gizmo from 'components/Gizmo'
|
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { NetworkHealthIndicator } from 'components/NetworkHealthIndicator'
|
import { NetworkHealthIndicator } from 'components/NetworkHealthIndicator'
|
||||||
import { HelpMenu } from './HelpMenu'
|
import { HelpMenu } from './HelpMenu'
|
||||||
@ -15,9 +14,8 @@ export function LowerRightControls(props: React.PropsWithChildren) {
|
|||||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3">
|
<section className="fixed bottom-2 right-2">
|
||||||
{props.children}
|
{props.children}
|
||||||
<Gizmo />
|
|
||||||
<menu className="flex items-center justify-end gap-3">
|
<menu className="flex items-center justify-end gap-3">
|
||||||
<a
|
<a
|
||||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
||||||
|
@ -300,23 +300,8 @@ export const ModelingMachineProvider = ({
|
|||||||
selectionRanges
|
selectionRanges
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'Has exportable geometry': () => {
|
'Has exportable geometry': () =>
|
||||||
if (
|
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
|
||||||
kclManager.kclErrors.length === 0 &&
|
|
||||||
kclManager.ast.body.length > 0
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
else {
|
|
||||||
let errorMessage = 'Unable to Export '
|
|
||||||
if (kclManager.kclErrors.length > 0)
|
|
||||||
errorMessage += 'due to KCL Errors'
|
|
||||||
else if (kclManager.ast.body.length === 0)
|
|
||||||
errorMessage += 'due to Empty Scene'
|
|
||||||
console.error(errorMessage)
|
|
||||||
toast.error(errorMessage)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
'AST-undo-startSketchOn': async ({ sketchDetails }) => {
|
'AST-undo-startSketchOn': async ({ sketchDetails }) => {
|
||||||
|
@ -210,7 +210,6 @@ function ModelingPaneButton({
|
|||||||
key={paneConfig.id}
|
key={paneConfig.id}
|
||||||
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-none"
|
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-none"
|
||||||
onClick={togglePane}
|
onClick={togglePane}
|
||||||
data-testid={paneConfig.title}
|
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={paneConfig.icon}
|
icon={paneConfig.icon}
|
||||||
|
@ -231,10 +231,7 @@ export const NetworkHealthIndicator = () => {
|
|||||||
Network Health ({NETWORK_HEALTH_TEXT[overallState]})
|
Network Health ({NETWORK_HEALTH_TEXT[overallState]})
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Popover.Panel
|
<Popover.Panel className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
|
||||||
className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm"
|
|
||||||
data-testid="network-popover"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between p-2 rounded-t-sm ${overallConnectionStateColor[overallState].bg} ${overallConnectionStateColor[overallState].icon}`}
|
className={`flex items-center justify-between p-2 rounded-t-sm ${overallConnectionStateColor[overallState].bg} ${overallConnectionStateColor[overallState].icon}`}
|
||||||
>
|
>
|
||||||
|
235
src/components/ProjectCard.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
|
import { paths } from 'lib/paths'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { ActionButton } from './ActionButton'
|
||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faPenAlt,
|
||||||
|
faTrashAlt,
|
||||||
|
faX,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FILE_EXT } from 'lib/constants'
|
||||||
|
import { Dialog } from '@headlessui/react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import Tooltip from './Tooltip'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
|
function ProjectCard({
|
||||||
|
project,
|
||||||
|
handleRenameProject,
|
||||||
|
handleDeleteProject,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
project: Project
|
||||||
|
handleRenameProject: (
|
||||||
|
e: FormEvent<HTMLFormElement>,
|
||||||
|
f: Project
|
||||||
|
) => Promise<void>
|
||||||
|
handleDeleteProject: (f: Project) => Promise<void>
|
||||||
|
}) {
|
||||||
|
useHotkeys('esc', () => setIsEditing(false))
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
|
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
||||||
|
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||||
|
|
||||||
|
let inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
function handleSave(e: FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayedTime(dateStr: string) {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const startOfToday = new Date()
|
||||||
|
startOfToday.setHours(0, 0, 0, 0)
|
||||||
|
return date.getTime() < startOfToday.getTime()
|
||||||
|
? date.toLocaleDateString()
|
||||||
|
: date.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getNumberOfFiles() {
|
||||||
|
setNumberOfFiles(project.kcl_file_count)
|
||||||
|
setNumberOfFolders(project.directory_count)
|
||||||
|
}
|
||||||
|
void getNumberOfFiles()
|
||||||
|
}, [project.kcl_file_count, project.directory_count])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus()
|
||||||
|
inputRef.current.select()
|
||||||
|
}
|
||||||
|
}, [inputRef.current])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
{...props}
|
||||||
|
className="group relative min-h-[5em] p-1 rounded-sm border border-chalkboard-20 dark:border-chalkboard-80 hover:!border-primary"
|
||||||
|
>
|
||||||
|
{isEditing ? (
|
||||||
|
<form onSubmit={handleSave} className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
className="min-w-0 p-1 dark:bg-chalkboard-80 dark:border-chalkboard-40 focus:outline-none"
|
||||||
|
type="text"
|
||||||
|
id="newProjectName"
|
||||||
|
name="newProjectName"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
defaultValue={project.name}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
type="submit"
|
||||||
|
iconStart={{
|
||||||
|
icon: faCheck,
|
||||||
|
size: 'sm',
|
||||||
|
className: 'p-1',
|
||||||
|
bgClassName: '!bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0"
|
||||||
|
>
|
||||||
|
<Tooltip position="left" delay={1000}>
|
||||||
|
Rename project
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: faX,
|
||||||
|
size: 'sm',
|
||||||
|
iconClassName: 'dark:!text-chalkboard-20',
|
||||||
|
bgClassName: '!bg-transparent',
|
||||||
|
className: 'p-1',
|
||||||
|
}}
|
||||||
|
className="!p-0"
|
||||||
|
onClick={() => setIsEditing(false)}
|
||||||
|
>
|
||||||
|
<Tooltip position="left" delay={1000}>
|
||||||
|
Cancel
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
className="relative z-0 flex flex-col h-full gap-2 p-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10"
|
||||||
|
to={`${paths.FILE}/${encodeURIComponent(project.default_file)}`}
|
||||||
|
data-testid="project-link"
|
||||||
|
>
|
||||||
|
<div className="flex-1">{project.name?.replace(FILE_EXT, '')}</div>
|
||||||
|
<span className="text-xs text-chalkboard-60">
|
||||||
|
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
|
||||||
|
{numberOfFolders > 0 &&
|
||||||
|
`/ ${numberOfFolders} folder${
|
||||||
|
numberOfFolders === 1 ? '' : 's'
|
||||||
|
}`}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-chalkboard-60">
|
||||||
|
Edited{' '}
|
||||||
|
{project.metadata && project.metadata?.modified
|
||||||
|
? getDisplayedTime(project.metadata.modified)
|
||||||
|
: 'never'}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<div className="absolute z-10 flex items-center gap-1 opacity-0 bottom-2 right-2 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: faPenAlt,
|
||||||
|
className: 'p-1',
|
||||||
|
iconClassName: 'dark:!text-chalkboard-20',
|
||||||
|
bgClassName: '!bg-transparent',
|
||||||
|
size: 'xs',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopPropagation()
|
||||||
|
setIsEditing(true)
|
||||||
|
}}
|
||||||
|
className="!p-0"
|
||||||
|
>
|
||||||
|
<Tooltip position="left" delay={1000}>
|
||||||
|
Rename project
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: faTrashAlt,
|
||||||
|
className: 'p-1',
|
||||||
|
size: 'xs',
|
||||||
|
bgClassName: '!bg-transparent',
|
||||||
|
iconClassName: '!text-destroy-70',
|
||||||
|
}}
|
||||||
|
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopPropagation()
|
||||||
|
setIsConfirmingDelete(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip position="left" delay={1000}>
|
||||||
|
Delete project
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<Dialog
|
||||||
|
open={isConfirmingDelete}
|
||||||
|
onClose={() => setIsConfirmingDelete(false)}
|
||||||
|
className="relative z-50"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 grid bg-chalkboard-110/80 place-content-center">
|
||||||
|
<Dialog.Panel className="max-w-2xl p-4 border rounded bg-chalkboard-10 dark:bg-chalkboard-100 border-destroy-80">
|
||||||
|
<Dialog.Title as="h2" className="mb-4 text-2xl font-bold">
|
||||||
|
Delete File
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
This will permanently delete "{project.name || 'this file'}".
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<p className="my-4">
|
||||||
|
Are you sure you want to delete "{project.name || 'this file'}
|
||||||
|
"? This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={async () => {
|
||||||
|
await handleDeleteProject(project)
|
||||||
|
setIsConfirmingDelete(false)
|
||||||
|
}}
|
||||||
|
iconStart={{
|
||||||
|
icon: faTrashAlt,
|
||||||
|
bgClassName: 'bg-destroy-80',
|
||||||
|
className: 'p-1',
|
||||||
|
size: 'sm',
|
||||||
|
iconClassName: '!text-destroy-70 dark:!text-destroy-40',
|
||||||
|
}}
|
||||||
|
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() => setIsConfirmingDelete(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectCard
|
@ -1,53 +0,0 @@
|
|||||||
import { Dialog } from '@headlessui/react'
|
|
||||||
import { ActionButton } from 'components/ActionButton'
|
|
||||||
|
|
||||||
interface DeleteProjectDialogProps {
|
|
||||||
projectName: string
|
|
||||||
onConfirm: () => void
|
|
||||||
onDismiss: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DeleteProjectDialog({
|
|
||||||
projectName,
|
|
||||||
onConfirm,
|
|
||||||
onDismiss,
|
|
||||||
}: DeleteProjectDialogProps) {
|
|
||||||
return (
|
|
||||||
<Dialog open={true} onClose={onDismiss} className="relative z-50">
|
|
||||||
<div className="fixed inset-0 grid bg-chalkboard-110/80 place-content-center">
|
|
||||||
<Dialog.Panel className="max-w-2xl p-4 border rounded bg-chalkboard-10 dark:bg-chalkboard-100 border-destroy-80">
|
|
||||||
<Dialog.Title as="h2" className="mb-4 text-2xl font-bold">
|
|
||||||
Delete File
|
|
||||||
</Dialog.Title>
|
|
||||||
<Dialog.Description>
|
|
||||||
This will permanently delete "{projectName || 'this file'}
|
|
||||||
".
|
|
||||||
</Dialog.Description>
|
|
||||||
|
|
||||||
<p className="my-4">
|
|
||||||
Are you sure you want to delete "{projectName || 'this file'}
|
|
||||||
"? This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={onConfirm}
|
|
||||||
iconStart={{
|
|
||||||
icon: 'trash',
|
|
||||||
bgClassName: 'bg-destroy-10 dark:bg-destroy-80',
|
|
||||||
iconClassName: '!text-destroy-80 dark:!text-destroy-20',
|
|
||||||
}}
|
|
||||||
className="hover:border-destroy-40 dark:hover:border-destroy-40 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton Element="button" onClick={onDismiss}>
|
|
||||||
Cancel
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,176 +0,0 @@
|
|||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
|
||||||
import { paths } from 'lib/paths'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { ActionButton } from '../ActionButton'
|
|
||||||
import { FILE_EXT } from 'lib/constants'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
import Tooltip from '../Tooltip'
|
|
||||||
import { DeleteProjectDialog } from './DeleteProjectDialog'
|
|
||||||
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
|
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
|
|
||||||
function ProjectCard({
|
|
||||||
project,
|
|
||||||
handleRenameProject,
|
|
||||||
handleDeleteProject,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
project: Project
|
|
||||||
handleRenameProject: (
|
|
||||||
e: FormEvent<HTMLFormElement>,
|
|
||||||
f: Project
|
|
||||||
) => Promise<void>
|
|
||||||
handleDeleteProject: (f: Project) => Promise<void>
|
|
||||||
}) {
|
|
||||||
useHotkeys('esc', () => setIsEditing(false))
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
|
||||||
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
|
||||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
|
||||||
// const [imageUrl, setImageUrl] = useState('')
|
|
||||||
|
|
||||||
let inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
function handleSave(e: FormEvent<HTMLFormElement>) {
|
|
||||||
e.preventDefault()
|
|
||||||
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayedTime(dateStr: string) {
|
|
||||||
const date = new Date(dateStr)
|
|
||||||
const startOfToday = new Date()
|
|
||||||
startOfToday.setHours(0, 0, 0, 0)
|
|
||||||
return date.getTime() < startOfToday.getTime()
|
|
||||||
? date.toLocaleDateString()
|
|
||||||
: date.toLocaleTimeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getNumberOfFiles() {
|
|
||||||
setNumberOfFiles(project.kcl_file_count)
|
|
||||||
setNumberOfFolders(project.directory_count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// async function setupImageUrl() {
|
|
||||||
// const projectImagePath = await join(project.path, PROJECT_IMAGE_NAME)
|
|
||||||
// if (await exists(projectImagePath)) {
|
|
||||||
// const imageData = await readFile(projectImagePath)
|
|
||||||
// const blob = new Blob([imageData], { type: 'image/jpg' })
|
|
||||||
// const imageUrl = URL.createObjectURL(blob)
|
|
||||||
// setImageUrl(imageUrl)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
void getNumberOfFiles()
|
|
||||||
// void setupImageUrl()
|
|
||||||
}, [project.kcl_file_count, project.directory_count])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef.current && isEditing) {
|
|
||||||
inputRef.current.focus()
|
|
||||||
inputRef.current.select()
|
|
||||||
}
|
|
||||||
}, [isEditing, inputRef.current])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
{...props}
|
|
||||||
className="group relative flex flex-col rounded-sm border border-primary/40 dark:border-chalkboard-80 hover:!border-primary"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
data-testid="project-link"
|
|
||||||
to={`${paths.FILE}/${encodeURIComponent(project.default_file)}`}
|
|
||||||
className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary"
|
|
||||||
>
|
|
||||||
{/* <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
|
|
||||||
{imageUrl && (
|
|
||||||
<img
|
|
||||||
src={imageUrl}
|
|
||||||
alt=""
|
|
||||||
className="h-full w-full transition-transform group-hover:scale-105 object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div> */}
|
|
||||||
<div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
|
|
||||||
{isEditing ? (
|
|
||||||
<ProjectCardRenameForm
|
|
||||||
onSubmit={handleSave}
|
|
||||||
className="flex items-center gap-2 p-2"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
project={project}
|
|
||||||
onDismiss={() => setIsEditing(false)}
|
|
||||||
ref={inputRef}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<h3 className="font-sans relative z-0 p-2">
|
|
||||||
{project.name?.replace(FILE_EXT, '')}
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
<span className="px-2 text-chalkboard-60 text-xs">
|
|
||||||
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
|
|
||||||
{numberOfFolders > 0 &&
|
|
||||||
`/ ${numberOfFolders} folder${numberOfFolders === 1 ? '' : 's'}`}
|
|
||||||
</span>
|
|
||||||
<span className="px-2 text-chalkboard-60 text-xs">
|
|
||||||
Edited{' '}
|
|
||||||
{project.metadata && project.metadata?.modified
|
|
||||||
? getDisplayedTime(project.metadata.modified)
|
|
||||||
: 'never'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
{!isEditing && (
|
|
||||||
<div className="absolute z-10 flex items-center gap-1 opacity-0 bottom-2 right-2 group-hover:opacity-100 group-focus-within:opacity-100">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
iconStart={{
|
|
||||||
icon: 'sketch',
|
|
||||||
iconClassName: 'dark:!text-chalkboard-20',
|
|
||||||
bgClassName: '!bg-transparent',
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
e.nativeEvent.stopPropagation()
|
|
||||||
setIsEditing(true)
|
|
||||||
}}
|
|
||||||
className="!p-0"
|
|
||||||
>
|
|
||||||
<Tooltip position="top-right" delay={1000}>
|
|
||||||
Rename project
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
iconStart={{
|
|
||||||
icon: 'trash',
|
|
||||||
iconClassName: 'dark:!text-chalkboard-30',
|
|
||||||
bgClassName: '!bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
e.nativeEvent.stopPropagation()
|
|
||||||
setIsConfirmingDelete(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip position="top-right" delay={1000}>
|
|
||||||
Delete project
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isConfirmingDelete && (
|
|
||||||
<DeleteProjectDialog
|
|
||||||
projectName={project.name}
|
|
||||||
onConfirm={async () => {
|
|
||||||
await handleDeleteProject(project)
|
|
||||||
setIsConfirmingDelete(false)
|
|
||||||
}}
|
|
||||||
onDismiss={() => setIsConfirmingDelete(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProjectCard
|
|
@ -1,67 +0,0 @@
|
|||||||
import { ActionButton } from 'components/ActionButton'
|
|
||||||
import Tooltip from 'components/Tooltip'
|
|
||||||
import { HTMLProps, forwardRef } from 'react'
|
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
|
|
||||||
interface ProjectCardRenameFormProps extends HTMLProps<HTMLFormElement> {
|
|
||||||
project: Project
|
|
||||||
onDismiss: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProjectCardRenameForm = forwardRef(
|
|
||||||
(
|
|
||||||
{ project, onDismiss, ...props }: ProjectCardRenameFormProps,
|
|
||||||
ref: React.Ref<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<form {...props}>
|
|
||||||
<input
|
|
||||||
className="min-w-0 dark:bg-chalkboard-80 dark:border-chalkboard-40 focus:outline-none"
|
|
||||||
type="text"
|
|
||||||
id="newProjectName"
|
|
||||||
onClickCapture={(e) => e.preventDefault()}
|
|
||||||
name="newProjectName"
|
|
||||||
required
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
defaultValue={project.name}
|
|
||||||
ref={ref}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
type="submit"
|
|
||||||
iconStart={{
|
|
||||||
icon: 'checkmark',
|
|
||||||
bgClassName: '!bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0"
|
|
||||||
>
|
|
||||||
<Tooltip position="left" delay={1000}>
|
|
||||||
Rename project
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
iconStart={{
|
|
||||||
icon: 'close',
|
|
||||||
iconClassName: 'dark:!text-chalkboard-20',
|
|
||||||
bgClassName: '!bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0"
|
|
||||||
onClick={onDismiss}
|
|
||||||
>
|
|
||||||
<Tooltip position="left" delay={1000}>
|
|
||||||
Cancel
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,6 +1,7 @@
|
|||||||
import { parse, recast, initPromise } from './wasm'
|
import { parse, recast, initPromise } from './wasm'
|
||||||
import {
|
import {
|
||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
|
findUnusedVariables,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
isTypeInValue,
|
isTypeInValue,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
@ -60,6 +61,91 @@ const variableBelowShouldNotBeIncluded = 3
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Test findUnusedVariables', () => {
|
||||||
|
it('should find unused variable in common kcl code', () => {
|
||||||
|
// example code
|
||||||
|
const code = `
|
||||||
|
const xRel001 = -20
|
||||||
|
const xRel002 = -50
|
||||||
|
const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([175.73, 109.38], %)
|
||||||
|
|> line([xRel001, 178.25], %)
|
||||||
|
|> line([-265.39, -87.86], %)
|
||||||
|
|> tangentialArcTo([543.32, -355.04], %)
|
||||||
|
`
|
||||||
|
// parse into ast
|
||||||
|
const ast = parse(code)
|
||||||
|
// find unused variables
|
||||||
|
const unusedVariables = findUnusedVariables(ast)
|
||||||
|
// check wether unused variables match the expected result
|
||||||
|
expect(
|
||||||
|
unusedVariables
|
||||||
|
.map((node) => node.declarations.map((decl) => decl.id.name))
|
||||||
|
.flat()
|
||||||
|
).toEqual(['xRel002'])
|
||||||
|
})
|
||||||
|
it("should not find used variable, even if it's heavy nested", () => {
|
||||||
|
// example code
|
||||||
|
const code = `
|
||||||
|
const deepWithin = 1
|
||||||
|
const veryNested = [
|
||||||
|
{ key: [{ key2: max(5, deepWithin) }] }
|
||||||
|
]
|
||||||
|
`
|
||||||
|
// parse into ast
|
||||||
|
const ast = parse(code)
|
||||||
|
// find unused variables
|
||||||
|
const unusedVariables = findUnusedVariables(ast)
|
||||||
|
// check wether unused variables match the expected result
|
||||||
|
expect(
|
||||||
|
unusedVariables.find((node) =>
|
||||||
|
node.declarations.find((decl) => decl.id.name === 'deepWithin')
|
||||||
|
)
|
||||||
|
).toBeFalsy()
|
||||||
|
})
|
||||||
|
it('should not find used variable, even if used in a closure', () => {
|
||||||
|
// example code
|
||||||
|
const code = `const usedInClosure = 1
|
||||||
|
fn myFunction = () => {
|
||||||
|
return usedInClosure
|
||||||
|
}
|
||||||
|
`
|
||||||
|
// parse into ast
|
||||||
|
const ast = parse(code)
|
||||||
|
// find unused variables
|
||||||
|
const unusedVariables = findUnusedVariables(ast)
|
||||||
|
// check wether unused variables match the expected result
|
||||||
|
expect(
|
||||||
|
unusedVariables.find((node) =>
|
||||||
|
node.declarations.find((decl) => decl.id.name === 'usedInClosure')
|
||||||
|
)
|
||||||
|
).toBeFalsy()
|
||||||
|
})
|
||||||
|
// TODO: The commented code in the below does not even parse due to a KCL bug
|
||||||
|
// When it does parse correctly we'de expect 'a' to be defined but unused
|
||||||
|
// as it's shadowed by the 'a' in the inner scope
|
||||||
|
// it('should find unused variable when the same identifier is used in deeper scope', () => {
|
||||||
|
// const code = `const a = 1
|
||||||
|
// const b = 2
|
||||||
|
// fn (a) => {
|
||||||
|
// return a + 1
|
||||||
|
// }
|
||||||
|
// const myVar = b + 5`
|
||||||
|
// // parse into ast
|
||||||
|
// const ast = parse(code)
|
||||||
|
// console.log('ast', ast)
|
||||||
|
// // find unused variables
|
||||||
|
// const unusedVariables = findUnusedVariables(ast)
|
||||||
|
// console.log('unusedVariables', unusedVariables)
|
||||||
|
// // check wether unused variables match the expected result
|
||||||
|
// expect(
|
||||||
|
// unusedVariables
|
||||||
|
// .map((node) => node.declarations.map((decl) => decl.id.name))
|
||||||
|
// .flat()
|
||||||
|
// ).toEqual(['a'])
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
|
||||||
describe('testing argIsNotIdentifier', () => {
|
describe('testing argIsNotIdentifier', () => {
|
||||||
const code = `const part001 = startSketchOn('XY')
|
const code = `const part001 = startSketchOn('XY')
|
||||||
|> startProfileAt([-1.2, 4.83], %)
|
|> startProfileAt([-1.2, 4.83], %)
|
||||||
|
@ -392,6 +392,68 @@ export function findAllPreviousVariables(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findUnusedVariables(ast: Program): Array<VariableDeclaration> {
|
||||||
|
const declaredVariables = new Map<string, VariableDeclarator>() // Map to store declared variables
|
||||||
|
const usedVariables = new Set<string>() // Set to track used variables
|
||||||
|
|
||||||
|
// 1. Traverse and populate
|
||||||
|
ast.body.forEach((node) => {
|
||||||
|
traverse(node, {
|
||||||
|
enter(node) {
|
||||||
|
if (node.type === 'VariableDeclarator') {
|
||||||
|
// if node is a VariableDeclarator,
|
||||||
|
// add it to declaredVariables
|
||||||
|
declaredVariables.set(node.id.name, node)
|
||||||
|
} else if (node.type === 'Identifier') {
|
||||||
|
// if the node is Identifier, (use of a variable)
|
||||||
|
// check if it is a declared value,
|
||||||
|
// just in case...
|
||||||
|
// to be sure it's a part of the declared variables
|
||||||
|
if (declaredVariables.has(node.name)) {
|
||||||
|
// if yes - mark it as used
|
||||||
|
usedVariables.add(node.name)
|
||||||
|
}
|
||||||
|
} else if (node.type === 'VariableDeclaration') {
|
||||||
|
// check if the declaration is model-defining (contains PipeExpression)
|
||||||
|
const isModelDefining = node.declarations.some(
|
||||||
|
(decl) => decl.init?.type === 'PipeExpression'
|
||||||
|
)
|
||||||
|
if (isModelDefining) {
|
||||||
|
// If it is, mark all contained variables as used
|
||||||
|
node.declarations.forEach((decl) => {
|
||||||
|
usedVariables.add(decl.id.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. Remove used variables from declaredVariables
|
||||||
|
usedVariables.forEach((name) => {
|
||||||
|
declaredVariables.delete(name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. collect unused VariableDeclarations
|
||||||
|
const unusedVariableDeclarations: Array<VariableDeclaration> = []
|
||||||
|
ast.body.forEach((node) => {
|
||||||
|
if (node.type === 'VariableDeclaration') {
|
||||||
|
const unusedDeclarators = node.declarations.filter((declarator) =>
|
||||||
|
declaredVariables.has(declarator.id.name)
|
||||||
|
)
|
||||||
|
if (unusedDeclarators.length > 0) {
|
||||||
|
unusedVariableDeclarations.push({
|
||||||
|
...node,
|
||||||
|
declarations: unusedDeclarators,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. Return the unused variables
|
||||||
|
return unusedVariableDeclarations
|
||||||
|
}
|
||||||
|
|
||||||
type ReplacerFn = (_ast: Program, varName: string) => { modifiedAst: Program }
|
type ReplacerFn = (_ast: Program, varName: string) => { modifiedAst: Program }
|
||||||
|
|
||||||
export function isNodeSafeToReplace(
|
export function isNodeSafeToReplace(
|
||||||
|
@ -590,8 +590,6 @@ class EngineConnection {
|
|||||||
) {
|
) {
|
||||||
this.engineCommandManager.inSequence = result.data.sequence
|
this.engineCommandManager.inSequence = result.data.sequence
|
||||||
callback(result)
|
callback(result)
|
||||||
} else if (result.type !== 'highlight_set_entity') {
|
|
||||||
callback(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -909,7 +907,7 @@ type UnreliableResponses = Extract<
|
|||||||
Models['OkModelingCmdResponse_type'],
|
Models['OkModelingCmdResponse_type'],
|
||||||
{ type: 'highlight_set_entity' | 'camera_drag_move' }
|
{ type: 'highlight_set_entity' | 'camera_drag_move' }
|
||||||
>
|
>
|
||||||
export interface UnreliableSubscription<T extends UnreliableResponses['type']> {
|
interface UnreliableSubscription<T extends UnreliableResponses['type']> {
|
||||||
event: T
|
event: T
|
||||||
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
||||||
}
|
}
|
||||||
@ -1121,6 +1119,24 @@ export class EngineCommandManager {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Make the axis gizmo.
|
||||||
|
// We do this after the connection opened to avoid a race condition.
|
||||||
|
// Connected opened is the last thing that happens when the stream
|
||||||
|
// is ready.
|
||||||
|
// We also do this here because we want to ensure we create the gizmo
|
||||||
|
// and execute the code everytime the stream is restarted.
|
||||||
|
const gizmoId = uuidv4()
|
||||||
|
void this.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: gizmoId,
|
||||||
|
cmd: {
|
||||||
|
type: 'make_axes_gizmo',
|
||||||
|
clobber: false,
|
||||||
|
// If true, axes gizmo will be placed in the corner of the screen.
|
||||||
|
// If false, it will be placed at the origin of the scene.
|
||||||
|
gizmo_mode: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
this._camControlsCameraChange()
|
this._camControlsCameraChange()
|
||||||
this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
// CameraControls subscribes to default_camera_get_settings response events
|
// CameraControls subscribes to default_camera_get_settings response events
|
||||||
@ -1132,26 +1148,10 @@ export class EngineCommandManager {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
this.initPlanes().then(async () => {
|
this.initPlanes().then(() => {
|
||||||
this.resolveReady()
|
this.resolveReady()
|
||||||
setIsStreamReady(true)
|
setIsStreamReady(true)
|
||||||
await executeCode()
|
executeCode()
|
||||||
await this.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'zoom_to_fit',
|
|
||||||
object_ids: [], // leave empty to zoom to all objects
|
|
||||||
padding: 0.1, // padding around the objects
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// make sure client camera syncs after zoom to fit since zoom to fit doesn't return camera settings
|
|
||||||
// TODO: https://github.com/KittyCAD/engine/issues/2098
|
|
||||||
await this.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: { type: 'default_camera_get_settings' },
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
@ -1420,6 +1420,17 @@ export class EngineCommandManager {
|
|||||||
this.lastArtifactMap = this.artifactMap
|
this.lastArtifactMap = this.artifactMap
|
||||||
this.artifactMap = {}
|
this.artifactMap = {}
|
||||||
await this.initPlanes()
|
await this.initPlanes()
|
||||||
|
await this.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'make_axes_gizmo',
|
||||||
|
clobber: false,
|
||||||
|
// If true, axes gizmo will be placed in the corner of the screen.
|
||||||
|
// If false, it will be placed at the origin of the scene.
|
||||||
|
gizmo_mode: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
subscribeTo<T extends ModelTypes>({
|
subscribeTo<T extends ModelTypes>({
|
||||||
event,
|
event,
|
||||||
|
@ -1114,28 +1114,6 @@ export function addNewSketchLn({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addCallExpressionsToPipe({
|
|
||||||
node,
|
|
||||||
pathToNode,
|
|
||||||
expressions,
|
|
||||||
}: {
|
|
||||||
node: Program
|
|
||||||
programMemory: ProgramMemory
|
|
||||||
pathToNode: PathToNode
|
|
||||||
expressions: CallExpression[]
|
|
||||||
}) {
|
|
||||||
const _node = { ...node }
|
|
||||||
const pipeExpression = getNodeFromPath<PipeExpression>(
|
|
||||||
_node,
|
|
||||||
pathToNode,
|
|
||||||
'PipeExpression'
|
|
||||||
).node
|
|
||||||
if (pipeExpression.type !== 'PipeExpression')
|
|
||||||
throw new Error('not a pipe expression')
|
|
||||||
pipeExpression.body = [...pipeExpression.body, ...expressions]
|
|
||||||
return _node
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addCloseToPipe({
|
export function addCloseToPipe({
|
||||||
node,
|
node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
|
@ -24,8 +24,6 @@ export const PROJECT_FOLDER = 'zoo-modeling-app-projects'
|
|||||||
export const FILE_EXT = '.kcl'
|
export const FILE_EXT = '.kcl'
|
||||||
/** Default file to open when a project is opened */
|
/** Default file to open when a project is opened */
|
||||||
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
|
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
|
||||||
/** Thumbnail file name */
|
|
||||||
export const PROJECT_IMAGE_NAME = `main.jpg` as const
|
|
||||||
/** The localStorage key for last-opened projects */
|
/** The localStorage key for last-opened projects */
|
||||||
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
|
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
|
||||||
/** The default name given to new kcl files in a project */
|
/** The default name given to new kcl files in a project */
|
||||||
|
@ -12,7 +12,7 @@ import { loadAndValidateSettings } from './settings/settingsUtils'
|
|||||||
import makeUrlPathRelative from './makeUrlPathRelative'
|
import makeUrlPathRelative from './makeUrlPathRelative'
|
||||||
import { sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
import { readTextFile } from '@tauri-apps/plugin-fs'
|
import { readTextFile } from '@tauri-apps/plugin-fs'
|
||||||
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons'
|
import { codeManager, kclManager } from 'lib/singletons'
|
||||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
import {
|
import {
|
||||||
getProjectInfo,
|
getProjectInfo,
|
||||||
@ -20,7 +20,6 @@ import {
|
|||||||
listProjects,
|
listProjects,
|
||||||
} from './tauri'
|
} from './tauri'
|
||||||
import { createSettings } from './settings/initialSettings'
|
import { createSettings } from './settings/initialSettings'
|
||||||
import { uuidv4 } from './utils'
|
|
||||||
|
|
||||||
// The root loader simply resolves the settings and any errors that
|
// The root loader simply resolves the settings and any errors that
|
||||||
// occurred during the settings load
|
// occurred during the settings load
|
||||||
@ -106,22 +105,6 @@ export const fileLoader: LoaderFunction = async ({
|
|||||||
codeManager.updateCurrentFilePath(current_file_path)
|
codeManager.updateCurrentFilePath(current_file_path)
|
||||||
codeManager.updateCodeStateEditor(code)
|
codeManager.updateCodeStateEditor(code)
|
||||||
kclManager.executeCode(true)
|
kclManager.executeCode(true)
|
||||||
await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'zoom_to_fit',
|
|
||||||
object_ids: [], // leave empty to zoom to all objects
|
|
||||||
padding: 0.1, // padding around the objects
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// make sure client camera syncs after zoom to fit since zoom to fit doesn't return camera settings
|
|
||||||
// TODO: https://github.com/KittyCAD/engine/issues/2098
|
|
||||||
await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: { type: 'default_camera_get_settings' },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set the file system manager to the project path
|
// Set the file system manager to the project path
|
||||||
// So that WASM gets an updated path for operations
|
// So that WASM gets an updated path for operations
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faCircle,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
const DESC = ':desc'
|
const DESC = ':desc'
|
||||||
|
|
||||||
export function getSortIcon(
|
export function getSortIcon(currentSort: string, newSort: string) {
|
||||||
currentSort: string,
|
|
||||||
newSort: string
|
|
||||||
): CustomIconName {
|
|
||||||
if (currentSort === newSort) {
|
if (currentSort === newSort) {
|
||||||
return 'arrowUp'
|
return faArrowUp
|
||||||
} else if (currentSort === newSort + DESC) {
|
} else if (currentSort === newSort + DESC) {
|
||||||
return 'arrowDown'
|
return faArrowDown
|
||||||
}
|
}
|
||||||
return 'horizontalDash'
|
return faCircle
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNextSearchParams(currentSort: string, newSort: string) {
|
export function getNextSearchParams(currentSort: string, newSort: string) {
|
||||||
|
@ -4,15 +4,16 @@ import {
|
|||||||
getNextProjectIndex,
|
getNextProjectIndex,
|
||||||
interpolateProjectNameWithIndex,
|
interpolateProjectNameWithIndex,
|
||||||
doesProjectNameNeedInterpolated,
|
doesProjectNameNeedInterpolated,
|
||||||
} from 'lib/tauriFS'
|
} from '../lib/tauriFS'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
|
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { AppHeader } from 'components/AppHeader'
|
import { AppHeader } from '../components/AppHeader'
|
||||||
import ProjectCard from 'components/ProjectCard/ProjectCard'
|
import ProjectCard from '../components/ProjectCard'
|
||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { type HomeLoaderData } from 'lib/types'
|
import { type HomeLoaderData } from 'lib/types'
|
||||||
import Loading from 'components/Loading'
|
import Loading from '../components/Loading'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { homeMachine } from '../machines/homeMachine'
|
import { homeMachine } from '../machines/homeMachine'
|
||||||
import { ContextFrom, EventFrom } from 'xstate'
|
import { ContextFrom, EventFrom } from 'xstate'
|
||||||
@ -186,12 +187,10 @@ const Home = () => {
|
|||||||
new FormData(e.target as HTMLFormElement)
|
new FormData(e.target as HTMLFormElement)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (newProjectName !== project.name) {
|
|
||||||
send('Rename project', {
|
send('Rename project', {
|
||||||
data: { oldName: project.name, newName: newProjectName },
|
data: { oldName: project.name, newName: newProjectName },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDeleteProject(project: Project) {
|
async function handleDeleteProject(project: Project) {
|
||||||
send('Delete project', { data: { name: project.name || '' } })
|
send('Delete project', { data: { name: project.name || '' } })
|
||||||
@ -200,45 +199,27 @@ const Home = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col h-screen overflow-hidden">
|
<div className="relative flex flex-col h-screen overflow-hidden">
|
||||||
<AppHeader showToolbar={false} />
|
<AppHeader showToolbar={false} />
|
||||||
<div className="w-full flex flex-col overflow-hidden max-w-5xl px-4 mx-auto mt-24 lg:px-2">
|
<div className="w-full max-w-5xl px-4 mx-auto my-24 overflow-y-auto lg:px-0">
|
||||||
<section>
|
<section className="flex justify-between">
|
||||||
<div className="flex justify-between items-baseline select-none">
|
|
||||||
<div className="flex gap-8 items-baseline">
|
|
||||||
<h1 className="text-3xl font-bold">Your Projects</h1>
|
<h1 className="text-3xl font-bold">Your Projects</h1>
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={() => send('Create project')}
|
|
||||||
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
|
|
||||||
iconStart={{
|
|
||||||
icon: 'plus',
|
|
||||||
bgClassName: '!bg-transparent rounded-sm',
|
|
||||||
iconClassName:
|
|
||||||
'!text-chalkboard-10 transition-transform group-active:rotate-90',
|
|
||||||
}}
|
|
||||||
data-testid="home-new-file"
|
|
||||||
>
|
|
||||||
New project
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<small>Sort by</small>
|
<small>Sort by</small>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
className={
|
className={
|
||||||
'text-xs border-primary/10 ' +
|
'text-sm ' +
|
||||||
(!sort.includes('name')
|
(!sort.includes('name')
|
||||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||||
: '')
|
: '')
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
|
||||||
setSearchParams(getNextSearchParams(sort, 'name'))
|
|
||||||
}
|
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: getSortIcon(sort, 'name'),
|
icon: getSortIcon(sort, 'name'),
|
||||||
bgClassName: 'bg-transparent',
|
className: 'p-1.5',
|
||||||
iconClassName: !sort.includes('name')
|
iconClassName: !sort.includes('name')
|
||||||
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
? '!text-chalkboard-40'
|
||||||
: '',
|
: '',
|
||||||
|
size: 'sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Name
|
Name
|
||||||
@ -246,7 +227,7 @@ const Home = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
className={
|
className={
|
||||||
'text-xs border-primary/10 ' +
|
'text-sm ' +
|
||||||
(!isSortByModified
|
(!isSortByModified
|
||||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||||
: '')
|
: '')
|
||||||
@ -255,38 +236,34 @@ const Home = () => {
|
|||||||
setSearchParams(getNextSearchParams(sort, 'modified'))
|
setSearchParams(getNextSearchParams(sort, 'modified'))
|
||||||
}
|
}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: sort ? getSortIcon(sort, 'modified') : 'arrowDown',
|
icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
|
||||||
bgClassName: 'bg-transparent',
|
className: 'p-1.5',
|
||||||
iconClassName: !isSortByModified
|
iconClassName: !isSortByModified ? '!text-chalkboard-40' : '',
|
||||||
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
size: 'sm',
|
||||||
: '',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Last Modified
|
Last Modified
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
<section data-testid="home-section">
|
||||||
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
||||||
Loaded from{' '}
|
Loaded from{' '}
|
||||||
<Link
|
<span className="text-chalkboard-90 dark:text-chalkboard-20">
|
||||||
to="settings?tab=user#projectDirectory"
|
|
||||||
className="text-chalkboard-90 dark:text-chalkboard-20 underline underline-offset-2"
|
|
||||||
>
|
|
||||||
{settings.app.projectDirectory.current}
|
{settings.app.projectDirectory.current}
|
||||||
|
</span>
|
||||||
|
.{' '}
|
||||||
|
<Link to="settings" className="underline underline-offset-2">
|
||||||
|
Edit in settings
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
|
||||||
<section
|
|
||||||
data-testid="home-section"
|
|
||||||
className="flex-1 overflow-y-auto pr-2 pb-24"
|
|
||||||
>
|
|
||||||
{state.matches('Reading projects') ? (
|
{state.matches('Reading projects') ? (
|
||||||
<Loading>Loading your Projects...</Loading>
|
<Loading>Loading your Projects...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{projects.length > 0 ? (
|
{projects.length > 0 ? (
|
||||||
<ul className="grid w-full grid-cols-4 gap-4">
|
<ul className="grid w-full grid-cols-4 gap-4 my-8">
|
||||||
{projects.sort(getSortFunction(sort)).map((project) => (
|
{projects.sort(getSortFunction(sort)).map((project) => (
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
key={project.name}
|
key={project.name}
|
||||||
@ -301,6 +278,14 @@ const Home = () => {
|
|||||||
No Projects found, ready to make your first one?
|
No Projects found, ready to make your first one?
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={() => send('Create project')}
|
||||||
|
iconStart={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
|
||||||
|
data-testid="home-new-file"
|
||||||
|
>
|
||||||
|
New project
|
||||||
|
</ActionButton>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
820
src/wasm-lib/kcl/fuzz/Cargo.lock
generated
@ -1,9 +1,14 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate libfuzzer_sys;
|
||||||
|
extern crate kcl_lib;
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
if let Ok(s) = std::str::from_utf8(data) {
|
||||||
fuzz_target!(|data: &str| {
|
let tokens = kcl_lib::tokeniser::lexer(s);
|
||||||
if let Ok(v) = kcl_lib::token::lexer(data) {
|
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||||
let _ = kcl_lib::parser::Parser::new(v).ast();
|
if let Ok(_) = parser.ast() {
|
||||||
|
println!("OK");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -16,9 +16,7 @@ pub use crate::ast::types::{literal_value::LiteralValue, none::KclNone};
|
|||||||
use crate::{
|
use crate::{
|
||||||
docs::StdLibFn,
|
docs::StdLibFn,
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{
|
executor::{BodyType, ExecutorContext, MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange, UserVal},
|
||||||
BodyType, ExecutorContext, MemoryItem, Metadata, PipeInfo, ProgramMemory, SourceRange, StatementKind, UserVal,
|
|
||||||
},
|
|
||||||
parser::PIPE_OPERATOR,
|
parser::PIPE_OPERATOR,
|
||||||
std::{kcl_stdlib::KclStdLibFn, FunctionKind},
|
std::{kcl_stdlib::KclStdLibFn, FunctionKind},
|
||||||
};
|
};
|
||||||
@ -1098,12 +1096,45 @@ impl CallExpression {
|
|||||||
let mut fn_args: Vec<MemoryItem> = Vec::with_capacity(self.arguments.len());
|
let mut fn_args: Vec<MemoryItem> = Vec::with_capacity(self.arguments.len());
|
||||||
|
|
||||||
for arg in &self.arguments {
|
for arg in &self.arguments {
|
||||||
let metadata = Metadata {
|
let result: MemoryItem = match arg {
|
||||||
source_range: SourceRange([arg.start(), arg.end()]),
|
Value::None(none) => none.into(),
|
||||||
|
Value::Literal(literal) => literal.into(),
|
||||||
|
Value::Identifier(identifier) => {
|
||||||
|
let value = memory.get(&identifier.name, identifier.into())?;
|
||||||
|
value.clone()
|
||||||
|
}
|
||||||
|
Value::BinaryExpression(binary_expression) => {
|
||||||
|
binary_expression.get_result(memory, pipe_info, ctx).await?
|
||||||
|
}
|
||||||
|
Value::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, ctx).await?,
|
||||||
|
Value::UnaryExpression(unary_expression) => unary_expression.get_result(memory, pipe_info, ctx).await?,
|
||||||
|
Value::ObjectExpression(object_expression) => object_expression.execute(memory, pipe_info, ctx).await?,
|
||||||
|
Value::ArrayExpression(array_expression) => array_expression.execute(memory, pipe_info, ctx).await?,
|
||||||
|
Value::PipeExpression(pipe_expression) => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("PipeExpression not implemented here: {:?}", pipe_expression),
|
||||||
|
source_ranges: vec![pipe_expression.into()],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Value::PipeSubstitution(pipe_substitution) => pipe_info
|
||||||
|
.previous_results
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("PipeSubstitution index out of bounds: {:?}", pipe_info),
|
||||||
|
source_ranges: vec![pipe_substitution.into()],
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.clone(),
|
||||||
|
Value::MemberExpression(member_expression) => member_expression.get_result(memory)?,
|
||||||
|
Value::FunctionExpression(function_expression) => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("FunctionExpression not implemented here: {:?}", function_expression),
|
||||||
|
source_ranges: vec![function_expression.into()],
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let result = ctx
|
|
||||||
.arg_into_mem_item(arg, memory, pipe_info, &metadata, StatementKind::Expression)
|
|
||||||
.await?;
|
|
||||||
fn_args.push(result);
|
fn_args.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2742,7 +2773,6 @@ impl PipeExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_recursion::async_recursion]
|
|
||||||
async fn execute_pipe_body(
|
async fn execute_pipe_body(
|
||||||
memory: &mut ProgramMemory,
|
memory: &mut ProgramMemory,
|
||||||
body: &[Value],
|
body: &[Value],
|
||||||
@ -2761,12 +2791,18 @@ async fn execute_pipe_body(
|
|||||||
// They use the `pipe_info` from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
|
// They use the `pipe_info` from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
|
||||||
// they use the % from the parent. After all, this pipe expression hasn't been executed yet, so it doesn't have any % value
|
// they use the % from the parent. After all, this pipe expression hasn't been executed yet, so it doesn't have any % value
|
||||||
// of its own.
|
// of its own.
|
||||||
let meta = Metadata {
|
let output = match first {
|
||||||
source_range: SourceRange([first.start(), first.end()]),
|
Value::BinaryExpression(binary_expression) => binary_expression.get_result(memory, pipe_info, ctx).await?,
|
||||||
|
Value::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, ctx).await?,
|
||||||
|
Value::Identifier(identifier) => memory.get(&identifier.name, identifier.into())?.clone(),
|
||||||
|
_ => {
|
||||||
|
// Return an error this should not happen.
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!("PipeExpression not implemented here: {:?}", first),
|
||||||
|
source_ranges: vec![first.into()],
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let output = ctx
|
|
||||||
.arg_into_mem_item(first, memory, pipe_info, &meta, StatementKind::Expression)
|
|
||||||
.await?;
|
|
||||||
// Now that we've evaluated the first child expression in the pipeline, following child expressions
|
// Now that we've evaluated the first child expression in the pipeline, following child expressions
|
||||||
// should use the previous child expression for %.
|
// should use the previous child expression for %.
|
||||||
// This means there's no more need for the previous `pipe_info` from the parent AST node above this one.
|
// This means there's no more need for the previous `pipe_info` from the parent AST node above this one.
|
||||||
@ -2783,7 +2819,7 @@ async fn execute_pipe_body(
|
|||||||
_ => {
|
_ => {
|
||||||
// Return an error this should not happen.
|
// Return an error this should not happen.
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
message: format!("PipeExpression not implemented here: {:?}", expression),
|
||||||
source_ranges: vec![expression.into()],
|
source_ranges: vec![expression.into()],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -1135,13 +1135,35 @@ impl ExecutorContext {
|
|||||||
let fn_name = call_expr.callee.name.to_string();
|
let fn_name = call_expr.callee.name.to_string();
|
||||||
let mut args: Vec<MemoryItem> = Vec::new();
|
let mut args: Vec<MemoryItem> = Vec::new();
|
||||||
for arg in &call_expr.arguments {
|
for arg in &call_expr.arguments {
|
||||||
let metadata = Metadata {
|
match arg {
|
||||||
source_range: SourceRange([arg.start(), arg.end()]),
|
Value::Literal(literal) => args.push(literal.into()),
|
||||||
};
|
Value::Identifier(identifier) => {
|
||||||
let mem_item = self
|
let memory_item = memory.get(&identifier.name, identifier.into())?;
|
||||||
.arg_into_mem_item(arg, memory, &pipe_info, &metadata, StatementKind::Expression)
|
args.push(memory_item.clone());
|
||||||
.await?;
|
}
|
||||||
args.push(mem_item);
|
Value::CallExpression(call_expr) => {
|
||||||
|
let result = call_expr.execute(memory, &pipe_info, self).await?;
|
||||||
|
args.push(result);
|
||||||
|
}
|
||||||
|
Value::BinaryExpression(binary_expression) => {
|
||||||
|
let result = binary_expression.get_result(memory, &pipe_info, self).await?;
|
||||||
|
args.push(result);
|
||||||
|
}
|
||||||
|
Value::UnaryExpression(unary_expression) => {
|
||||||
|
let result = unary_expression.get_result(memory, &pipe_info, self).await?;
|
||||||
|
args.push(result);
|
||||||
|
}
|
||||||
|
Value::ObjectExpression(object_expression) => {
|
||||||
|
let result = object_expression.execute(memory, &pipe_info, self).await?;
|
||||||
|
args.push(result);
|
||||||
|
}
|
||||||
|
Value::ArrayExpression(array_expression) => {
|
||||||
|
let result = array_expression.execute(memory, &pipe_info, self).await?;
|
||||||
|
args.push(result);
|
||||||
|
}
|
||||||
|
// We do nothing for the rest.
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
match self.stdlib.get_either(&call_expr.callee.name) {
|
match self.stdlib.get_either(&call_expr.callee.name) {
|
||||||
FunctionKind::Core(func) => {
|
FunctionKind::Core(func) => {
|
||||||
@ -1177,16 +1199,88 @@ impl ExecutorContext {
|
|||||||
let source_range: SourceRange = declaration.init.clone().into();
|
let source_range: SourceRange = declaration.init.clone().into();
|
||||||
let metadata = Metadata { source_range };
|
let metadata = Metadata { source_range };
|
||||||
|
|
||||||
let memory_item = self
|
match &declaration.init {
|
||||||
.arg_into_mem_item(
|
Value::None(none) => {
|
||||||
&declaration.init,
|
memory.add(&var_name, none.into(), source_range)?;
|
||||||
memory,
|
}
|
||||||
&pipe_info,
|
Value::Literal(literal) => {
|
||||||
&metadata,
|
memory.add(&var_name, literal.into(), source_range)?;
|
||||||
StatementKind::Declaration { name: &var_name },
|
}
|
||||||
|
Value::Identifier(identifier) => {
|
||||||
|
let value = memory.get(&identifier.name, identifier.into())?;
|
||||||
|
memory.add(&var_name, value.clone(), source_range)?;
|
||||||
|
}
|
||||||
|
Value::BinaryExpression(binary_expression) => {
|
||||||
|
let result = binary_expression.get_result(memory, &pipe_info, self).await?;
|
||||||
|
memory.add(&var_name, result, source_range)?;
|
||||||
|
}
|
||||||
|
Value::FunctionExpression(function_expression) => {
|
||||||
|
let mem_func = force_memory_function(
|
||||||
|
|args: Vec<MemoryItem>,
|
||||||
|
memory: ProgramMemory,
|
||||||
|
function_expression: Box<FunctionExpression>,
|
||||||
|
_metadata: Vec<Metadata>,
|
||||||
|
ctx: ExecutorContext| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut fn_memory =
|
||||||
|
assign_args_to_params(&function_expression, args, memory.clone())?;
|
||||||
|
|
||||||
|
let result = ctx
|
||||||
|
.inner_execute(
|
||||||
|
function_expression.body.clone(),
|
||||||
|
&mut fn_memory,
|
||||||
|
BodyType::Block,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
memory.add(&var_name, memory_item, source_range)?;
|
|
||||||
|
Ok(result.return_)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
memory.add(
|
||||||
|
&var_name,
|
||||||
|
MemoryItem::Function {
|
||||||
|
expression: function_expression.clone(),
|
||||||
|
meta: vec![metadata],
|
||||||
|
func: Some(mem_func),
|
||||||
|
},
|
||||||
|
source_range,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Value::CallExpression(call_expression) => {
|
||||||
|
let result = call_expression.execute(memory, &pipe_info, self).await?;
|
||||||
|
memory.add(&var_name, result, source_range)?;
|
||||||
|
}
|
||||||
|
Value::PipeExpression(pipe_expression) => {
|
||||||
|
let result = pipe_expression.get_result(memory, &pipe_info, self).await?;
|
||||||
|
memory.add(&var_name, result, source_range)?;
|
||||||
|
}
|
||||||
|
Value::PipeSubstitution(pipe_substitution) => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"pipe substitution not implemented for declaration of variable {}",
|
||||||
|
var_name
|
||||||
|
),
|
||||||
|
source_ranges: vec![pipe_substitution.into()],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Value::ArrayExpression(array_expression) => {
|
||||||
|
let result = array_expression.execute(memory, &pipe_info, self).await?;
|
||||||
|
memory.add(&var_name, result, source_range)?;
|
||||||
|
}
|
||||||
|
Value::ObjectExpression(object_expression) => {
|
||||||
|
let result = object_expression.execute(memory, &pipe_info, self).await?;
|
||||||
|
memory.add(&var_name, result, source_range)?;
|
||||||
|
}
|
||||||
|
Value::MemberExpression(member_expression) => {
|
||||||
|
let result = member_expression.get_result(memory)?;
|
||||||
|
memory.add(&var_name, result, source_range)?;
|
||||||
|
}
|
||||||
|
Value::UnaryExpression(unary_expression) => {
|
||||||
|
let result = unary_expression.get_result(memory, &pipe_info, self).await?;
|
||||||
|
memory.add(&var_name, result, source_range)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BodyItem::ReturnStatement(return_statement) => match &return_statement.argument {
|
BodyItem::ReturnStatement(return_statement) => match &return_statement.argument {
|
||||||
@ -1242,77 +1336,6 @@ impl ExecutorContext {
|
|||||||
Ok(memory.clone())
|
Ok(memory.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn arg_into_mem_item<'a>(
|
|
||||||
&self,
|
|
||||||
init: &Value,
|
|
||||||
memory: &mut ProgramMemory,
|
|
||||||
pipe_info: &PipeInfo,
|
|
||||||
metadata: &Metadata,
|
|
||||||
statement_kind: StatementKind<'a>,
|
|
||||||
) -> Result<MemoryItem, KclError> {
|
|
||||||
let item = match init {
|
|
||||||
Value::None(none) => none.into(),
|
|
||||||
Value::Literal(literal) => literal.into(),
|
|
||||||
Value::Identifier(identifier) => {
|
|
||||||
let value = memory.get(&identifier.name, identifier.into())?;
|
|
||||||
value.clone()
|
|
||||||
}
|
|
||||||
Value::BinaryExpression(binary_expression) => binary_expression.get_result(memory, pipe_info, self).await?,
|
|
||||||
Value::FunctionExpression(function_expression) => {
|
|
||||||
let mem_func = force_memory_function(
|
|
||||||
|args: Vec<MemoryItem>,
|
|
||||||
memory: ProgramMemory,
|
|
||||||
function_expression: Box<FunctionExpression>,
|
|
||||||
_metadata: Vec<Metadata>,
|
|
||||||
ctx: ExecutorContext| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut fn_memory = assign_args_to_params(&function_expression, args, memory.clone())?;
|
|
||||||
|
|
||||||
let result = ctx
|
|
||||||
.inner_execute(function_expression.body.clone(), &mut fn_memory, BodyType::Block)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(result.return_)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
MemoryItem::Function {
|
|
||||||
expression: function_expression.clone(),
|
|
||||||
meta: vec![metadata.to_owned()],
|
|
||||||
func: Some(mem_func),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::CallExpression(call_expression) => call_expression.execute(memory, pipe_info, self).await?,
|
|
||||||
Value::PipeExpression(pipe_expression) => pipe_expression.get_result(memory, pipe_info, self).await?,
|
|
||||||
Value::PipeSubstitution(pipe_substitution) => match statement_kind {
|
|
||||||
StatementKind::Declaration { name } => {
|
|
||||||
let message = format!(
|
|
||||||
"you cannot declare variable {name} as %, because % can only be used in function calls"
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
|
||||||
message,
|
|
||||||
source_ranges: vec![pipe_substitution.into()],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
StatementKind::Expression => match pipe_info.previous_results.clone() {
|
|
||||||
Some(x) => x,
|
|
||||||
None => {
|
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
|
||||||
message: "cannot use % outside a pipe expression".to_owned(),
|
|
||||||
source_ranges: vec![pipe_substitution.into()],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Value::ArrayExpression(array_expression) => array_expression.execute(memory, pipe_info, self).await?,
|
|
||||||
Value::ObjectExpression(object_expression) => object_expression.execute(memory, pipe_info, self).await?,
|
|
||||||
Value::MemberExpression(member_expression) => member_expression.get_result(memory)?,
|
|
||||||
Value::UnaryExpression(unary_expression) => unary_expression.get_result(memory, pipe_info, self).await?,
|
|
||||||
};
|
|
||||||
Ok(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the units for the executor.
|
/// Update the units for the executor.
|
||||||
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
|
pub fn update_units(&mut self, units: crate::settings::types::UnitLength) {
|
||||||
self.settings.units = units;
|
self.settings.units = units;
|
||||||
@ -1374,11 +1397,6 @@ fn assign_args_to_params(
|
|||||||
Ok(fn_memory)
|
Ok(fn_memory)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum StatementKind<'a> {
|
|
||||||
Declaration { name: &'a str },
|
|
||||||
Expression,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -5,7 +5,6 @@ use crate::{
|
|||||||
token::{Token, TokenType},
|
token::{Token, TokenType},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod bad_inputs;
|
|
||||||
mod math;
|
mod math;
|
||||||
pub(crate) mod parser_impl;
|
pub(crate) mod parser_impl;
|
||||||
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
macro_rules! parse_and_lex {
|
|
||||||
($func_name:ident, $test_kcl_program:expr) => {
|
|
||||||
#[test]
|
|
||||||
fn $func_name() {
|
|
||||||
if let Ok(v) = $crate::token::lexer($test_kcl_program) {
|
|
||||||
let _ = $crate::parser::Parser::new(v).ast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_and_lex!(crash_eof_1, "{\"ގގ\0\0\0\"\".");
|
|
||||||
parse_and_lex!(crash_eof_2, "(/=e\"\u{616}ݝ\"\"");
|
|
||||||
}
|
|
@ -23,18 +23,6 @@ impl From<ParseError<Located<&str>, winnow::error::ContextError>> for KclError {
|
|||||||
fn from(err: ParseError<Located<&str>, winnow::error::ContextError>) -> Self {
|
fn from(err: ParseError<Located<&str>, winnow::error::ContextError>) -> Self {
|
||||||
let (input, offset): (Vec<char>, usize) = (err.input().chars().collect(), err.offset());
|
let (input, offset): (Vec<char>, usize) = (err.input().chars().collect(), err.offset());
|
||||||
|
|
||||||
if offset >= input.len() {
|
|
||||||
// From the winnow docs:
|
|
||||||
//
|
|
||||||
// This is an offset, not an index, and may point to
|
|
||||||
// the end of input (input.len()) on eof errors.
|
|
||||||
|
|
||||||
return KclError::Lexical(KclErrorDetails {
|
|
||||||
source_ranges: vec![SourceRange([offset, offset])],
|
|
||||||
message: "unexpected EOF while parsing".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add the Winnow tokenizer context to the error.
|
// TODO: Add the Winnow tokenizer context to the error.
|
||||||
// See https://github.com/KittyCAD/modeling-app/issues/784
|
// See https://github.com/KittyCAD/modeling-app/issues/784
|
||||||
let bad_token = &input[offset];
|
let bad_token = &input[offset];
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
fn cube = (length, center) => {
|
|
||||||
let l = length/2
|
|
||||||
let x = center[0]
|
|
||||||
let y = center[1]
|
|
||||||
let p0 = [-l + x, -l + y]
|
|
||||||
let p1 = [-l + x, l + y]
|
|
||||||
let p2 = [ l + x, l + y]
|
|
||||||
let p3 = [ l + x, -l + y]
|
|
||||||
|
|
||||||
return startSketchAt(p0)
|
|
||||||
|> lineTo(p1, %)
|
|
||||||
|> lineTo(p2, %)
|
|
||||||
|> lineTo(p3, %)
|
|
||||||
|> lineTo(p0, %)
|
|
||||||
|> close(%)
|
|
||||||
|> extrude(length, %)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn double = (x) => { return x * 2}
|
|
||||||
fn width = () => { return 200 }
|
|
||||||
|
|
||||||
const myCube = cube(200 |> double(%), [0,0])
|
|
@ -128,15 +128,6 @@ async fn serial_test_lego() {
|
|||||||
twenty_twenty::assert_image("tests/executor/outputs/lego.png", &result, 0.999);
|
twenty_twenty::assert_image("tests/executor/outputs/lego.png", &result, 0.999);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn serial_test_pipe_as_arg() {
|
|
||||||
let code = include_str!("inputs/pipe_as_arg.kcl");
|
|
||||||
let result = execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
twenty_twenty::assert_image("tests/executor/outputs/pipe_as_arg.png", &result, 0.999);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn serial_test_pentagon_fillet_sugar() {
|
async fn serial_test_pentagon_fillet_sugar() {
|
||||||
let code = include_str!("inputs/pentagon_fillet_sugar.kcl");
|
let code = include_str!("inputs/pentagon_fillet_sugar.kcl");
|
||||||
|
Before Width: | Height: | Size: 170 KiB |
72
yarn.lock
@ -1831,10 +1831,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||||
|
|
||||||
"@kittycad/lib@^0.0.63":
|
"@kittycad/lib@^0.0.60":
|
||||||
version "0.0.63"
|
version "0.0.60"
|
||||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.63.tgz#cc70cf1c0780543bbca6f55aae40d0904cfd45d7"
|
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.60.tgz#478aa1f750ab05cd4e67503de96f2f3bbc075329"
|
||||||
integrity sha512-fDpGnycumT1xI/tSubRZzU9809/7s+m06w2EuJzxowgFrdIlvThnIHVf3EYvSujdFb0bHR/LZjodAw2ocXkXZw==
|
integrity sha512-LW9NFy2gv0pm1GJyquMXPiFKOBSdJJxYGkmacDton6jluGhAa8Qtcuj3O5vqUEeq9ObSM1Jt8gp39P9nvXG9yg==
|
||||||
dependencies:
|
dependencies:
|
||||||
node-fetch "3.3.2"
|
node-fetch "3.3.2"
|
||||||
openapi-types "^12.0.0"
|
openapi-types "^12.0.0"
|
||||||
@ -1961,10 +1961,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e"
|
resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e"
|
||||||
integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==
|
integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==
|
||||||
|
|
||||||
"@react-hook/resize-observer@^2.0.1":
|
"@react-hook/resize-observer@^1.2.6":
|
||||||
version "2.0.1"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/@react-hook/resize-observer/-/resize-observer-2.0.1.tgz#b1b090be25c74d89b371183e5e2bc0d03f94986f"
|
resolved "https://registry.yarnpkg.com/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz#9a8cf4c5abb09becd60d1d65f6bf10eec211e291"
|
||||||
integrity sha512-9PCX9grWfxdPizY8ohr+X4IkV1JhGMWr2Nm4ngbg6IcAIv0WBs7YoJcNBqYl22OqPHr5eOMItGcStZrmj2mbmQ==
|
integrity sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@juggle/resize-observer" "^3.3.1"
|
"@juggle/resize-observer" "^3.3.1"
|
||||||
"@react-hook/latest" "^1.0.2"
|
"@react-hook/latest" "^1.0.2"
|
||||||
@ -2111,16 +2111,16 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.11.tgz#7ec6f6f007da4e55315270619448ce39fd11b4e3"
|
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.11.tgz#7ec6f6f007da4e55315270619448ce39fd11b4e3"
|
||||||
integrity sha512-wJRY+fBUm3KpqZDHMIz5HRv+1vlnvRJ/dFxiyY3NlINTx2qXqDou5qWYcP1CuZXsd39InWVPV3FAZvno/kGCkA==
|
integrity sha512-wJRY+fBUm3KpqZDHMIz5HRv+1vlnvRJ/dFxiyY3NlINTx2qXqDou5qWYcP1CuZXsd39InWVPV3FAZvno/kGCkA==
|
||||||
|
|
||||||
"@tauri-apps/api@2.0.0-beta.12":
|
|
||||||
version "2.0.0-beta.12"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.12.tgz#0b552086e6382cfd5798537b304d00cbf42db7a1"
|
|
||||||
integrity sha512-77OvAnsExtiprnjQcvmDyZGfnIvMF/zVL5+8Vkl1R8o8E3iDtvEJZpbbH1F4dPtNa3gr4byp/5dm8hAa1+r3AA==
|
|
||||||
|
|
||||||
"@tauri-apps/api@2.0.0-beta.4":
|
"@tauri-apps/api@2.0.0-beta.4":
|
||||||
version "2.0.0-beta.4"
|
version "2.0.0-beta.4"
|
||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.4.tgz#7688950f6e03f38b3bac73585f8f4cdd61be6aa6"
|
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.4.tgz#7688950f6e03f38b3bac73585f8f4cdd61be6aa6"
|
||||||
integrity sha512-Nxtj28NYUo5iwYkpYslxmOPkdI2WkELU2e3UH9nbJm9Ydki2CQwJVGQxx4EANtdZcMNsEsUzRqaDTvEUYH1l6w==
|
integrity sha512-Nxtj28NYUo5iwYkpYslxmOPkdI2WkELU2e3UH9nbJm9Ydki2CQwJVGQxx4EANtdZcMNsEsUzRqaDTvEUYH1l6w==
|
||||||
|
|
||||||
|
"@tauri-apps/api@2.0.0-beta.8":
|
||||||
|
version "2.0.0-beta.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.8.tgz#36ac5365360312928bdf593d561cd490a8185a48"
|
||||||
|
integrity sha512-fN5u+9HsSfhRaXGOdD2kPGbqDgyX+nkm1XF/4d/LNuM4WiNfvHjjRAqWQYBhQsg1aF9nsTPmSW+tmy+Yn5T5+A==
|
||||||
|
|
||||||
"@tauri-apps/cli-darwin-arm64@2.0.0-beta.13":
|
"@tauri-apps/cli-darwin-arm64@2.0.0-beta.13":
|
||||||
version "2.0.0-beta.13"
|
version "2.0.0-beta.13"
|
||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.13.tgz#4926b310f5c39f967753c1c6b9aa20916011ebb6"
|
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.13.tgz#4926b310f5c39f967753c1c6b9aa20916011ebb6"
|
||||||
@ -8185,7 +8185,16 @@ string-natural-compare@^3.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^8.0.0"
|
||||||
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
|
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@ -8258,7 +8267,14 @@ string_decoder@~1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
|
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@ -8925,16 +8941,11 @@ vitest@^1.6.0:
|
|||||||
vite-node "1.6.0"
|
vite-node "1.6.0"
|
||||||
why-is-node-running "^2.2.2"
|
why-is-node-running "^2.2.2"
|
||||||
|
|
||||||
vscode-jsonrpc@8.2.0:
|
vscode-jsonrpc@8.2.0, vscode-jsonrpc@^8.1.0:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
|
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
|
||||||
integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
|
integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
|
||||||
|
|
||||||
vscode-jsonrpc@^8.2.1:
|
|
||||||
version "8.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz#a322cc0f1d97f794ffd9c4cd2a898a0bde097f34"
|
|
||||||
integrity sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==
|
|
||||||
|
|
||||||
vscode-languageserver-protocol@^3.17.5:
|
vscode-languageserver-protocol@^3.17.5:
|
||||||
version "3.17.5"
|
version "3.17.5"
|
||||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea"
|
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea"
|
||||||
@ -9240,7 +9251,7 @@ workerpool@6.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
|
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
|
||||||
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
|
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
@ -9258,6 +9269,15 @@ wrap-ansi@^6.2.0:
|
|||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
wrap-ansi@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^8.1.0:
|
wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
@ -9282,10 +9302,10 @@ ws@^7.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
|
||||||
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
||||||
|
|
||||||
ws@^8.17.0, ws@^8.8.0:
|
ws@^8.16.0, ws@^8.8.0:
|
||||||
version "8.17.0"
|
version "8.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
|
||||||
integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==
|
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==
|
||||||
|
|
||||||
"xstate-beta@npm:xstate@beta":
|
"xstate-beta@npm:xstate@beta":
|
||||||
version "5.0.0-beta.54"
|
version "5.0.0-beta.54"
|
||||||
|