Compare commits

..

4 Commits

Author SHA1 Message Date
c6f43cb607 WIP 2024-06-06 16:09:27 -05:00
9df69fb1c2 Further factor out functions 2024-06-06 13:55:40 -05:00
24f69a2b6f Add hyper for tests 2024-06-06 13:41:47 -05:00
cf6f474dc6 Factor ExecutionCtx into its own fn 2024-06-06 11:48:15 -05:00
20 changed files with 328 additions and 528 deletions

View File

@ -150,7 +150,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Cache Playwright Browsers - name: Cache Playwright Browsers
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
~/.cache/ms-playwright ~/.cache/ms-playwright

View File

@ -17,7 +17,6 @@ import {
TEST_SETTINGS_CORRUPTED, TEST_SETTINGS_CORRUPTED,
TEST_SETTINGS_ONBOARDING_EXPORT, TEST_SETTINGS_ONBOARDING_EXPORT,
TEST_SETTINGS_ONBOARDING_START, TEST_SETTINGS_ONBOARDING_START,
TEST_CODE_GIZMO,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
import { LineInputsType } from 'lang/std/sketchcombos' import { LineInputsType } from 'lang/std/sketchcombos'
@ -332,15 +331,15 @@ test('if you click the format button it formats your code', async ({
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click() await page.click('.cm-content')
await page.keyboard.type(`const sketch001 = startSketchOn('XY') await page.keyboard.type(`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
await page.locator('#code-pane button:first-child').click() await page.click('#code-pane button:first-child')
await page.locator('button:has-text("Format code")').click() await page.click('button:has-text("Format code")')
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XY') .toHaveText(`const sketch001 = startSketchOn('XY')
@ -388,7 +387,7 @@ test('if you use the format keyboard binding it formats your code', async ({
await u.closeDebugPanel() await u.closeDebugPanel()
// focus the editor // focus the editor
await u.codeLocator.click() await page.click('.cm-line')
// Hit alt+shift+f to format the code // Hit alt+shift+f to format the code
await page.keyboard.press('Alt+Shift+KeyF') await page.keyboard.press('Alt+Shift+KeyF')
@ -426,7 +425,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const topAng = 30 const topAng = 30
const bottomAng = 25 const bottomAng = 25
*/ */
await u.codeLocator.click() await page.click('.cm-content')
await page.keyboard.type('$ error') await page.keyboard.type('$ error')
// press arrows to clear autocomplete // press arrows to clear autocomplete
@ -523,7 +522,7 @@ fn squareHole = (l, w) => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click on the bottom of the code editor to add a new line // Click on the bottom of the code editor to add a new line
await u.codeLocator.click() await page.click('.cm-content')
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
@ -788,7 +787,7 @@ test('Auto complete works', async ({ page }) => {
// tests clicking on an option, selection the first option // tests clicking on an option, selection the first option
// and arrowing down to an option // and arrowing down to an option
await u.codeLocator.click() await page.click('.cm-content')
await page.keyboard.type('const sketch001 = start') await page.keyboard.type('const sketch001 = start')
// expect there to be six auto complete options // expect there to be six auto complete options
@ -930,7 +929,7 @@ test('Project settings can be opened with keybinding from the editor', async ({
.waitFor({ state: 'visible' }) .waitFor({ state: 'visible' })
// Put the cursor in the editor // Put the cursor in the editor
await page.locator('.cm-content').click() await page.click('.cm-content')
// Open the settings modal with the browser keyboard shortcut // Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,') await page.keyboard.press('Meta+Shift+,')
@ -979,7 +978,7 @@ test('Project and user settings can be reset', async ({ page }) => {
.waitFor({ state: 'visible' }) .waitFor({ state: 'visible' })
// Put the cursor in the editor // Put the cursor in the editor
await page.locator('.cm-content').click() await page.click('.cm-content')
// Open the settings modal with the browser keyboard shortcut // Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,') await page.keyboard.press('Meta+Shift+,')
@ -1558,7 +1557,7 @@ test.describe('Command bar tests', () => {
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
// Put the cursor in the code editor // Put the cursor in the code editor
await page.locator('.cm-content').click() await page.click('.cm-content')
// Now try the same, but with the keyboard shortcut, check focus // Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
@ -3480,24 +3479,14 @@ test.describe('Testing segment overlays', () => {
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
await page.getByText('xLineTo(5 + 9 - 5, %)').click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
const clickUnconstrained = _clickUnconstrained(page)
const clickConstrained = _clickConstrained(page)
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.sendCustomCmd({ await u.sendCustomCmd({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'default_camera_look_at', type: 'default_camera_look_at',
vantage: { x: 80, y: -1350, z: 510 }, vantage: { x: 0, y: -1250, z: 580 },
center: { x: 80, y: 0, z: 510 }, center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 }, up: { x: 0, y: 0, z: 1 },
}, },
}) })
@ -3512,6 +3501,27 @@ test.describe('Testing segment overlays', () => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.getByText('xLineTo(5 + 9 - 5, %)').click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
const clickUnconstrained = _clickUnconstrained(page)
const clickConstrained = _clickConstrained(page)
// Drag the sketch into view
await page.mouse.move(600, 64)
await page.mouse.down({ button: 'middle' })
await page.mouse.move(600, 450, { steps: 10 })
await page.mouse.up({ button: 'middle' })
await page.mouse.move(600, 64)
await page.mouse.down({ button: 'middle' })
await page.mouse.move(600, 120, { steps: 10 })
await page.mouse.up({ button: 'middle' })
let ang = 0 let ang = 0
const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`) const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
@ -3560,8 +3570,11 @@ test.describe('Testing segment overlays', () => {
}) })
await page.mouse.move(700, 250) await page.mouse.move(700, 250)
await page.mouse.wheel(0, 25) for (let i = 0; i < 5; i++) {
await page.waitForTimeout(100) await page.mouse.wheel(0, 100)
await page.waitForTimeout(25)
}
await page.waitForTimeout(200)
let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`) let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
ang = await u.getAngle(`[data-overlay-index="2"]`) ang = await u.getAngle(`[data-overlay-index="2"]`)
@ -3643,8 +3656,12 @@ const part001 = startSketchOn('XZ')
const clickUnconstrained = _clickUnconstrained(page) const clickUnconstrained = _clickUnconstrained(page)
await page.mouse.move(700, 250) await page.mouse.move(700, 250)
await page.mouse.wheel(0, 25) for (let i = 0; i < 7; i++) {
await page.waitForTimeout(100) await page.mouse.wheel(0, 100)
await page.waitForTimeout(25)
}
await page.waitForTimeout(300)
let ang = 0 let ang = 0
@ -4845,25 +4862,25 @@ test.describe('Testing Gizmo', () => {
expectedCameraTarget: { x: 800, y: -152, z: 26 }, expectedCameraTarget: { x: 800, y: -152, z: 26 },
}, },
{ {
testDescription: 'right view', testDescription: '+x view',
clickPosition: { x: 929, y: 417 }, clickPosition: { x: 929, y: 417 },
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 }, expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 }, expectedCameraTarget: { x: 800, y: -152, z: 26 },
}, },
{ {
testDescription: 'left view', testDescription: '-x view',
clickPosition: { x: 974, y: 397 }, clickPosition: { x: 974, y: 397 },
expectedCameraPosition: { x: -4060.02, y: -152, z: 26 }, expectedCameraPosition: { x: -4060.02, y: -152, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 }, expectedCameraTarget: { x: 800, y: -152, z: 26 },
}, },
{ {
testDescription: 'back view', testDescription: '+y view',
clickPosition: { x: 967, y: 421 }, clickPosition: { x: 967, y: 421 },
expectedCameraPosition: { x: 800, y: 4708.02, z: 26 }, expectedCameraPosition: { x: 800, y: 4708.02, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 }, expectedCameraTarget: { x: 800, y: -152, z: 26 },
}, },
{ {
testDescription: 'front view', testDescription: '-y view',
clickPosition: { x: 935, y: 393 }, clickPosition: { x: 935, y: 393 },
expectedCameraPosition: { x: 800, y: -5012.02, z: 26 }, expectedCameraPosition: { x: 800, y: -5012.02, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 }, expectedCameraTarget: { x: 800, y: -152, z: 26 },
@ -4875,11 +4892,34 @@ test.describe('Testing Gizmo', () => {
expectedCameraTarget, expectedCameraTarget,
testDescription, testDescription,
} of cases) { } of cases) {
test(`check ${testDescription}`, async ({ page, browserName }) => { test(`check ${testDescription}`, async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript((TEST_CODE_GIZMO) => { await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO) localStorage.setItem(
}, TEST_CODE_GIZMO) 'persistCode',
`const part001 = startSketchOn('XZ')
|> startProfileAt([20, 0], %)
|> line([7.13, 4 + 0], %)
|> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %)
|> lineTo([20.14 + 0, -0.14 + 0], %)
|> xLineTo(29 + 0, %)
|> yLine(-3.14 + 0, %, 'a')
|> xLine(1.63, %)
|> angledLineOfXLength({ angle: 3 + 0, length: 3.14 }, %)
|> angledLineOfYLength({ angle: 30, length: 3 + 0 }, %)
|> angledLineToX({ angle: 22.14 + 0, to: 12 }, %)
|> angledLineToY({ angle: 30, to: 11.14 }, %)
|> angledLineThatIntersects({
angle: 3.14,
intersectTag: 'a',
offset: 0
}, %)
|> tangentialArcTo([13.14 + 0, 13.14], %)
|> close(%)
|> extrude(5 + 7, %)
`
)
}, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -4916,11 +4956,11 @@ test.describe('Testing Gizmo', () => {
}) })
await u.waitForCmdReceive('default_camera_get_settings') await u.waitForCmdReceive('default_camera_get_settings')
await u.clearCommandLogs() await page.waitForTimeout(400)
await page.mouse.move(clickPosition.x, clickPosition.y) await page.mouse.move(clickPosition.x, clickPosition.y)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.clearCommandLogs()
await page.mouse.click(clickPosition.x, clickPosition.y) await page.mouse.click(clickPosition.x, clickPosition.y)
await page.mouse.move(0, 0)
await u.waitForCmdReceive('default_camera_look_at') await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs() await u.clearCommandLogs()
@ -4932,6 +4972,7 @@ test.describe('Testing Gizmo', () => {
}, },
}) })
await u.waitForCmdReceive('default_camera_get_settings') await u.waitForCmdReceive('default_camera_get_settings')
await page.waitForTimeout(400)
await Promise.all([ await Promise.all([
// position // position
@ -4957,103 +4998,6 @@ test.describe('Testing Gizmo', () => {
]) ])
}) })
} }
test('Context menu', async ({ page }) => {
const testCase = {
testDescription: 'Right view',
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
}
// Test prelude taken from the above test
const u = await getUtils(page)
await page.addInitScript((TEST_CODE_GIZMO) => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
}, TEST_CODE_GIZMO)
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(100)
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: {
x: 3000,
y: 3000,
z: 3000,
},
center: {
x: 800,
y: -152,
z: 26,
},
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.clearCommandLogs()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await u.waitForCmdReceive('default_camera_get_settings')
// Now find and select the correct
// view from the context menu
await u.clearCommandLogs()
const gizmo = page.locator('[aria-label*=gizmo]')
await gizmo.click({ button: 'right' })
const buttonToTest = page.getByRole('button', {
name: testCase.testDescription,
})
await expect(buttonToTest).toBeVisible()
await buttonToTest.click()
// Now assert we've moved to the correct view
// Taken from the above test
await u.waitForCmdReceive('default_camera_look_at')
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await u.waitForCmdReceive('default_camera_get_settings')
await page.waitForTimeout(400)
await Promise.all([
// position
expect(page.getByTestId('cam-x-position')).toHaveValue(
testCase.expectedCameraPosition.x.toString()
),
expect(page.getByTestId('cam-y-position')).toHaveValue(
testCase.expectedCameraPosition.y.toString()
),
expect(page.getByTestId('cam-z-position')).toHaveValue(
testCase.expectedCameraPosition.z.toString()
),
// target
expect(page.getByTestId('cam-x-target')).toHaveValue(
testCase.expectedCameraTarget.x.toString()
),
expect(page.getByTestId('cam-y-target')).toHaveValue(
testCase.expectedCameraTarget.y.toString()
),
expect(page.getByTestId('cam-z-target')).toHaveValue(
testCase.expectedCameraTarget.z.toString()
),
])
})
}) })
test('Successful export shows a success toast', async ({ page }) => { test('Successful export shows a success toast', async ({ page }) => {

View File

@ -50,25 +50,3 @@ export const TEST_SETTINGS_CORRUPTED = {
textWrapping: true, textWrapping: true,
}, },
} satisfies Partial<SaveSettingsPayload> } satisfies Partial<SaveSettingsPayload>
export const TEST_CODE_GIZMO = `const part001 = startSketchOn('XZ')
|> startProfileAt([20, 0], %)
|> line([7.13, 4 + 0], %)
|> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %)
|> lineTo([20.14 + 0, -0.14 + 0], %)
|> xLineTo(29 + 0, %)
|> yLine(-3.14 + 0, %, 'a')
|> xLine(1.63, %)
|> angledLineOfXLength({ angle: 3 + 0, length: 3.14 }, %)
|> angledLineOfYLength({ angle: 30, length: 3 + 0 }, %)
|> angledLineToX({ angle: 22.14 + 0, to: 12 }, %)
|> angledLineToY({ angle: 30, to: 11.14 }, %)
|> angledLineThatIntersects({
angle: 3.14,
intersectTag: 'a',
offset: 0
}, %)
|> tangentialArcTo([13.14 + 0, 13.14], %)
|> close(%)
|> extrude(5 + 7, %)
`

View File

@ -12,16 +12,14 @@ async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner // wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor() await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone // wait for all spinners to be gone
await page await page.getByTestId('loading').waitFor({ state: 'detached' })
.getByTestId('loading')
.waitFor({ state: 'detached', timeout: 20_000 })
await page.getByTestId('start-sketch').waitFor() await page.getByTestId('start-sketch').waitFor()
} }
async function removeCurrentCode(page: Page) { async function removeCurrentCode(page: Page) {
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control' const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.locator('.cm-content').click() await page.click('.cm-content')
await page.keyboard.down(hotkey) await page.keyboard.down(hotkey)
await page.keyboard.press('a') await page.keyboard.press('a')
await page.keyboard.up(hotkey) await page.keyboard.up(hotkey)
@ -30,12 +28,12 @@ async function removeCurrentCode(page: Page) {
} }
async function sendCustomCmd(page: Page, cmd: EngineCommand) { async function sendCustomCmd(page: Page, cmd: EngineCommand) {
await page.getByTestId('custom-cmd-input').fill(JSON.stringify(cmd)) await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd))
await page.getByTestId('custom-cmd-send-button').click() await page.click('[data-testid="custom-cmd-send-button"]')
} }
async function clearCommandLogs(page: Page) { async function clearCommandLogs(page: Page) {
await page.getByTestId('clear-commands').click() await page.click('[data-testid="clear-commands"]')
} }
async function expectCmdLog(page: Page, locatorStr: string) { async function expectCmdLog(page: Page, locatorStr: string) {

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.22.1", "version": "0.22.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",

109
src-tauri/Cargo.lock generated
View File

@ -200,7 +200,7 @@ dependencies = [
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-updater", "tauri-plugin-updater",
"tokio", "tokio",
"toml 0.8.14", "toml 0.8.13",
"url", "url",
] ]
@ -766,7 +766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
dependencies = [ dependencies = [
"serde", "serde",
"toml 0.8.14", "toml 0.8.13",
] ]
[[package]] [[package]]
@ -1373,7 +1373,7 @@ dependencies = [
"cc", "cc",
"memchr", "memchr",
"rustc_version", "rustc_version",
"toml 0.8.14", "toml 0.8.13",
"vswhom", "vswhom",
"winreg 0.52.0", "winreg 0.52.0",
] ]
@ -2578,6 +2578,8 @@ dependencies = [
"gltf-json", "gltf-json",
"js-sys", "js-sys",
"kittycad", "kittycad",
"kittycad-execution-plan-macros",
"kittycad-execution-plan-traits",
"lazy_static", "lazy_static",
"mime_guess", "mime_guess",
"parse-display", "parse-display",
@ -2590,7 +2592,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"toml 0.8.14", "toml 0.8.13",
"tower-lsp", "tower-lsp",
"ts-rs", "ts-rs",
"url", "url",
@ -2653,6 +2655,28 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "kittycad-execution-plan-macros"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0611fc9b9786175da21d895ffa0f65039e19c9111e94a41b7af999e3b95f045f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "kittycad-execution-plan-traits"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "123cb47e2780ea8ef3aa67b4db237a27b388d3d3b96db457e274aa4565723151"
dependencies = [
"serde",
"thiserror",
"uuid",
]
[[package]] [[package]]
name = "kuchikiki" name = "kuchikiki"
version = "0.8.2" version = "0.8.2"
@ -3329,9 +3353,9 @@ dependencies = [
[[package]] [[package]]
name = "parse-display" name = "parse-display"
version = "0.9.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" checksum = "06af5f9333eb47bd9ba8462d612e37a8328a5cb80b13f0af4de4c3b89f52dee5"
dependencies = [ dependencies = [
"parse-display-derive", "parse-display-derive",
"regex", "regex",
@ -3340,9 +3364,9 @@ dependencies = [
[[package]] [[package]]
name = "parse-display-derive" name = "parse-display-derive"
version = "0.9.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" checksum = "dc9252f259500ee570c75adcc4e317fa6f57a1e47747d622e0bf838002a7b790"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3711,9 +3735,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.85" version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -4275,19 +4299,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls"
version = "0.23.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki 0.102.3",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.7.0" version = "0.7.0"
@ -5031,7 +5042,7 @@ dependencies = [
"cfg-expr", "cfg-expr",
"heck 0.5.0", "heck 0.5.0",
"pkg-config", "pkg-config",
"toml 0.8.14", "toml 0.8.13",
"version-compare", "version-compare",
] ]
@ -5184,7 +5195,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"tauri-winres", "tauri-winres",
"toml 0.8.14", "toml 0.8.13",
"walkdir", "walkdir",
] ]
@ -5242,7 +5253,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"toml 0.8.14", "toml 0.8.13",
"walkdir", "walkdir",
] ]
@ -5519,7 +5530,7 @@ dependencies = [
"serde_with", "serde_with",
"swift-rs", "swift-rs",
"thiserror", "thiserror",
"toml 0.8.14", "toml 0.8.13",
"url", "url",
"urlpattern", "urlpattern",
"walkdir", "walkdir",
@ -5653,9 +5664,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.0" version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -5673,9 +5684,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.3.0" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5703,30 +5714,19 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.7",
"rustls-pki-types",
"tokio",
]
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.23.0" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0" checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
"rustls 0.23.7", "rustls 0.22.4",
"rustls-native-certs", "rustls-native-certs",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.25.0",
"tungstenite", "tungstenite",
] ]
@ -5758,14 +5758,14 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.14" version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_edit 0.22.14", "toml_edit 0.22.13",
] ]
[[package]] [[package]]
@ -5814,9 +5814,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.14" version = "0.22.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
dependencies = [ dependencies = [
"indexmap 2.2.6", "indexmap 2.2.6",
"serde", "serde",
@ -6035,9 +6035,9 @@ dependencies = [
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.23.0" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
@ -6046,10 +6046,11 @@ dependencies = [
"httparse", "httparse",
"log", "log",
"rand 0.8.5", "rand 0.8.5",
"rustls 0.23.7", "rustls 0.22.4",
"rustls-pki-types", "rustls-pki-types",
"sha1", "sha1",
"thiserror", "thiserror",
"url",
"utf-8", "utf-8",
] ]

View File

@ -74,5 +74,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.22.1" "version": "0.22.0"
} }

