Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gizmo Normal Snapping #2539

Merged
merged 41 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
da56ced
gizmo 2.0
max-mrgrsk May 26, 2024
2781261
A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
github-actions[bot] May 27, 2024
956adc1
initial mouse position fix
max-mrgrsk May 27, 2024
609866e
animation loop / disposal optimization
max-mrgrsk May 27, 2024
f0a506d
A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
github-actions[bot] May 27, 2024
5edbb04
reset camera tweak
Irev-Dev May 27, 2024
a47d1c1
Merge branch 'main' into 2445-gizmo-snapping
Irev-Dev May 27, 2024
6093f61
add cam target to debug panel
Irev-Dev May 28, 2024
fc37031
test stub
Irev-Dev May 28, 2024
f7be198
reset camera position handle removed from gizmo
max-mrgrsk May 28, 2024
1a98418
gizmo refactoring
max-mrgrsk May 28, 2024
0e224e9
small fix
max-mrgrsk May 28, 2024
73ddfa6
reset camera view
max-mrgrsk May 29, 2024
1d6af39
nicer updateCameraToAxis
max-mrgrsk May 29, 2024
1d01595
micro refactoring
max-mrgrsk May 29, 2024
eaf5a8d
playwright update
max-mrgrsk May 29, 2024
9348b90
playwright remove timeout + fmt
max-mrgrsk May 30, 2024
c18f820
hide gizmo while loading stream
max-mrgrsk May 30, 2024
e575bf0
Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"
Irev-Dev May 31, 2024
78eb92c
Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"
Irev-Dev May 31, 2024
d73ba82
Merge remote-tracking branch 'origin' into 2445-gizmo-snapping
Irev-Dev May 31, 2024
2e08856
try make gizmo test more realiable
Irev-Dev May 31, 2024
bc3cef1
tweak
Irev-Dev May 31, 2024
4c2ba6a
refactoring
max-mrgrsk May 31, 2024
fb30528
increase timeout time
max-mrgrsk May 31, 2024
125b217
1 sec wait after mouse click
max-mrgrsk May 31, 2024
30c4482
3 sec timeout
max-mrgrsk May 31, 2024
96940bc
Merge branch 'main' into 2445-gizmo-snapping
max-mrgrsk May 31, 2024
24cd4e2
better clickPosition
max-mrgrsk Jun 1, 2024
6e99d14
test with 10 sec timeout
max-mrgrsk Jun 1, 2024
ce1db5d
0.5 sec timeout
max-mrgrsk Jun 1, 2024
3c37e31
add passive update for gizmo to avoid some edge cases
max-mrgrsk Jun 1, 2024
bea2a06
default_camera_get_settings after click
max-mrgrsk Jun 2, 2024
e8d3d10
Merge branch 'main' into 2445-gizmo-snapping
max-mrgrsk Jun 3, 2024
a3313e2
try and remove timeouts
Irev-Dev Jun 5, 2024
079fcf9
Merge remote-tracking branch 'origin' into 2445-gizmo-snapping
Irev-Dev Jun 5, 2024
aafadb0
fix unrelated test
Irev-Dev Jun 5, 2024
2e19a05
Merge remote-tracking branch 'origin' into 2445-gizmo-snapping
Irev-Dev Jun 5, 2024
e22785d
test tweak
Irev-Dev Jun 5, 2024
c5f6e14
Merge remote-tracking branch 'origin/main' into 2445-gizmo-snapping
Irev-Dev Jun 5, 2024
8222fb5
put waits back in
Irev-Dev Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions src/clientSideScene/CameraControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,80 @@ export class CameraControls {
})
}

