Skip to content

Commit

Permalink
Merge pull request #49 from PaulHax/camera
Browse files Browse the repository at this point in the history
Add orbit-camera for remote viewport
  • Loading branch information
thewtex authored Aug 4, 2023
2 parents 898cab0 + 7c1cc48 commit d388696
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 19 deletions.
1 change: 1 addition & 0 deletions packages/element/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@itk-viewer/viewer": "workspace:*",
"gl-matrix": "^3.4.3",
"lit": "^2.7.4",
"orbit-camera": "^1.0.0",
"xstate-lit": "^1.3.1"
}
}
171 changes: 161 additions & 10 deletions packages/element/src/itk-camera.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,191 @@
import { LitElement, PropertyValues, html } from 'lit';
import { LitElement, PropertyValues, css, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { mat4 } from 'gl-matrix';
import { ReadonlyMat4, mat4 } from 'gl-matrix';
import createOrbitCamera from 'orbit-camera';

import { Viewport } from '@itk-viewer/viewer/viewport.js';
import { createCamera } from '@itk-viewer/viewer/camera-machine.js';
import { Camera, createCamera } from '@itk-viewer/viewer/camera-machine.js';
import { Ref, createRef, ref } from 'lit/directives/ref.js';

type OrbitCameraController = ReturnType<typeof createOrbitCamera>;

const PAN_SPEED = 1;
const ZOOM_SPEED = 0.005;

const bindCamera = (
camera: OrbitCameraController,
viewport: HTMLElement,
onUpdate: (view: ReadonlyMat4) => unknown
) => {
let width = viewport.clientWidth;
let height = viewport.clientHeight;

const view = mat4.create();

const updateView = () => {
camera.view(view);
mat4.invert(view, view);
onUpdate(view);
};

const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
width = entry.contentRect.width;
height = entry.contentRect.height;
}
});
resizeObserver.observe(viewport);

let rotate = false;
let pan = false;
let scale = false;

const onMouseDown = (e: MouseEvent) => {
e.preventDefault();
if (e.button === 0) {
rotate = true;
} else if (e.button === 1) {
scale = true;
} else if (e.button === 2) {
pan = true;
}
};
viewport.addEventListener('mousedown', onMouseDown);

const onMouseUp = (e: MouseEvent) => {
e.preventDefault();
if (e.button === 0) {
rotate = false;
} else if (e.button === 1) {
scale = false;
} else if (e.button === 2) {
pan = false;
}
};
window.addEventListener('mouseup', onMouseUp);

let prevMouseX = 0;
let prevMouseY = 0;

const onMouseMove = (e: MouseEvent) => {
const mouseX = e.offsetX;
const mouseY = e.offsetY;

if (rotate) {
camera.rotate(
[mouseX / width - 0.5, mouseY / height - 0.5],
[prevMouseX / width - 0.5, prevMouseY / height - 0.5]
);
}

if (pan) {
camera.pan([
(PAN_SPEED * (mouseX - prevMouseX)) / width,
(PAN_SPEED * (mouseY - prevMouseY)) / height,
]);
}

if (scale) {
const d = mouseY - prevMouseY;
if (d) camera.distance *= Math.exp(d / height);
}

prevMouseX = mouseX;
prevMouseY = mouseY;

if (!rotate && !pan && !scale) return;

updateView();
};
viewport.addEventListener('mousemove', onMouseMove);

const onWheel = (e: WheelEvent) => {
e.preventDefault();
camera.zoom(e.deltaY * ZOOM_SPEED);

updateView();
};
viewport.addEventListener('wheel', onWheel, { passive: false });

const preventDefault = (e: Event) => e.preventDefault();
viewport.addEventListener('contextmenu', preventDefault);

const unbind = () => {
resizeObserver.disconnect();
viewport.removeEventListener('mousedown', onMouseDown);
window.removeEventListener('mouseup', onMouseUp);
viewport.removeEventListener('mousemove', onMouseMove);
viewport.removeEventListener('wheel', onWheel);
viewport.removeEventListener('contextmenu', preventDefault);
};

return unbind;
};