View File

@ -444,7 +444,7 @@ export class CameraControls {
this.handleEnd() this.handleEnd()
return return
} }
this.engineCommandManager.sendSceneCommand({ this.throttledEngCmd({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'default_camera_zoom', type: 'default_camera_zoom',
@ -456,11 +456,11 @@ export class CameraControls {
return return
} }
// Else "clientToEngine" (Sketch Mode) or forceUpdate const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
// From onMouseMove zoom handling which seems to be really smooth const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1 this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + event.deltaY * 0.01 this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
this.handleEnd() this.handleEnd()
} }

View File

@ -1,199 +0,0 @@
import toast from 'react-hot-toast'
import { ActionIcon, ActionIconProps } from './ActionIcon'
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { Dialog } from '@headlessui/react'
interface ContextMenuProps
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
items?: React.ReactElement[]
menuTargetElement?: RefObject<HTMLElement>
}
const DefaultContextMenuItems = [
<ContextMenuItemRefresh />,
<ContextMenuItemCopy />,
// add more default context menu items here
]
export function ContextMenu({
items = DefaultContextMenuItems,
menuTargetElement,
className,
...props
}: ContextMenuProps) {
const dialogRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
const [windowSize, setWindowSize] = useState({
width: globalThis?.window?.innerWidth,
height: globalThis?.window?.innerHeight,
})
const [position, setPosition] = useState({ x: 0, y: 0 })
useHotkeys('esc', () => setOpen(false), {
enabled: open,
})
const dialogPositionStyle = useMemo(() => {
if (!dialogRef.current)
return {
top: 0,
left: 0,
right: 'auto',
bottom: 'auto',
}
return {
top:
position.y + dialogRef.current.clientHeight > windowSize.height
? 'auto'
: position.y,
left:
position.x + dialogRef.current.clientWidth > windowSize.width
? 'auto'
: position.x,
right:
position.x + dialogRef.current.clientWidth > windowSize.width
? windowSize.width - position.x
: 'auto',
bottom:
position.y + dialogRef.current.clientHeight > windowSize.height
? windowSize.height - position.y
: 'auto',
}
}, [position, windowSize, dialogRef.current])
// Listen for window resize to update context menu position
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: globalThis?.window?.innerWidth,
height: globalThis?.window?.innerHeight,
})
}
globalThis?.window?.addEventListener('resize', handleResize)
return () => {
globalThis?.window?.removeEventListener('resize', handleResize)
}
}, [])
// Add context menu listener to target once mounted
useEffect(() => {
const handleContextMenu = (e: MouseEvent) => {
console.log('context menu', e)
e.preventDefault()
setPosition({ x: e.x, y: e.y })
setOpen(true)
}
menuTargetElement?.current?.addEventListener(
'contextmenu',
handleContextMenu
)
return () => {
menuTargetElement?.current?.removeEventListener(
'contextmenu',
handleContextMenu
)
}
}, [menuTargetElement?.current])
return (
<Dialog open={open} onClose={() => setOpen(false)}>
<div
className="fixed inset-0 z-50 w-screen h-screen"
onContextMenu={(e) => e.preventDefault()}
>
<Dialog.Backdrop className="fixed z-10 inset-0" />
<Dialog.Panel
ref={dialogRef}
className={`w-48 fixed bg-chalkboard-10 dark:bg-chalkboard-90
border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded
shadow-lg backdrop:fixed backdrop:inset-0 backdrop:bg-primary ${className}`}
style={{
...dialogPositionStyle,
...props.style,
}}
>
<ul
{...props}
className="relative flex flex-col gap-0.5 items-stretch content-stretch"
onClick={() => setOpen(false)}
>
{...items}
</ul>
</Dialog.Panel>
</div>
</Dialog>
)
}
export function ContextMenuDivider() {
return <hr className="border-chalkboard-20 dark:border-chalkboard-80" />
}
interface ContextMenuItemProps {
children: React.ReactNode
icon?: ActionIconProps['icon']
onClick?: () => void
hotkey?: string
}
export function ContextMenuItem({
children,
icon,
onClick,
hotkey,
}: ContextMenuItemProps) {
return (
<button
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={onClick}
>
{icon && <ActionIcon icon={icon} bgClassName="!bg-transparent" />}
<div className="flex-1">{children}</div>
{hotkey && (
<kbd className="px-1.5 py-0.5 rounded bg-primary/10 text-primary dark:bg-chalkboard-80 dark:text-chalkboard-40">
{hotkey}
</kbd>
)}
</button>
)
}
export function ContextMenuItemRefresh() {
return (
<ContextMenuItem
icon="arrowRotateRight"
onClick={() => globalThis?.window?.location.reload()}
>
Refresh
</ContextMenuItem>
)
}
interface ContextMenuItemCopyProps {
toBeCopiedContent?: string
toBeCopiedLabel?: string
}
export function ContextMenuItemCopy({
toBeCopiedContent = globalThis.window?.getSelection()?.toString(),
toBeCopiedLabel = 'selection',
}: ContextMenuItemCopyProps) {
return (
<ContextMenuItem
icon="clipboardPlus"
onClick={() => {
if (toBeCopiedContent) {
globalThis?.navigator?.clipboard
.writeText(toBeCopiedContent)
.then(() => toast.success(`Copied ${toBeCopiedLabel} to clipboard`))
.catch(() =>
toast.error(`Failed to copy ${toBeCopiedLabel} to clipboard`)
)
}
}}
>
Copy
</ContextMenuItem>
)
}

