Gizmo Normal Snapping (#2539)
* gizmo 2.0 nice and clickable * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * initial mouse position fix when the scene first loads, mouse position is 0,0, which renders the gizmo selected. * animation loop / disposal optimization * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * reset camera tweak * add cam target to debug panel * test stub * reset camera position handle removed from gizmo it is now a button in the debug panel * gizmo refactoring * small fix * reset camera view bug fix * nicer updateCameraToAxis now gizmo rotates around the target instead of world 0,0,0 * micro refactoring * playwright update * playwright remove timeout + fmt * hide gizmo while loading stream * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commitf0a506d6b9
. * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commit2781261331
. * try make gizmo test more realiable * tweak * refactoring * increase timeout time * 1 sec wait after mouse click * 3 sec timeout * better clickPosition * test with 10 sec timeout * 0.5 sec timeout * add passive update for gizmo to avoid some edge cases * default_camera_get_settings after click * try and remove timeouts --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
@ -93,7 +93,7 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.click(700, 200)
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(u.codeLocator).toHaveText(
|
||||||
`const sketch001 = startSketchOn('XZ')`
|
`const sketch001 = startSketchOn('XZ')`
|
||||||
)
|
)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -102,29 +102,25 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1}], %)`)
|
|> line([0, ${commonPoints.num1}], %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${commonPoints.num1}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
@ -154,8 +150,7 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Constrain' }).click()
|
await page.getByRole('button', { name: 'Constrain' }).click()
|
||||||
await page.getByRole('button', { name: 'Equal Length' }).click()
|
await page.getByRole('button', { name: 'Equal Length' }).click()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %, 'seg01')
|
|> line([${commonPoints.num1}, 0], %, 'seg01')
|
||||||
|> line([0, ${commonPoints.num1}], %)
|
|> line([0, ${commonPoints.num1}], %)
|
||||||
@ -1533,7 +1528,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
||||||
const { toSU, click00r, expectCodeToBe } = getMovementUtils({ center, page })
|
const { toSU, click00r } = getMovementUtils({ center, page })
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
@ -1550,25 +1545,25 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
let codeStr = "const sketch001 = startSketchOn('XY')"
|
let codeStr = "const sketch001 = startSketchOn('XY')"
|
||||||
|
|
||||||
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
await click00r(0, 0)
|
await click00r(0, 0)
|
||||||
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(50, 0)
|
await click00r(50, 0)
|
||||||
codeStr += ` |> line(${toSU([50, 0])}, %)`
|
codeStr += ` |> line(${toSU([50, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 50)
|
await click00r(0, 50)
|
||||||
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-50, 0)
|
await click00r(-50, 0)
|
||||||
codeStr += ` |> line(${toSU([-50, 0])}, %)`
|
codeStr += ` |> line(${toSU([-50, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// exit the sketch, reset relative clicker
|
// exit the sketch, reset relative clicker
|
||||||
click00r(undefined, undefined)
|
click00r(undefined, undefined)
|
||||||
@ -1586,24 +1581,24 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
await page.mouse.click(center.x + 30, center.y)
|
await page.mouse.click(center.x + 30, center.y)
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
codeStr += "const sketch002 = startSketchOn('XY')"
|
codeStr += "const sketch002 = startSketchOn('XY')"
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> startProfileAt(${toSU([30, 0])}, %)`
|
codeStr += ` |> startProfileAt(${toSU([30, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> line(${toSU([30 - 0.1 /* imprecision */, 0])}, %)`
|
codeStr += ` |> line(${toSU([30 + 0.1 /* imprecision */, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 30)
|
await click00r(0, 30)
|
||||||
codeStr += ` |> line(${toSU([0, 30])}, %)`
|
codeStr += ` |> line(${toSU([0, 30])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-30, 0)
|
await click00r(-30, 0)
|
||||||
codeStr += ` |> line(${toSU([-30 + 0.1, 0])}, %)`
|
codeStr += ` |> line(${toSU([-30 - 0.1, 0])}, %)`
|
||||||
await expectCodeToBe(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
click00r(undefined, undefined)
|
click00r(undefined, undefined)
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
@ -4781,6 +4776,159 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
|
|||||||
).not.toBeVisible()
|
).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Testing Gizmo', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
testDescription: 'top view',
|
||||||
|
clickPosition: { x: 951, y: 385 },
|
||||||
|
expectedCameraPosition: { x: 800, y: -152, z: 4886.02 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: 'bottom view',
|
||||||
|
clickPosition: { x: 951, y: 429 },
|
||||||
|
expectedCameraPosition: { x: 800, y: -152, z: -4834.02 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: '+x view',
|
||||||
|
clickPosition: { x: 929, y: 417 },
|
||||||
|
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: '-x view',
|
||||||
|
clickPosition: { x: 974, y: 397 },
|
||||||
|
expectedCameraPosition: { x: -4060.02, y: -152, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: '+y view',
|
||||||
|
clickPosition: { x: 967, y: 421 },
|
||||||
|
expectedCameraPosition: { x: 800, y: 4708.02, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testDescription: '-y view',
|
||||||
|
clickPosition: { x: 935, y: 393 },
|
||||||
|
expectedCameraPosition: { x: 800, y: -5012.02, z: 26 },
|
||||||
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const {
|
||||||
|
clickPosition,
|
||||||
|
expectedCameraPosition,
|
||||||
|
expectedCameraTarget,
|
||||||
|
testDescription,
|
||||||
|
} of cases) {
|
||||||
|
test(`check ${testDescription}`, async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'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.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')
|
||||||
|
|
||||||
|
await page.waitForTimeout(400)
|
||||||
|
await page.mouse.move(clickPosition.x, clickPosition.y)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await page.mouse.click(clickPosition.x, clickPosition.y)
|
||||||
|
await u.waitForCmdReceive('default_camera_look_at')
|
||||||
|
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')
|
||||||
|
await page.waitForTimeout(400)
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
// position
|
||||||
|
expect(page.getByTestId('cam-x-position')).toHaveValue(
|
||||||
|
expectedCameraPosition.x.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-y-position')).toHaveValue(
|
||||||
|
expectedCameraPosition.y.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-z-position')).toHaveValue(
|
||||||
|
expectedCameraPosition.z.toString()
|
||||||
|
),
|
||||||
|
// target
|
||||||
|
expect(page.getByTestId('cam-x-target')).toHaveValue(
|
||||||
|
expectedCameraTarget.x.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-y-target')).toHaveValue(
|
||||||
|
expectedCameraTarget.y.toString()
|
||||||
|
),
|
||||||
|
expect(page.getByTestId('cam-z-target')).toHaveValue(
|
||||||
|
expectedCameraTarget.z.toString()
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('Successful export shows a success toast', async ({ page }) => {
|
test('Successful export shows a success toast', async ({ page }) => {
|
||||||
// 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
|
||||||
|
@ -162,12 +162,7 @@ export const getMovementUtils = (opts: any) => {
|
|||||||
return ret.then(() => [last.x, last.y])
|
return ret.then(() => [last.x, last.y])
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectCodeToBe = async (str: string) => {
|
return { toSU, click00r }
|
||||||
await expect(opts.page.locator('.cm-content')).toHaveText(str)
|
|
||||||
await opts.page.waitForTimeout(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { toSU, click00r, expectCodeToBe }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUtils(page: Page) {
|
export async function getUtils(page: Page) {
|
||||||
@ -228,6 +223,7 @@ export async function getUtils(page: Page) {
|
|||||||
.locator(locator)
|
.locator(locator)
|
||||||
.boundingBox()
|
.boundingBox()
|
||||||
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
|
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
|
||||||
|
codeLocator: page.locator('.cm-content'),
|
||||||
doAndWaitForCmd: async (
|
doAndWaitForCmd: async (
|
||||||
fn: () => Promise<void>,
|
fn: () => Promise<void>,
|
||||||
commandType: string,
|
commandType: string,
|
||||||
|
@ -48,12 +48,14 @@ export type ReactCameraProperties =
|
|||||||
type: 'perspective'
|
type: 'perspective'
|
||||||
fov?: number
|
fov?: number
|
||||||
position: [number, number, number]
|
position: [number, number, number]
|
||||||
|
target: [number, number, number]
|
||||||
quaternion: [number, number, number, number]
|
quaternion: [number, number, number, number]
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'orthographic'
|
type: 'orthographic'
|
||||||
zoom?: number
|
zoom?: number
|
||||||
position: [number, number, number]
|
position: [number, number, number]
|
||||||
|
target: [number, number, number]
|
||||||
quaternion: [number, number, number, number]
|
quaternion: [number, number, number, number]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -773,6 +775,75 @@ export class CameraControls {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCameraToAxis(
|
||||||
|
axis: 'x' | 'y' | 'z' | '-x' | '-y' | '-z'
|
||||||
|
): Promise<void> {
|
||||||
|
const distance = this.camera.position.distanceTo(this.target)
|
||||||
|
|
||||||
|
const vantage = this.target.clone()
|
||||||
|
let up = { x: 0, y: 0, z: 1 }
|
||||||
|
|
||||||
|
if (axis === 'x') {
|
||||||
|
vantage.x += distance
|
||||||
|
} else if (axis === 'y') {
|
||||||
|
vantage.y += distance
|
||||||
|
} else if (axis === 'z') {
|
||||||
|
vantage.z += distance
|
||||||
|
up = { x: -1, y: 0, z: 0 }
|
||||||
|
} else if (axis === '-x') {
|
||||||
|
vantage.x -= distance
|
||||||
|
} else if (axis === '-y') {
|
||||||
|
vantage.y -= distance
|
||||||
|
} else if (axis === '-z') {
|
||||||
|
vantage.z -= distance
|
||||||
|
up = { x: -1, y: 0, z: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
center: this.target,
|
||||||
|
vantage: vantage,
|
||||||
|
up: up,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await this.engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetCameraPosition(): Promise<void> {
|
||||||
|
await this.engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
center: this.target,
|
||||||
|
vantage: {
|
||||||
|
x: this.target.x,
|
||||||
|
y: this.target.y - 128,
|
||||||
|
z: this.target.z + 64,
|
||||||
|
},
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await this.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.2, // padding around the objects
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async tweenCameraToQuaternion(
|
async tweenCameraToQuaternion(
|
||||||
targetQuaternion: Quaternion,
|
targetQuaternion: Quaternion,
|
||||||
targetPosition = new Vector3(),
|
targetPosition = new Vector3(),
|
||||||
@ -957,6 +1028,11 @@ export class CameraControls {
|
|||||||
roundOff(this.camera.position.y, 2),
|
roundOff(this.camera.position.y, 2),
|
||||||
roundOff(this.camera.position.z, 2),
|
roundOff(this.camera.position.z, 2),
|
||||||
],
|
],
|
||||||
|
target: [
|
||||||
|
roundOff(this.target.x, 2),
|
||||||
|
roundOff(this.target.y, 2),
|
||||||
|
roundOff(this.target.z, 2),
|
||||||
|
],
|
||||||
quaternion: [
|
quaternion: [
|
||||||
roundOff(this.camera.quaternion.x, 2),
|
roundOff(this.camera.quaternion.x, 2),
|
||||||
roundOff(this.camera.quaternion.y, 2),
|
roundOff(this.camera.quaternion.y, 2),
|
||||||
|
@ -699,6 +699,15 @@ export const CamDebugSettings = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
sceneInfra.camControls.resetCameraPosition()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Camera Position
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{camSettings.type === 'perspective' && (
|
{camSettings.type === 'perspective' && (
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@ -816,6 +825,71 @@ export const CamDebugSettings = () => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
target
|
||||||
|
<ul className="flex">
|
||||||
|
<li>
|
||||||
|
<span className="pl-2 pr-1">x:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={5}
|
||||||
|
data-testid="cam-x-target"
|
||||||
|
value={camSettings.target[0]}
|
||||||
|
className="text-black w-16"
|
||||||
|
onChange={(e) => {
|
||||||
|
sceneInfra.camControls.setCam({
|
||||||
|
...camSettings,
|
||||||
|
target: [
|
||||||
|
parseFloat(e.target.value),
|
||||||
|
camSettings.target[1],
|
||||||
|
camSettings.target[2],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="pl-2 pr-1">y:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={5}
|
||||||
|
data-testid="cam-y-target"
|
||||||
|
value={camSettings.target[1]}
|
||||||
|
className="text-black w-16"
|
||||||
|
onChange={(e) => {
|
||||||
|
sceneInfra.camControls.setCam({
|
||||||
|
...camSettings,
|
||||||
|
target: [
|
||||||
|
camSettings.target[0],
|
||||||
|
parseFloat(e.target.value),
|
||||||
|
camSettings.target[2],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="pl-2 pr-1">z:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={5}
|
||||||
|
data-testid="cam-z-target"
|
||||||
|
value={camSettings.target[2]}
|
||||||
|
className="text-black w-16"
|
||||||
|
onChange={(e) => {
|
||||||
|
sceneInfra.camControls.setCam({
|
||||||
|
...camSettings,
|
||||||
|
target: [
|
||||||
|
camSettings.target[0],
|
||||||
|
camSettings.target[1],
|
||||||
|
parseFloat(e.target.value),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
||||||
import { sceneInfra } from 'lib/singletons'
|
import { sceneInfra } from 'lib/singletons'
|
||||||
import { useEffect, useRef } from 'react'
|
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
WebGLRenderer,
|
WebGLRenderer,
|
||||||
Scene,
|
Scene,
|
||||||
@ -12,21 +13,37 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
ColorRepresentation,
|
ColorRepresentation,
|
||||||
|
Vector2,
|
||||||
|
Raycaster,
|
||||||
|
Camera,
|
||||||
|
Intersection,
|
||||||
|
Object3D,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
|
|
||||||
const CANVAS_SIZE = 80
|
const CANVAS_SIZE = 80
|
||||||
const FRUSTUM_SIZE = 0.5
|
const FRUSTUM_SIZE = 0.5
|
||||||
const AXIS_LENGTH = 0.35
|
const AXIS_LENGTH = 0.35
|
||||||
const AXIS_WIDTH = 0.02
|
const AXIS_WIDTH = 0.02
|
||||||
const AXIS_COLORS = {
|
enum AxisColors {
|
||||||
x: '#fa6668',
|
X = '#fa6668',
|
||||||
y: '#11eb6b',
|
Y = '#11eb6b',
|
||||||
z: '#6689ef',
|
Z = '#6689ef',
|
||||||
gray: '#c6c7c2',
|
Gray = '#c6c7c2',
|
||||||
|
}
|
||||||
|
enum AxisNames {
|
||||||
|
X = 'x',
|
||||||
|
Y = 'y',
|
||||||
|
Z = 'z',
|
||||||
|
NEG_X = '-x',
|
||||||
|
NEG_Y = '-y',
|
||||||
|
NEG_Z = '-z',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Gizmo() {
|
export default function Gizmo() {
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
|
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||||
|
const cameraPassiveUpdateTimer = useRef(0)
|
||||||
|
const raycasterPassiveUpdateTimer = useRef(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvasRef.current) return
|
if (!canvasRef.current) return
|
||||||
@ -41,24 +58,44 @@ export default function Gizmo() {
|
|||||||
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
|
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
|
||||||
scene.add(...gizmoAxes, ...gizmoAxisHeads)
|
scene.add(...gizmoAxes, ...gizmoAxisHeads)
|
||||||
|
|
||||||
|
const raycaster = new Raycaster()
|
||||||
|
const { mouse, disposeMouseEvents } = initializeMouseEvents(
|
||||||
|
canvas,
|
||||||
|
raycasterIntersect,
|
||||||
|
sceneInfra
|
||||||
|
)
|
||||||
|
const raycasterObjects = [...gizmoAxisHeads]
|
||||||
|
|
||||||
const clock = new Clock()
|
const clock = new Clock()
|
||||||
const clientCamera = sceneInfra.camControls.camera
|
const clientCamera = sceneInfra.camControls.camera
|
||||||
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)
|
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
requestAnimationFrame(animate)
|
const delta = clock.getDelta()
|
||||||
updateCameraOrientation(
|
updateCameraOrientation(
|
||||||
camera,
|
camera,
|
||||||
currentQuaternion,
|
currentQuaternion,
|
||||||
sceneInfra.camControls.camera.quaternion,
|
sceneInfra.camControls.camera.quaternion,
|
||||||
clock.getDelta()
|
delta,
|
||||||
|
cameraPassiveUpdateTimer
|
||||||
|
)
|
||||||
|
updateRayCaster(
|
||||||
|
raycasterObjects,
|
||||||
|
raycaster,
|
||||||
|
mouse,
|
||||||
|
camera,
|
||||||
|
raycasterIntersect,
|
||||||
|
delta,
|
||||||
|
raycasterPassiveUpdateTimer
|
||||||
)
|
)
|
||||||
renderer.render(scene, camera)
|
renderer.render(scene, camera)
|
||||||
|
requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
animate()
|
animate()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
renderer.dispose()
|
renderer.dispose()
|
||||||
|
disposeMouseEvents()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -69,7 +106,7 @@ export default function Gizmo() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCamera = () => {
|
const createCamera = (): OrthographicCamera => {
|
||||||
return new OrthographicCamera(
|
return new OrthographicCamera(
|
||||||
-FRUSTUM_SIZE,
|
-FRUSTUM_SIZE,
|
||||||
FRUSTUM_SIZE,
|
FRUSTUM_SIZE,
|
||||||
@ -82,21 +119,21 @@ const createCamera = () => {
|
|||||||
|
|
||||||
const createGizmo = () => {
|
const createGizmo = () => {
|
||||||
const gizmoAxes = [
|
const gizmoAxes = [
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.x, 0, 'z'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.X, 0, 'z'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Y, Math.PI / 2, 'z'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Z, -Math.PI / 2, 'y'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI, 'z'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI, 'z'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, -Math.PI / 2, 'z'),
|
||||||
createAxis(AXIS_LENGTH, AXIS_WIDTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
|
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI / 2, 'y'),
|
||||||
]
|
]
|
||||||
|
|
||||||
const gizmoAxisHeads = [
|
const gizmoAxisHeads = [
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.x, 0, 'z'),
|
createAxisHead(AxisNames.X, AxisColors.X, [AXIS_LENGTH, 0, 0]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.y, Math.PI / 2, 'z'),
|
createAxisHead(AxisNames.Y, AxisColors.Y, [0, AXIS_LENGTH, 0]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.z, -Math.PI / 2, 'y'),
|
createAxisHead(AxisNames.Z, AxisColors.Z, [0, 0, AXIS_LENGTH]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI, 'z'),
|
createAxisHead(AxisNames.NEG_X, AxisColors.Gray, [-AXIS_LENGTH, 0, 0]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, -Math.PI / 2, 'z'),
|
createAxisHead(AxisNames.NEG_Y, AxisColors.Gray, [0, -AXIS_LENGTH, 0]),
|
||||||
createAxisHead(AXIS_LENGTH, AXIS_COLORS.gray, Math.PI / 2, 'y'),
|
createAxisHead(AxisNames.NEG_Z, AxisColors.Gray, [0, 0, -AXIS_LENGTH]),
|
||||||
]
|
]
|
||||||
|
|
||||||
return { gizmoAxes, gizmoAxisHeads }
|
return { gizmoAxes, gizmoAxisHeads }
|
||||||
@ -108,12 +145,9 @@ const createAxis = (
|
|||||||
color: ColorRepresentation,
|
color: ColorRepresentation,
|
||||||
rotation = 0,
|
rotation = 0,
|
||||||
axis = 'x'
|
axis = 'x'
|
||||||
) => {
|
): Mesh => {
|
||||||
const geometry = new BoxGeometry(length, width, width).translate(
|
const geometry = new BoxGeometry(length, width, width)
|
||||||
length / 2,
|
geometry.translate(length / 2, 0, 0)
|
||||||
0,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
const material = new MeshBasicMaterial({ color: new Color(color) })
|
||||||
const mesh = new Mesh(geometry, material)
|
const mesh = new Mesh(geometry, material)
|
||||||
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
||||||
@ -121,15 +155,17 @@ const createAxis = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createAxisHead = (
|
const createAxisHead = (
|
||||||
length: number,
|
name: AxisNames,
|
||||||
color: ColorRepresentation,
|
color: ColorRepresentation,
|
||||||
rotation = 0,
|
position: number[]
|
||||||
axis = 'x'
|
): Mesh => {
|
||||||
) => {
|
const geometry = new SphereGeometry(0.065, 16, 8)
|
||||||
const geometry = new SphereGeometry(0.065, 16, 8).translate(length, 0, 0)
|
|
||||||
const material = new MeshBasicMaterial({ color: new Color(color) })
|
const material = new MeshBasicMaterial({ color: new Color(color) })
|
||||||
const mesh = new Mesh(geometry, material)
|
const mesh = new Mesh(geometry, material)
|
||||||
mesh.rotation[axis as 'x' | 'y' | 'z'] = rotation
|
|
||||||
|
mesh.position.set(position[0], position[1], position[2])
|
||||||
|
mesh.updateMatrixWorld()
|
||||||
|
mesh.name = name
|
||||||
return mesh
|
return mesh
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,10 +173,97 @@ const updateCameraOrientation = (
|
|||||||
camera: OrthographicCamera,
|
camera: OrthographicCamera,
|
||||||
currentQuaternion: Quaternion,
|
currentQuaternion: Quaternion,
|
||||||
targetQuaternion: Quaternion,
|
targetQuaternion: Quaternion,
|
||||||
deltaTime: number
|
deltaTime: number,
|
||||||
|
cameraPassiveUpdateTimer: MutableRefObject<number>
|
||||||
) => {
|
) => {
|
||||||
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
|
cameraPassiveUpdateTimer.current += deltaTime
|
||||||
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
|
if (
|
||||||
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
|
!quaternionsEqual(currentQuaternion, targetQuaternion) ||
|
||||||
camera.quaternion.copy(currentQuaternion)
|
cameraPassiveUpdateTimer.current >= 5
|
||||||
|
) {
|
||||||
|
const slerpFactor = 1 - Math.exp(-30 * deltaTime)
|
||||||
|
currentQuaternion.slerp(targetQuaternion, slerpFactor).normalize()
|
||||||
|
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
|
||||||
|
camera.quaternion.copy(currentQuaternion)
|
||||||
|
cameraPassiveUpdateTimer.current = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const quaternionsEqual = (
|
||||||
|
q1: Quaternion,
|
||||||
|
q2: Quaternion,
|
||||||
|
tolerance: number = 0.001
|
||||||
|
): boolean => {
|
||||||
|
return (
|
||||||
|
Math.abs(q1.x - q2.x) < tolerance &&
|
||||||
|
Math.abs(q1.y - q2.y) < tolerance &&
|
||||||
|
Math.abs(q1.z - q2.z) < tolerance &&
|
||||||
|
Math.abs(q1.w - q2.w) < tolerance
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeMouseEvents = (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
|
||||||
|
sceneInfra: SceneInfra
|
||||||
|
): { mouse: Vector2; disposeMouseEvents: () => void } => {
|
||||||
|
const mouse = new Vector2()
|
||||||
|
mouse.x = 1 // fix initial mouse position issue
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
const { left, top, width, height } = canvas.getBoundingClientRect()
|
||||||
|
mouse.x = ((event.clientX - left) / width) * 2 - 1
|
||||||
|
mouse.y = ((event.clientY - top) / height) * -2 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (raycasterIntersect.current) {
|
||||||
|
const axisName = raycasterIntersect.current.object.name as AxisNames
|
||||||
|
sceneInfra.camControls.updateCameraToAxis(axisName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', handleMouseMove)
|
||||||
|
window.addEventListener('click', handleClick)
|
||||||
|
|
||||||
|
const disposeMouseEvents = () => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
window.removeEventListener('click', handleClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mouse, disposeMouseEvents }
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRayCaster = (
|
||||||
|
objects: Object3D[],
|
||||||
|
raycaster: Raycaster,
|
||||||
|
mouse: Vector2,
|
||||||
|
camera: Camera,
|
||||||
|
raycasterIntersect: MutableRefObject<Intersection<Object3D> | null>,
|
||||||
|
deltaTime: number,
|
||||||
|
raycasterPassiveUpdateTimer: MutableRefObject<number>
|
||||||
|
) => {
|
||||||
|
raycasterPassiveUpdateTimer.current += deltaTime
|
||||||
|
|
||||||
|
// check if mouse is outside the canvas bounds and stop raycaster
|
||||||
|
if (raycasterPassiveUpdateTimer.current < 2) {
|
||||||
|
if (mouse.x < -1 || mouse.x > 1 || mouse.y < -1 || mouse.y > 1) {
|
||||||
|
raycasterIntersect.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raycaster.setFromCamera(mouse, camera)
|
||||||
|
const intersects = raycaster.intersectObjects(objects)
|
||||||
|
|
||||||
|
objects.forEach((object) => object.scale.set(1, 1, 1))
|
||||||
|
if (intersects.length) {
|
||||||
|
intersects[0].object.scale.set(1.5, 1.5, 1.5)
|
||||||
|
raycasterIntersect.current = intersects[0] // filter first object
|
||||||
|
} else {
|
||||||
|
raycasterIntersect.current = null
|
||||||
|
}
|
||||||
|
if (raycasterPassiveUpdateTimer.current > 2) {
|
||||||
|
raycasterPassiveUpdateTimer.current = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1673,7 +1673,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
(command.cmd.type === 'highlight_set_entity' ||
|
(command.cmd.type === 'highlight_set_entity' ||
|
||||||
command.cmd.type === 'mouse_move' ||
|
command.cmd.type === 'mouse_move' ||
|
||||||
command.cmd.type === 'camera_drag_move' ||
|
command.cmd.type === 'camera_drag_move' ||
|
||||||
command.cmd.type === 'default_camera_look_at' ||
|
|
||||||
command.cmd.type === ('default_camera_perspective_settings' as any))
|
command.cmd.type === ('default_camera_perspective_settings' as any))
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@ -1702,7 +1701,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
if (
|
if (
|
||||||
(cmd.type === 'camera_drag_move' ||
|
(cmd.type === 'camera_drag_move' ||
|
||||||
cmd.type === 'handle_mouse_drag_move' ||
|
cmd.type === 'handle_mouse_drag_move' ||
|
||||||
cmd.type === 'default_camera_look_at' ||
|
|
||||||
cmd.type === ('default_camera_perspective_settings' as any)) &&
|
cmd.type === ('default_camera_perspective_settings' as any)) &&
|
||||||
this.engineConnection?.unreliableDataChannel &&
|
this.engineConnection?.unreliableDataChannel &&
|
||||||
!forceWebsocket
|
!forceWebsocket
|
||||||
|
Reference in New Issue
Block a user