@customElement('itk-camera')
export class ItkCamera extends LitElement {
@property({ attribute: false })
viewport: Viewport | undefined;

camera = createCamera();
camera: Camera;
cameraController: OrbitCameraController;
unbind: (() => unknown) | undefined;
container: Ref<HTMLElement> = createRef();

constructor() {
super();
this.addEventListener('mousemove', (e: MouseEvent) => {
const pose = mat4.create();
mat4.copy(pose, this.camera.getSnapshot().context.pose);
const dX = e.movementX / 1000;
mat4.translate(pose, pose, [dX, 0, 0]);
this.camera = createCamera();

this.cameraController = createOrbitCamera(
[-0.747528, -0.570641, 0.754992],
[0.5, 0.5, 0.5],
[-0.505762, 0.408327, -0.759916]
);

const pose = this.cameraController.view();
this.camera.send({
type: 'setPose',
pose,
});
}

firstUpdated(): void {
const container = this.container.value;
if (!container) throw new Error('container not found');

this.unbind = bindCamera(this.cameraController, container, (pose) => {
this.camera.send({
type: 'setPose',
pose,
});
});
}

disconnectedCallback(): void {
super.disconnectedCallback();
this.unbind?.();
}

willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has('viewport')) {
this.viewport?.send({ type: 'setCamera', camera: this.camera });
}
}

render() {
return html` <slot></slot>`;
return html`
<div class="container" ${ref(this.container)}>
<slot></slot>
</div>
`;
}

static styles = css`
.container {
min-width: 500px;
min-height: 400px;
}
`;
}

declare global {
Expand Down
7 changes: 7 additions & 0 deletions packages/element/src/orbit-camera.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare module 'orbit-camera' {
export default function createOrbitCamera(
eye: number[],
target: number[],
up: number[]
): any;

Check warning on line 6 in packages/element/src/orbit-camera.d.ts

View workflow job for this annotation

GitHub Actions / build-test

Unexpected any. Specify a different type
}
18 changes: 10 additions & 8 deletions packages/remote-viewport/src/remote-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const remoteMachine = createMachine({
disconnected: {
entry: ({ context, self }) => {
// Update camera pose on viewport change
// FIXME: not capturing initial camera position because updateRender handler not registered before connection
context.viewport.subscribe(() => {
const cameraPose = context.viewport
.getSnapshot()
Expand Down Expand Up @@ -98,8 +99,8 @@ export const remoteMachine = createMachine({
actions: assign({
server: ({ event }) => event.output,
// initially, send all props to renderer
queuedRendererEvents: ({ context }) =>
getEntries(context.rendererProps),
// queuedRendererEvents: ({ context }) =>
// getEntries(context.rendererProps),
}),
target: 'online',
},
Expand All @@ -110,10 +111,12 @@ export const remoteMachine = createMachine({
updateRenderer: {
actions: [
assign({
rendererProps: ({ event: { props }, context }) => ({
...context.rendererProps,
...props,
}),
rendererProps: ({ event: { props }, context }) => {
return {
...context.rendererProps,
...props,
};
},
queuedRendererEvents: ({ event: { props }, context }) => [
...context.queuedRendererEvents,
...(getEntries(props) as RendererEntries),
Expand Down Expand Up @@ -149,8 +152,7 @@ export const remoteMachine = createMachine({
idle: {
always: {
// Renderer props changed while rendering? Then render.
guard: ({ context }) =>
Object.keys(context.queuedRendererEvents).length > 0,
guard: ({ context }) => context.queuedRendererEvents.length > 0,
target: 'render',
},
on: {
Expand Down
8 changes: 7 additions & 1 deletion packages/remote-viewport/src/remote-viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ export const createHyphaActors: () => RemoteMachineActors = () => ({
if (key === 'cameraPose') {
const eye = vec3.create();
mat4.getTranslation(eye, value);
return ['cameraPose', { eye }];

const target = vec3.fromValues(value[8], value[9], value[10]);
vec3.subtract(target, eye, target);

const up = vec3.fromValues(value[4], value[5], value[6]);

return ['cameraPose', { eye, up, target }];
}
return [key, value];
});
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d388696

Please sign in to comment.