View File

@ -18,8 +18,6 @@ import { useLspContext } from './LspProvider'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog' import { DeleteConfirmationDialog } from './ProjectCard/DeleteProjectDialog'
import { ContextMenu, ContextMenuItem } from './ContextMenu'
import usePlatform from 'hooks/usePlatform'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` return `calc(1rem * ${level + 1})`
@ -127,7 +125,6 @@ const FileTreeItem = ({
const navigate = useNavigate() const navigate = useNavigate()
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const isCurrentFile = fileOrDir.path === currentFile?.path const isCurrentFile = fileOrDir.path === currentFile?.path
const itemRef = useRef(null)
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path) const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
const removeCurrentItemFromRenaming = useCallback( const removeCurrentItemFromRenaming = useCallback(
@ -188,7 +185,7 @@ const FileTreeItem = ({
} }
return ( return (
<div className="contents" ref={itemRef}> <>
{fileOrDir.children === undefined ? ( {fileOrDir.children === undefined ? (
<li <li
className={ className={
@ -324,41 +321,7 @@ const FileTreeItem = ({
setIsOpen={setIsConfirmingDelete} setIsOpen={setIsConfirmingDelete}
/> />
)} )}
<FileTreeContextMenu </>
itemRef={itemRef}
onRename={addCurrentItemToRenaming}
onDelete={() => setIsConfirmingDelete(true)}
/>
</div>
)
}
interface FileTreeContextMenuProps {
itemRef: React.RefObject<HTMLElement>
onRename: () => void
onDelete: () => void
}
function FileTreeContextMenu({
itemRef,
onRename,
onDelete,
}: FileTreeContextMenuProps) {
const platform = usePlatform()
const metaKey = platform === 'macos' ? '⌘' : 'Ctrl'
return (
<ContextMenu
menuTargetElement={itemRef}
items={[
<ContextMenuItem onClick={onRename} hotkey="Enter">
Rename
</ContextMenuItem>,
<ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}>
Delete
</ContextMenuItem>,
]}
/>
) )
} }

View File

@ -19,12 +19,6 @@ import {
Intersection, Intersection,
Object3D, Object3D,
} from 'three' } from 'three'
import {
ContextMenu,
ContextMenuDivider,
ContextMenuItem,
ContextMenuItemRefresh,
} from './ContextMenu'
const CANVAS_SIZE = 80 const CANVAS_SIZE = 80
const FRUSTUM_SIZE = 0.5 const FRUSTUM_SIZE = 0.5
@ -44,17 +38,8 @@ enum AxisNames {
NEG_Y = '-y', NEG_Y = '-y',
NEG_Z = '-z', NEG_Z = '-z',
} }
const axisNamesSemantic: Record<AxisNames, string> = {
[AxisNames.X]: 'Right',
[AxisNames.Y]: 'Back',
[AxisNames.Z]: 'Top',
[AxisNames.NEG_X]: 'Left',
[AxisNames.NEG_Y]: 'Front',
[AxisNames.NEG_Z]: 'Bottom',
}
export default function Gizmo() { export default function Gizmo() {
const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null) const canvasRef = useRef<HTMLCanvasElement | null>(null)
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null) const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
const cameraPassiveUpdateTimer = useRef(0) const cameraPassiveUpdateTimer = useRef(0)
@ -115,43 +100,9 @@ export default function Gizmo() {
}, []) }, [])
return ( return (
<> <div className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-none">
<div <canvas ref={canvasRef} />
ref={wrapperRef} </div>
aria-label="View orientation gizmo"
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto"
>
<canvas ref={canvasRef} />
<ContextMenu
menuTargetElement={wrapperRef}
items={[
...Object.entries(axisNamesSemantic).map(
([axisName, axisSemantic]) => (
<ContextMenuItem
key={axisName}
onClick={() => {
sceneInfra.camControls.updateCameraToAxis(
axisName as AxisNames
)
}}
>
{axisSemantic} view
</ContextMenuItem>
)
),
<ContextMenuItem
onClick={() => {
sceneInfra.camControls.resetCameraPosition()
}}
>
Reset view
</ContextMenuItem>,
<ContextMenuDivider />,
<ContextMenuItemRefresh />,
]}
/>
</div>
</>
) )
} }

View File

@ -312,6 +312,8 @@ class EngineConnection extends EventTarget {
if (next.type === EngineConnectionStateType.Disconnecting) { if (next.type === EngineConnectionStateType.Disconnecting) {
const sub = next.value const sub = next.value
if (sub.type === DisconnectingType.Error) { if (sub.type === DisconnectingType.Error) {
console.log(sub)
// Record the last step we failed at. // Record the last step we failed at.
// (Check the current state that we're about to override that // (Check the current state that we're about to override that
// it was a Connecting state.) // it was a Connecting state.)
@ -754,6 +756,8 @@ class EngineConnection extends EventTarget {
// when assuming we're the only consumer or that all messages will // when assuming we're the only consumer or that all messages will
// be carefully formatted here. // be carefully formatted here.
console.log(event)
if (typeof event.data !== 'string') { if (typeof event.data !== 'string') {
return return
} }
@ -774,6 +778,7 @@ class EngineConnection extends EventTarget {
`Error in response to request ${message.request_id}:\n${errorsString} `Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.commandType}` failed cmd type was ${artifactThatFailed?.commandType}`
) )
console.log(artifactThatFailed)
} else { } else {
console.error(`Error from server:\n${errorsString}`) console.error(`Error from server:\n${errorsString}`)
} }
@ -864,6 +869,7 @@ class EngineConnection extends EventTarget {
this.pc this.pc
?.createOffer() ?.createOffer()
.then((offer: RTCSessionDescriptionInit) => { .then((offer: RTCSessionDescriptionInit) => {
console.log(offer)
this.state = { this.state = {
type: EngineConnectionStateType.Connecting, type: EngineConnectionStateType.Connecting,
value: { value: {
@ -935,6 +941,7 @@ class EngineConnection extends EventTarget {
case 'trickle_ice': case 'trickle_ice':
let candidate = resp.data?.candidate let candidate = resp.data?.candidate
console.log('trickle_ice: using this candidate: ', candidate)
void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit) void this.pc?.addIceCandidate(candidate as RTCIceCandidateInit)
break break
@ -1337,6 +1344,8 @@ export class EngineCommandManager extends EventTarget {
this.engineConnection?.addEventListener( this.engineConnection?.addEventListener(
EngineConnectionEvents.NewTrack, EngineConnectionEvents.NewTrack,
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => { (({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
console.log('received track', mediaStream)
mediaStream.getVideoTracks()[0].addEventListener('mute', () => { mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
console.error( console.error(
'video track mute: check webrtc internals -> inbound rtp' 'video track mute: check webrtc internals -> inbound rtp'
@ -1667,6 +1676,7 @@ export class EngineCommandManager extends EventTarget {
command.type === 'modeling_cmd_req' && command.type === 'modeling_cmd_req' &&
command.cmd.type !== lastMessage command.cmd.type !== lastMessage
) { ) {
console.log('sending command', command.cmd.type)
lastMessage = command.cmd.type lastMessage = command.cmd.type
} }
if (command.type === 'modeling_cmd_batch_req') { if (command.type === 'modeling_cmd_batch_req') {

View File

@ -1138,6 +1138,16 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-body"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http 1.1.0",
]
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.8.0"
@ -1162,7 +1172,7 @@ dependencies = [
"futures-util", "futures-util",
"h2", "h2",
"http 0.2.12", "http 0.2.12",
"http-body", "http-body 0.4.6",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
@ -1174,6 +1184,21 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [
"bytes",
"http 1.1.0",
"http-body 1.0.0",
"httpdate",
"pin-project-lite",
"smallvec",
"tokio",
]
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.24.2" version = "0.24.2"
@ -1182,7 +1207,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"http 0.2.12", "http 0.2.12",
"hyper", "hyper 0.14.29",
"rustls 0.21.12", "rustls 0.21.12",
"tokio", "tokio",
"tokio-rustls 0.24.1", "tokio-rustls 0.24.1",
@ -1434,6 +1459,15 @@ dependencies = [
"syn 2.0.66", "syn 2.0.66",
] ]
[[package]]
name = "kcl-test-server"
version = "0.1.0"
dependencies = [
"hyper 1.3.1",
"kcl-lib",
"tokio",
]
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.3.3" version = "0.3.3"
@ -2095,8 +2129,8 @@ dependencies = [
"futures-util", "futures-util",
"h2", "h2",
"http 0.2.12", "http 0.2.12",
"http-body", "http-body 0.4.6",
"hyper", "hyper 0.14.29",
"hyper-rustls", "hyper-rustls",
"ipnet", "ipnet",
"js-sys", "js-sys",
@ -2165,7 +2199,7 @@ dependencies = [
"futures", "futures",
"getrandom", "getrandom",
"http 0.2.12", "http 0.2.12",
"hyper", "hyper 0.14.29",
"parking_lot 0.11.2", "parking_lot 0.11.2",
"reqwest", "reqwest",
"reqwest-middleware", "reqwest-middleware",
@ -2975,9 +3009,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.14" version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -2996,9 +3030,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.14" version = "0.22.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
dependencies = [ dependencies = [
"indexmap 2.2.5", "indexmap 2.2.5",
"serde", "serde",
@ -3444,7 +3478,7 @@ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"futures", "futures",
"gloo-utils", "gloo-utils",
"hyper", "hyper 0.14.29",
"image", "image",
"js-sys", "js-sys",
"kcl-lib", "kcl-lib",

View File

@ -17,7 +17,7 @@ kcl-lib = { path = "kcl" }
kittycad = { workspace = true } kittycad = { workspace = true }
serde_json = "1.0.116" serde_json = "1.0.116"
tokio = { version = "1.38.0", features = ["sync"] } tokio = { version = "1.38.0", features = ["sync"] }
toml = "0.8.14" toml = "0.8.13"
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] } uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91" wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42" wasm-bindgen-futures = "0.4.42"
@ -65,6 +65,7 @@ members = [
"derive-docs", "derive-docs",
"kcl", "kcl",
"kcl-macros", "kcl-macros",
"kcl-test-server",
] ]
[workspace.dependencies] [workspace.dependencies]

View File

@ -0,0 +1,24 @@
[package]
name = "grackle"
version = "0.1.0"
edition = "2021"
description = "A new executor for KCL which compiles to Execution Plans"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
image = { version = "0.25.1", default-features = false, features = ["png"] }
kcl-lib = { path = "../kcl" }
kittycad = { workspace = true }
kittycad-execution-plan = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
kittycad-execution-plan-macros = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
kittycad-modeling-session = { workspace = true }
thiserror = "1.0.61"
tokio = { version = "1.37.0", features = ["macros", "rt"] }
twenty-twenty = "0.8.0"
uuid = "1.8"
[dev-dependencies]
pretty_assertions = "1"
serde_json = "1.0.116"

View File

@ -0,0 +1,9 @@
[package]
name = "kcl-test-server"
version = "0.1.0"
edition = "2021"
[dependencies]
hyper = { version = "1.3.1", features = ["server"] }
kcl-lib = { path = "../kcl" }
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }

View File

@ -0,0 +1,67 @@
use std::net::SocketAddr;
use hyper::header::CONTENT_TYPE;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Error, Response, Server};
use kcl_lib::executor::ExecutorContext;
use kcl_lib::settings::types::UnitLength;
use crate::new_context;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let listen_on = std::env::args().skip(1).next().unwrap();
start(listen_on).await?
}
pub async fn start(listen_on: SocketAddr) -> anyhow::Result<()> {
let state: ExecutorContext = new_context(UnitLength::Mm).await?;
// In hyper, a `MakeService` is basically your server.
// It makes a `Service` for each connection, which manages the connection.
let make_service = make_service_fn(
// This closure is run for each connection.
move |_| {
let state = state.clone();
async move {
// This is the `Service` which handles the connection.
// `service_fn` converts a function which returns a Response
// into a `Service`.
Ok::<_, Error>(service_fn(move |req| {
// Return a response.
async move {
let whole_body = hyper::body::to_bytes(req.into_body()).await?;
let Ok(kcl_src_code) = String::from_utf8(whole_body.into()) else {
return Ok(bad_request("Body was not UTF-8".to_owned()));
};
let parser = match kcl_lib::token::lexer(&kcl_src_code) {
Ok(t) => kcl_lib::parser::Parser::new(t),
Err(e) => return Ok(bad_request(format!("tokenization error: {e}"))),
};
let program = match parser.ast() {
Ok(p) => p,
Err(e) => return Ok(bad_request(format!("Parse error: {e}"))),
};
let png_bytes: Vec<u8> = todo!();
let mut resp = Response::new(Body::from(png_bytes));
resp.headers_mut().insert(CONTENT_TYPE, "image/png".parse().unwrap());
Ok::<_, Error>(resp)
}
}))
}
},
);
let server = Server::bind(&listen_on).serve(make_service);
println!("Listening on {listen_on}");
if let Err(e) = server.await {
eprintln!("Server error: {e}");
return Err(e.into());
}
Ok(())
}
fn bad_request(msg: String) -> Response<Body> {
let mut resp = Response::new(Body::from(msg));
*resp.status_mut() = hyper::StatusCode::BAD_REQUEST;
resp
}

View File

@ -35,7 +35,7 @@ serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.116" serde_json = "1.0.116"
sha2 = "0.10.8" sha2 = "0.10.8"
thiserror = "1.0.61" thiserror = "1.0.61"
toml = "0.8.14" toml = "0.8.13"
# TODO: change this to a cargo release once 8.1.1 comes out # TODO: change this to a cargo release once 8.1.1 comes out
ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] } ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] }
url = { version = "2.5.0", features = ["serde"] } url = { version = "2.5.0", features = ["serde"] }

View File

@ -4,8 +4,6 @@ use kcl_lib::{
settings::types::UnitLength, settings::types::UnitLength,
}; };
// mod server;
async fn new_context(units: UnitLength) -> Result<ExecutorContext> { async fn new_context(units: UnitLength) -> Result<ExecutorContext> {
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),); let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
let http_client = reqwest::Client::builder() let http_client = reqwest::Client::builder()
@ -1959,3 +1957,23 @@ async fn serial_test_neg_xz_plane() {
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/neg_xz_plane.png", &result, 1.0); twenty_twenty::assert_image("tests/executor/outputs/neg_xz_plane.png", &result, 1.0);
} }
#[tokio::test(flavor = "multi_thread")]
async fn start_server() {
let server_url = "0.0.0.0:3333";
tokio::task::spawn(server::start(server_url.parse().unwrap()));
let code = r#"const part001 = startSketchOn('-XZ')
|> startProfileAt([0, 0], %)
|> lineTo([100, 100], %)
|> lineTo([100, 0], %)
|> close(%)
|> extrude(5 + 7, %)
"#;
let client = reqwest::Client::new();
client
.post(format!("http://{server_url}"))
.body(code)
.send()
.await
.unwrap();
}

View File

@ -0,0 +1 @@