async updateCameraToAxis(
axis: 'x' | 'y' | 'z' | '-x' | '-y' | '-z' | 'reset'
): Promise<void> {
if (axis === 'reset') {
await this.resetCameraPosition()
return
}

const distance = this.camera.position.distanceTo(this.target)

let vantage = { x: 0, y: 0, z: 0 }
let up = { x: 0, y: 0, z: 0 }

if (axis === 'x') {
vantage = { x: distance, y: 0, z: 0 }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we change this so to something like

    const vantage = this.target.clone()
    let up = { x: 0, y: 0, z: 0 }

    if (axis === 'x') {
      vantage.x += distance
      up = { x: 0, y: 0, z: 1 }
// ...

And then for the default camera look at keep the users's target

await this.engineCommandManager.sendSceneCommand({
      type: 'modeling_cmd_req',
      cmd_id: uuidv4(),
      cmd: {
        type: 'default_camera_look_at',
        center: this.target,
        vantage: vantage,
        up: up,
      },
    })

Sorry if if this is different from what I originally asked for, but think this will be better as it respects the user's current target, and just rotates the camera to a different perspective.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now I can see that too ! so much better to code with viewport on :DDD

up = { x: 0, y: 0, z: 1 }
} else if (axis === 'y') {
vantage = { x: 0, y: distance, z: 0 }
up = { x: 0, y: 0, z: 1 }
} else if (axis === 'z') {
vantage = { x: 0, y: 0, z: distance }
up = { x: -1, y: 0, z: 0 }
} else if (axis === '-x') {
vantage = { x: -distance, y: 0, z: 0 }
up = { x: 0, y: 0, z: 1 }
} else if (axis === '-y') {
vantage = { x: 0, y: -distance, z: 0 }
up = { x: 0, y: 0, z: 1 }
} else if (axis === '-z') {
vantage = { x: 0, y: 0, 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: { x: 0, y: 0, z: 0 },
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: { x: 0, y: 0, z: 0 },
vantage: { x: 0, y: -128, 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
},
})
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I couldn't help but make a commit, just cause I wanted to see this working.

All I did is change the vantage to where the camera is on boot up vantage: { x: 0, y: -128, z: 64 }, The most important thing here is the ratio of y and z (double y).

But immediately followed up by a zoom to fit means it will look exactly the same as when you refresh the page.

Note: default_camera_get_settings is not needed because we already subscribe to the camera payload of zoom_to_fit

Copy link
Collaborator Author

@max-mrgrsk max-mrgrsk May 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we fixed my viewport issue today, I could test this one...

In my case it does not work. The camera resets to weird position somewhere far away.

My findings:

1. Unexpected Camera Position: After executing both commands, the camera ended up in a strange position, far from where it should have been. The coordinates before resetting were something like {x: 1719.28, y: -1096.2126, z: 2252.1978} (zoomed and default rotation), but after resetting, they became {x: 1719.28, y: 4877.849, z: -696.26904}, instead of staying the same. Weird math...

2. Testing Commands Individually: When I tested each command (default_camera_look_at and zoom_to_fit) separately, they worked fine on their own. The issue only appeared when both commands were sent one after the other.

3. Pause Between Commands: I suspected that the rapid change from large coordinate values to small ones and back to large values might be causing the issue. I tried adding a short pause (1000ms) between the two commands to give the server time to process the changes, but it didn't help.

4. Eliminating the Coordinate Jump: Instead of using fixed small values for the default_camera_look_at command, I used the current target position for the center and adjusted the vantage point relative to this target. This way, there wasn't a large jump in coordinate values. For example:

center: this.target,
vantage: { x: this.target.x, y: this.target.y - 128, z: this.target.z + 64 }

instead of:

center: { x: 0, y: 0, z: 0 },
vantage: { x: 0, y: -128, z: 64 },

This approach worked and the camera reset correctly without any issues.

but in case of moving camera super far away you might need to click reset 2 times :DD

5. Further Investigation: I found it strange that using the this.target position worked, but hardcoded values did not. It turns out that the vantage value can be any value; the angle may not be default, but the zoom-in will work as expected. The problem lies with the center value. Any value other than this.target causes an offset in the zoom_to_fit command.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay this sounds like a good work around, but maybe the zoom to fit should be robust against this kind of thing?

Maybe I'll mess with it and raise something with the engine team because I'm not sure it should require us to finagle like this.


async tweenCameraToQuaternion(
targetQuaternion: Quaternion,
targetPosition = new Vector3(),
Expand Down
182 changes: 146 additions & 36 deletions src/components/Gizmo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SceneInfra } from 'clientSideScene/sceneInfra'
import { sceneInfra } from 'lib/singletons'
import { useEffect, useRef } from 'react'
import { MutableRefObject, useEffect, useRef } from 'react'
import {
WebGLRenderer,
Scene,
Expand All @@ -12,21 +13,36 @@ import {
Clock,
Quaternion,
ColorRepresentation,
Vector2,
Raycaster,
Camera,
Intersection,
Object3D,
} 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',
enum AxisColors {
X = '#fa6668',
Y = '#11eb6b',
Z = '#6689ef',
Gray = '#c6c7c2',
}
enum AxisNames {
X = 'x',
Y = 'y',
Z = 'z',
NEG_X = '-x',
NEG_Y = '-y',
NEG_Z = '-z',
RESET = 'reset',
}

export default function Gizmo() {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)

useEffect(() => {
if (!canvasRef.current) return
Expand All @@ -41,24 +57,61 @@ export default function Gizmo() {
const { gizmoAxes, gizmoAxisHeads } = createGizmo()
scene.add(...gizmoAxes, ...gizmoAxisHeads)

const raycaster = new Raycaster()
const { mouse, disposeMouseEvents } = initializeMouseEvents(
canvas,
raycasterIntersect,
sceneInfra
)
const raycasterObjects = [...gizmoAxisHeads]

const clock = new Clock()
const clientCamera = sceneInfra.camControls.camera
let currentQuaternion = new Quaternion().copy(clientCamera.quaternion)

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 animate = () => {
requestAnimationFrame(animate)
updateCameraOrientation(
const delta = clock.getDelta()
if (
!quaternionsEqual(
currentQuaternion,
sceneInfra.camControls.camera.quaternion
)
) {
updateCameraOrientation(
camera,
currentQuaternion,
sceneInfra.camControls.camera.quaternion,
delta
)
}
updateRayCaster(
raycasterObjects,
raycaster,
mouse,
camera,
currentQuaternion,
sceneInfra.camControls.camera.quaternion,
clock.getDelta()
raycasterIntersect
)
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
animate()

return () => {
renderer.dispose()
disposeMouseEvents()
}
}, [])

Expand All @@ -69,7 +122,7 @@ export default function Gizmo() {
)
}

const createCamera = () => {
const createCamera = (): OrthographicCamera => {
return new OrthographicCamera(
-FRUSTUM_SIZE,
FRUSTUM_SIZE,
Expand All @@ -82,21 +135,22 @@ const createCamera = () => {

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'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.X, 0, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Y, Math.PI / 2, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Z, -Math.PI / 2, 'y'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, Math.PI, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.Gray, -Math.PI / 2, 'z'),
createAxis(AXIS_LENGTH, AXIS_WIDTH, AxisColors.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'),
createAxisHead(AxisNames.X, AxisColors.X, [AXIS_LENGTH, 0, 0]),
createAxisHead(AxisNames.Y, AxisColors.Y, [0, AXIS_LENGTH, 0]),
createAxisHead(AxisNames.Z, AxisColors.Z, [0, 0, AXIS_LENGTH]),
createAxisHead(AxisNames.NEG_X, AxisColors.Gray, [-AXIS_LENGTH, 0, 0]),
createAxisHead(AxisNames.NEG_Y, AxisColors.Gray, [0, -AXIS_LENGTH, 0]),
createAxisHead(AxisNames.NEG_Z, AxisColors.Gray, [0, 0, -AXIS_LENGTH]),
createAxisHead(AxisNames.RESET, AxisColors.Gray, [0, 0, 0]),
]

return { gizmoAxes, gizmoAxisHeads }
Expand All @@ -108,28 +162,27 @@ const createAxis = (
color: ColorRepresentation,
rotation = 0,
axis = 'x'
) => {
const geometry = new BoxGeometry(length, width, width).translate(
length / 2,
0,
0
)
): Mesh => {
const geometry = new BoxGeometry(length, width, width)
geometry.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,
name: AxisNames,
color: ColorRepresentation,
rotation = 0,
axis = 'x'
) => {
const geometry = new SphereGeometry(0.065, 16, 8).translate(length, 0, 0)
position: number[]
): Mesh => {
const geometry = new SphereGeometry(0.065, 16, 8)
const material = new MeshBasicMaterial({ color: new Color(color) })
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
}

Expand All @@ -144,3 +197,60 @@ const updateCameraOrientation = (
camera.position.set(0, 0, 1).applyQuaternion(currentQuaternion)
camera.quaternion.copy(currentQuaternion)
}

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>
) => {
// check if mouse is outside the canvas bounds and stop raycaster
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
}
}
Loading