Socket
Socket
Sign inDemoInstall

@itk-viewer/remote-viewport

Package Overview
Dependencies
Maintainers
2
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@itk-viewer/remote-viewport - npm Package Compare versions

Comparing version 0.2.7 to 0.2.8

13

CHANGELOG.md
# @itk-viewer/remote-viewport
## 0.2.8
### Patch Changes
- c13abd2: Add view-2d-vtkjs. Fix computeRanges.
- 991d042: Performance improvements to canvas blitting and render loop
- 14817ae: Load image with bounded by clip bounds.
- Updated dependencies [c13abd2]
- Updated dependencies [991d042]
- Updated dependencies [14817ae]
- @itk-viewer/viewer@0.2.5
- @itk-viewer/io@0.1.5
## 0.2.7

@@ -4,0 +17,0 @@

40

dist/remote-machine.d.ts

@@ -8,3 +8,3 @@ /// <reference types="gl-matrix/index.js" />

import { Bounds, ReadOnlyDimensionBounds } from '@itk-viewer/io/types.js';
type RendererProps = {
type RendererState = {
density: number;

@@ -20,3 +20,3 @@ cameraPose: ReadonlyMat4;

}[keyof T][];
export type RendererEntries = Entries<RendererProps>;
export type RendererEntries = Entries<RendererState>;
export type Context = {

@@ -26,11 +26,14 @@ serverConfig?: unknown;

frame?: Image;
rendererProps: RendererProps;
queuedRendererEvents: RendererEntries;
stagedRendererEvents: RendererEntries;
rendererState: RendererState;
queuedRendererCommands: RendererEntries;
stagedRendererCommands: RendererEntries;
viewport: ActorRefFrom<typeof viewportMachine>;
maxImageBytes: number;
imageScale: number;
toRendererCoordinateSystem: ReadonlyMat4;
imageWorldBounds: Bounds;
imageIndexClipBounds?: ReadOnlyDimensionBounds;
loadedImageIndexBounds?: ReadOnlyDimensionBounds;
clipBounds: Bounds;
loadedImageClipBounds: Bounds;
imageWorldToIndex: ReadonlyMat4;

@@ -44,3 +47,3 @@ };

type: 'updateRenderer';
props: Partial<RendererProps>;
state: Partial<RendererState>;
};

@@ -50,4 +53,4 @@ type RenderEvent = {

};
type SetImage = {
type: 'setImage';
type ImageAssigned = {
type: 'imageAssigned';
image: MultiscaleSpatialImage;

@@ -74,7 +77,16 @@ };

};
type UpdateImageScaleResult = {
type: 'noChange';
} | {
type: 'coarserScale';
imageScale: number;
} | {
type: 'load';
imageScale: number;
};
type UpdateImageScaleDone = {
type: 'xstate.done.actor.updateImageScale';
output: number;
output: UpdateImageScaleResult;
};
export declare const remoteMachine: import("xstate").StateMachine<Context, ConnectEvent | UpdateRendererEvent | RenderEvent | SetImage | SlowFps | FastFps | CameraPoseUpdated | ImageProcessorDone | UpdateImageScaleDone | {
export declare const remoteMachine: import("xstate").StateMachine<Context, ConnectEvent | UpdateRendererEvent | RenderEvent | ImageAssigned | SlowFps | FastFps | CameraPoseUpdated | ImageProcessorDone | UpdateImageScaleDone | {
type: 'setResolution';

@@ -85,6 +97,7 @@ resolution: [number, number];

clipBounds: Bounds;
imageScale?: number | undefined;
} | {
type: 'setImageScale';
imageScale: number;
} | {
type: 'sendCommands';
}, import("xstate").ProvidedActor, import("xstate").ParameterizedObject, import("xstate").ParameterizedObject, string, string, {

@@ -169,3 +182,3 @@ viewport: import("xstate").Actor<import("xstate").StateMachine<{

}, import("xstate").ProvidedActor, import("xstate").ParameterizedObject, import("xstate").ParameterizedObject, string, string>>>;
}, import("xstate").NonReducibleUnknown, import("xstate").ResolveTypegenMeta<import("xstate").TypegenDisabled, ConnectEvent | UpdateRendererEvent | RenderEvent | SetImage | SlowFps | FastFps | CameraPoseUpdated | ImageProcessorDone | UpdateImageScaleDone | {
}, import("xstate").NonReducibleUnknown, import("xstate").ResolveTypegenMeta<import("xstate").TypegenDisabled, ConnectEvent | UpdateRendererEvent | RenderEvent | ImageAssigned | SlowFps | FastFps | CameraPoseUpdated | ImageProcessorDone | UpdateImageScaleDone | {
type: 'setResolution';

@@ -176,7 +189,8 @@ resolution: [number, number];

clipBounds: Bounds;
imageScale?: number | undefined;
} | {
type: 'setImageScale';
imageScale: number;
} | {
type: 'sendCommands';
}, import("xstate").ProvidedActor, import("xstate").ParameterizedObject, import("xstate").ParameterizedObject, string, string>>;
export {};

@@ -17,5 +17,5 @@ import { mat4 } from 'gl-matrix';

const image = getImage(context);
if (context.rendererProps.imageScale === undefined)
if (context.rendererState.imageScale === undefined)
throw new Error('imageScale not found');
const currentScale = context.rendererProps.imageScale;
const currentScale = context.rendererState.imageScale;
const scaleChange = event.type === 'slowFps' ? 1 : -1;

@@ -25,6 +25,21 @@ const targetScale = currentScale + scaleChange;

};
const computeBytes = async (image, targetScale) => {
const voxelCount = await getVoxelCount(image, targetScale);
const computeBytes = async (image, targetScale, clipBounds) => {
const voxelCount = await getVoxelCount(image, targetScale, clipBounds);
return getBytes(image, voxelCount);
};
const EPSILON = 0.000001;
const checkBoundsBigger = (fullImage, benchmark, sample) => {
// clamp rendered bounds to max size of image
sample.forEach((b, i) => {
sample[i] =
i % 2
? Math.min(b, fullImage[i]) // high bound case
: Math.max(b, fullImage[i]); // low bound case
});
return benchmark.some((loaded, i) => {
return i % 2
? sample[i] - loaded > EPSILON // high bound case: currentBounds[i] > loadedBound
: loaded - sample[i] > EPSILON; // low bound case: currentBounds[i] < loadedBound
});
};
export const remoteMachine = createMachine({

@@ -34,3 +49,3 @@ types: {},

context: ({ input }) => ({
rendererProps: {
rendererState: {
density: 30,

@@ -41,4 +56,5 @@ cameraPose: mat4.create(),

},
queuedRendererEvents: [],
stagedRendererEvents: [],
queuedRendererCommands: [],
stagedRendererCommands: [],
imageScale: 0,
// computed async image values

@@ -48,2 +64,3 @@ toRendererCoordinateSystem: mat4.create(),

clipBounds: createBounds(),
loadedImageClipBounds: createBounds(),
imageWorldToIndex: mat4.create(),

@@ -58,3 +75,3 @@ maxImageBytes: MAX_IMAGE_BYTES_DEFAULT,

on: {
setImage: '.updatingScale',
imageAssigned: '.updatingScale',
setImageScale: '.updatingScale',

@@ -64,2 +81,14 @@ },

idle: {},
raiseCoarserScale: {
entry: raise(({ event }) => {
const result = event.output;
if (result.type === 'coarserScale') {
return {
type: 'setImageScale',
imageScale: result.imageScale,
};
}
throw new Error('Unexpected result type: ' + result.type);
}),
},
updatingScale: {

@@ -74,3 +103,3 @@ // Ensure imageScale is not the same as before and fits in memory

return event.imageScale;
if (event.type === 'setImage')
if (event.type === 'imageAssigned')
return image.coarsestScale;

@@ -87,25 +116,47 @@ throw new Error('Unexpected event type: ' + event.type);

const image = getImage(context);
if (imageScale === context.rendererProps.imageScale)
return;
const imageBytes = await computeBytes(image, imageScale);
if (imageBytes > context.maxImageBytes)
return;
return imageScale;
const boundsBiggerThanLoaded = checkBoundsBigger(context.imageWorldBounds, context.loadedImageClipBounds, context.clipBounds);
if (!boundsBiggerThanLoaded &&
imageScale === context.imageScale)
return { type: 'noChange' }; // no new data to load
// Always try to load base scale
if (imageScale !== image.coarsestScale) {
const imageBytes = await computeBytes(image, imageScale, context.clipBounds);
if (imageBytes > context.maxImageBytes)
return { type: 'coarserScale', imageScale: imageScale + 1 };
}
return { type: 'load', imageScale };
}),
onDone: {
guard: ({ event }) => event.output !== undefined,
target: 'updatingComputedValues',
},
onDone: [
{
guard: ({ event }) => event.output.type === 'coarserScale',
target: 'raiseCoarserScale',
},
{
guard: ({ event }) => event.output.type === 'load',
target: 'updatingComputedValues',
},
],
},
},
updatingComputedValues: {
entry: [
assign({
imageScale: ({ event }) => {
const result = event.output;
if (result.type === 'load')
return result.imageScale;
throw new Error('Unexpected result type: ' + result.type);
},
}),
],
// For new image scale, compute imageWorldBounds, imageWorldToIndex, toRendererCoordinateSystem
invoke: {
src: 'imageProcessor',
input: ({ context, event }) => {
input: ({ context }) => {
const image = getImage(context);
const imageScale = event.output;
const { imageScale, clipBounds } = context;
return {
image,
imageScale,
clipBounds,
};

@@ -120,2 +171,7 @@ },

}),
'computeImageIndexClipBounds',
assign({
loadedImageIndexBounds: ({ context }) => context.imageIndexClipBounds,
loadedImageClipBounds: ({ context }) => context.clipBounds,
}),
],

@@ -129,3 +185,3 @@ target: 'checkingFirstImage',

{
guard: ({ context }) => context.rendererProps.image === undefined,
guard: ({ context }) => context.rendererState.image === undefined,
target: 'initClipBounds',

@@ -141,3 +197,2 @@ },

clipBounds: event.output.bounds,
imageScale: event.output.imageScale,
};

@@ -148,16 +203,20 @@ }),

sendingToRenderer: {
entry: raise(({ event }) => {
const { image, imageScale } = event.output;
return {
type: 'updateRenderer',
props: {
image: image.name,
imageScale,
},
};
}),
entry: [
raise(({ event }) => {
const { image, imageScale } = event
.output;
return {
type: 'updateRenderer',
state: {
image: image.name,
imageScale,
},
};
}),
'updateNormalizedClipBounds',
],
},
},
},
// root state captures initial rendererProps events even when disconnected
// root state captures initial updateRenderer events, even when disconnected
root: {

@@ -173,15 +232,15 @@ entry: [

assign({
rendererProps: ({ event: { props }, context }) => {
rendererState: ({ event: { state }, context }) => {
return {
...context.rendererProps,
...props,
...context.rendererState,
...state,
};
},
queuedRendererEvents: ({ event: { props }, context }) => [
...context.queuedRendererEvents,
...getEntries(props),
queuedRendererCommands: ({ event: { state }, context }) => [
...context.queuedRendererCommands,
...getEntries(state),
],
}),
// Trigger a render (if in idle state)
raise({ type: 'render' }),
raise({ type: 'sendCommands' }),
],

@@ -194,3 +253,3 @@ },

type: 'updateRenderer',
props: { cameraPose: event.pose },
state: { cameraPose: event.pose },
};

@@ -205,3 +264,3 @@ }),

type: 'updateRenderer',
props: { renderSize: event.resolution },
state: { renderSize: event.resolution },
};

@@ -216,21 +275,8 @@ }),

}),
raise(({ context: { viewport, clipBounds, imageWorldToIndex, rendererProps, }, event, }) => {
var _a;
const imageScale = (_a = event.imageScale) !== null && _a !== void 0 ? _a : rendererProps.imageScale;
const { image } = viewport.getSnapshot().context;
if (!image || imageScale === undefined)
throw new Error('image or imageScale not found');
const fullIndexBounds = image.getIndexBounds(imageScale);
const imageIndexClipBounds = worldBoundsToIndexBounds({
bounds: clipBounds,
fullIndexBounds,
worldToIndex: imageWorldToIndex,
});
// Compute normalized bounds in image space
const spatialImageBounds = ensuredDims([0, 1], XYZ, fullIndexBounds);
const ranges = Object.fromEntries(XYZ.map((dim) => [dim, spatialImageBounds.get(dim)[1]]));
const normalizedClipBounds = XYZ.flatMap((dim) => { var _a; return (_a = imageIndexClipBounds.get(dim)) === null || _a === void 0 ? void 0 : _a.map((v) => v / ranges[dim]); });
'computeImageIndexClipBounds',
'updateNormalizedClipBounds',
raise(({ context }) => {
return {
type: 'updateRenderer',
props: { normalizedClipBounds },
type: 'setImageScale',
imageScale: context.imageScale,
};

@@ -268,4 +314,4 @@ }),

server: ({ event }) => event.output,
// initially, send all props to renderer
queuedRendererEvents: ({ context }) => getEntries(context.rendererProps),
// initially, send all commands to renderer
queuedRendererCommands: ({ context }) => getEntries(context.rendererState),
}),

@@ -305,2 +351,41 @@ target: 'online',

},
commandLoop: {
initial: 'sendingCommands',
states: {
sendingCommands: {
invoke: {
id: 'commandSender',
src: 'commandSender',
input: ({ context }) => ({
commands: [...context.stagedRendererCommands],
context,
}),
onDone: {
target: 'idle',
},
onError: {
actions: (e) => console.error(`Error while sending commands.`, e.event.data.stack ?? e.event.data),
target: 'idle', // soldier on
},
},
},
idle: {
always: {
// More commands while sending commands? Then send commands.
guard: ({ context }) => context.queuedRendererCommands.length > 0,
target: 'sendingCommands',
},
on: {
sendCommands: { target: 'sendingCommands' },
},
exit: assign({
// consumes queue in prep for renderer
stagedRendererCommands: ({ context }) => [
...context.queuedRendererCommands,
],
queuedRendererCommands: [],
}),
},
},
},
renderLoop: {

@@ -314,3 +399,2 @@ initial: 'render',

input: ({ context }) => ({
events: [...context.stagedRendererEvents],
context,

@@ -333,6 +417,3 @@ }),

onError: {
actions: (e) => {
var _a;
return console.error(`Error while updating renderer.`, (_a = e.event.data.stack) !== null && _a !== void 0 ? _a : e.event.data);
},
actions: (e) => console.error(`Error while updating renderer.`, e.event.data.stack ?? e.event.data),
target: 'idle', // soldier on

@@ -343,17 +424,5 @@ },

idle: {
always: {
// Renderer props changed while rendering? Then render.
guard: ({ context }) => context.queuedRendererEvents.length > 0,
target: 'render',
},
on: {
render: { target: 'render' },
},
exit: assign({
// consumes queue in prep for renderer
stagedRendererEvents: ({ context }) => [
...context.queuedRendererEvents,
],
queuedRendererEvents: [],
}),
},

@@ -367,3 +436,41 @@ },

},
}, {
actions: {
computeImageIndexClipBounds: assign({
imageIndexClipBounds: ({ context: { viewport, imageWorldToIndex, clipBounds, imageScale }, }) => {
const { image } = viewport.getSnapshot().context;
if (!image || imageScale === undefined)
throw new Error('image or imageScale not found');
const fullIndexBounds = image.getIndexBounds(imageScale);
return worldBoundsToIndexBounds({
bounds: clipBounds,
fullIndexBounds,
worldToIndex: imageWorldToIndex,
});
},
}),
updateNormalizedClipBounds: raise(({ context: { viewport, imageIndexClipBounds, loadedImageIndexBounds, imageScale, }, }) => {
const { image } = viewport.getSnapshot().context;
if (!image || imageScale === undefined)
throw new Error('image or imageScale not found');
if (!imageIndexClipBounds)
throw new Error('imageIndexClipBounds not found');
if (!loadedImageIndexBounds)
throw new Error('loadedImageIndexBounds not found');
// Compute normalized bounds in loaded image space
const spatialImageBounds = ensuredDims([0, 1], XYZ, loadedImageIndexBounds);
const normalizedClipBounds = XYZ.flatMap((dim) => {
const [floor, top] = spatialImageBounds.get(dim);
const range = top - floor;
return imageIndexClipBounds
.get(dim)
.map((v) => (v - floor) / range);
});
return {
type: 'updateRenderer',
state: { normalizedClipBounds },
};
}),
},
});
//# sourceMappingURL=remote-machine.js.map

@@ -6,2 +6,3 @@ /// <reference types="gl-matrix/index.js" />

import { RendererEntries, remoteMachine, Context } from './remote-machine.js';
import { Bounds } from '@itk-viewer/io/types.js';
export type { Image } from '@itk-wasm/htj2k';

@@ -24,3 +25,3 @@ type RenderedFrame = {

context: MachineContext;
events: RendererEntries;
commands: RendererEntries;
};

@@ -41,3 +42,3 @@ type ConnectInput = {

type: "updateRenderer";
props: Partial<{
state: Partial<{
density: number;

@@ -48,3 +49,3 @@ cameraPose: ReadonlyMat4;

imageScale?: number | undefined;
normalizedClipBounds: import("@itk-viewer/io/types.js").Bounds;
normalizedClipBounds: Bounds;
}>;

@@ -54,3 +55,3 @@ } | {

} | {
type: "setImage";
type: "imageAssigned";
image: import("@itk-viewer/io/MultiscaleSpatialImage.js").MultiscaleSpatialImage;

@@ -67,3 +68,3 @@ } | {

output: {
bounds: import("@itk-viewer/io/types.js").Bounds;
bounds: Bounds;
toRendererCoordinateSystem: ReadonlyMat4;

@@ -75,3 +76,11 @@ imageScale: number;

type: "xstate.done.actor.updateImageScale";
output: number;
output: {
type: "noChange";
} | {
type: "coarserScale";
imageScale: number;
} | {
type: "load";
imageScale: number;
};
} | {

@@ -82,7 +91,8 @@ type: "setResolution";

type: "setClipBounds";
clipBounds: import("@itk-viewer/io/types.js").Bounds;
imageScale?: number | undefined;
clipBounds: Bounds;
} | {
type: "setImageScale";
imageScale: number;
} | {
type: "sendCommands";
}, import("xstate").ProvidedActor, import("xstate").ParameterizedObject, import("xstate").ParameterizedObject, string, string, {

@@ -172,3 +182,3 @@ viewport: import("xstate").Actor<import("xstate").StateMachine<{

type: "updateRenderer";
props: Partial<{
state: Partial<{
density: number;

@@ -179,3 +189,3 @@ cameraPose: ReadonlyMat4;

imageScale?: number | undefined;
normalizedClipBounds: import("@itk-viewer/io/types.js").Bounds;
normalizedClipBounds: Bounds;
}>;

@@ -185,3 +195,3 @@ } | {

} | {
type: "setImage";
type: "imageAssigned";
image: import("@itk-viewer/io/MultiscaleSpatialImage.js").MultiscaleSpatialImage;

@@ -198,3 +208,3 @@ } | {

output: {
bounds: import("@itk-viewer/io/types.js").Bounds;
bounds: Bounds;
toRendererCoordinateSystem: ReadonlyMat4;

@@ -206,3 +216,11 @@ imageScale: number;

type: "xstate.done.actor.updateImageScale";
output: number;
output: {
type: "noChange";
} | {
type: "coarserScale";
imageScale: number;
} | {
type: "load";
imageScale: number;
};
} | {

@@ -213,7 +231,8 @@ type: "setResolution";

type: "setClipBounds";
clipBounds: import("@itk-viewer/io/types.js").Bounds;
imageScale?: number | undefined;
clipBounds: Bounds;
} | {
type: "setImageScale";
imageScale: number;
} | {
type: "sendCommands";
}, import("xstate").ProvidedActor, import("xstate").ParameterizedObject, import("xstate").ParameterizedObject, string, string>>>;

@@ -227,3 +246,3 @@ export type RemoteActor = ReturnType<typeof createRemote>;

type: "updateRenderer";
props: Partial<{
state: Partial<{
density: number;

@@ -234,3 +253,3 @@ cameraPose: ReadonlyMat4;

imageScale?: number | undefined;
normalizedClipBounds: import("@itk-viewer/io/types.js").Bounds;
normalizedClipBounds: Bounds;
}>;

@@ -240,3 +259,3 @@ } | {

} | {
type: "setImage";
type: "imageAssigned";
image: import("@itk-viewer/io/MultiscaleSpatialImage.js").MultiscaleSpatialImage;

@@ -253,3 +272,3 @@ } | {

output: {
bounds: import("@itk-viewer/io/types.js").Bounds;
bounds: Bounds;
toRendererCoordinateSystem: ReadonlyMat4;

@@ -261,3 +280,11 @@ imageScale: number;

type: "xstate.done.actor.updateImageScale";
output: number;
output: {
type: "noChange";
} | {
type: "coarserScale";
imageScale: number;
} | {
type: "load";
imageScale: number;
};
} | {

@@ -268,7 +295,8 @@ type: "setResolution";

type: "setClipBounds";
clipBounds: import("@itk-viewer/io/types.js").Bounds;
imageScale?: number | undefined;
clipBounds: Bounds;
} | {
type: "setImageScale";
imageScale: number;
} | {
type: "sendCommands";
}, import("xstate").ProvidedActor, import("xstate").ParameterizedObject, import("xstate").ParameterizedObject, string, string, {

@@ -358,3 +386,3 @@ viewport: import("xstate").Actor<import("xstate").StateMachine<{

type: "updateRenderer";
props: Partial<{
state: Partial<{
density: number;

@@ -365,3 +393,3 @@ cameraPose: ReadonlyMat4;

imageScale?: number | undefined;
normalizedClipBounds: import("@itk-viewer/io/types.js").Bounds;
normalizedClipBounds: Bounds;
}>;

@@ -371,3 +399,3 @@ } | {

} | {
type: "setImage";
type: "imageAssigned";
image: import("@itk-viewer/io/MultiscaleSpatialImage.js").MultiscaleSpatialImage;

@@ -384,3 +412,3 @@ } | {

output: {
bounds: import("@itk-viewer/io/types.js").Bounds;
bounds: Bounds;
toRendererCoordinateSystem: ReadonlyMat4;

@@ -392,3 +420,11 @@ imageScale: number;

type: "xstate.done.actor.updateImageScale";
output: number;
output: {
type: "noChange";
} | {
type: "coarserScale";
imageScale: number;
} | {
type: "load";
imageScale: number;
};
} | {

@@ -399,7 +435,8 @@ type: "setResolution";

type: "setClipBounds";
clipBounds: import("@itk-viewer/io/types.js").Bounds;
imageScale?: number | undefined;
clipBounds: Bounds;
} | {
type: "setImageScale";
imageScale: number;
} | {
type: "sendCommands";
}, import("xstate").ProvidedActor, import("xstate").ParameterizedObject, import("xstate").ParameterizedObject, string, string>>>;

@@ -406,0 +443,0 @@ viewport: import("xstate").ActorRef<{

@@ -6,2 +6,5 @@ import { createActor, fromPromise } from 'xstate';

import { remoteMachine } from './remote-machine.js';
import { XYZ } from '@itk-viewer/io/dimensionUtils.js';
import { worldBoundsToIndexBounds } from '@itk-viewer/io/MultiscaleSpatialImage.js';
import { transformBounds } from '@itk-viewer/io/transformBounds.js';
// Should match constant in agave-renderer::renderer.py

@@ -36,2 +39,23 @@ const RENDERER_SERVICE_ID = 'agave-renderer';

};
const makeLoadImageCommand = ([type, payload], context) => {
const { imageIndexClipBounds } = context;
if (!imageIndexClipBounds)
throw new Error('No image index clip bounds');
const image_path = type === 'image' ? payload : context.rendererState.image;
const multiresolution_level = type === 'imageScale' ? payload : context.rendererState.imageScale;
const channelRange = imageIndexClipBounds.get('c') ?? [0, 0];
const cDelta = channelRange[1] - channelRange[0];
const channels = Array.from({ length: cDelta + 1 }, // +1 for inclusive
(_, i) => i + channelRange[0]);
const region = XYZ.flatMap((dim) => imageIndexClipBounds.get(dim));
return [
'loadImage',
{
image_path,
multiresolution_level,
channels,
region,
},
];
};
export const createHyphaMachineConfig = () => {

@@ -42,4 +66,4 @@ let decodeWorker = null;

connect: fromPromise(async ({ input }) => createHyphaRenderer(input.context)),
renderer: fromPromise(async ({ input: { context, events } }) => {
const commands = events
commandSender: fromPromise(async ({ input: { context, commands } }) => {
const { commands: translatedCommands } = commands
.map(([key, value]) => {

@@ -49,20 +73,9 @@ if (key === 'cameraPose') {

}
if (key === 'image') {
const { imageScale: multiresolution_level } = context.rendererProps;
return [
'loadImage',
{ image_path: value, multiresolution_level },
];
if (key === 'image' || key === 'imageScale') {
return makeLoadImageCommand([key, value], context);
}
if (key === 'imageScale') {
const { image: image_path } = context.rendererProps;
return [
'loadImage',
{ image_path, multiresolution_level: value },
];
}
return [key, value];
})
.flatMap((event) => {
const [type] = event;
.flatMap((command) => {
const [type] = command;
if (type === 'loadImage') {

@@ -72,10 +85,24 @@ // Resend camera pose after load image.

return [
event,
makeCameraPoseCommand(context.toRendererCoordinateSystem, context.rendererProps.cameraPose),
command,
makeCameraPoseCommand(context.toRendererCoordinateSystem, context.rendererState.cameraPose),
];
}
return [event];
return [command];
})
.reduceRight(
// filter duplicate commands
({ commands, seenCommands }, command) => {
const [type] = command;
if (seenCommands.has(type)) {
return { commands, seenCommands };
}
seenCommands.add(type);
return { commands: [command, ...commands], seenCommands };
}, {
commands: [],
seenCommands: new Set(),
});
const { server } = context;
server.updateRenderer(commands);
context.server.updateRenderer(translatedCommands);
}),
renderer: fromPromise(async ({ input: { context: { server }, }, }) => {
const { frame: encodedImage, renderTime } = await server.render();

@@ -87,6 +114,13 @@ const { image: frame, webWorker } = await decode(decodeWorker, encodedImage);

// Computes world bounds and transform from VTK to Agave coordinate system
imageProcessor: fromPromise(async ({ input: { image, imageScale } }) => {
const bounds = await image.getWorldBounds(imageScale);
imageProcessor: fromPromise(async ({ input: { image, imageScale, clipBounds } }) => {
const indexToWorld = await image.scaleIndexToWorld(imageScale);
const imageWorldToIndex = mat4.invert(mat4.create(), indexToWorld);
const fullIndexBounds = image.getIndexBounds(imageScale);
const byDim = worldBoundsToIndexBounds({
bounds: clipBounds,
fullIndexBounds,
worldToIndex: imageWorldToIndex,
});
// loaded image world bounds
const bounds = transformBounds(indexToWorld, XYZ.flatMap((dim) => byDim.get(dim)));
// Remove image origin offset to world origin

@@ -104,5 +138,6 @@ const imageOrigin = vec3.fromValues(bounds[0], bounds[2], bounds[4]);

mat4.invert(transform, transform);
const fullWorldBounds = await image.getWorldBounds(imageScale);
return {
toRendererCoordinateSystem: transform,
bounds,
bounds: fullWorldBounds,
image,

@@ -109,0 +144,0 @@ imageScale,

{
"name": "@itk-viewer/remote-viewport",
"version": "0.2.7",
"version": "0.2.8",
"description": "",

@@ -24,8 +24,8 @@ "type": "module",

"dependencies": {
"@itk-wasm/htj2k": "^1.1.0",
"@itk-wasm/htj2k": "2.1.0",
"gl-matrix": "^3.4.3",
"imjoy-rpc": "^0.5.44",
"xstate": "5.0.0-beta.34",
"@itk-viewer/io": "^0.1.4",
"@itk-viewer/viewer": "^0.2.4"
"@itk-viewer/io": "^0.1.5",
"@itk-viewer/viewer": "^0.2.5"
},

@@ -32,0 +32,0 @@ "scripts": {

@@ -30,3 +30,3 @@ import { ReadonlyMat4, mat4 } from 'gl-matrix';

type RendererProps = {
type RendererState = {
density: number;

@@ -49,3 +49,3 @@ cameraPose: ReadonlyMat4;

// example: [['density', 30], ['cameraPose', mat4.create()]]
export type RendererEntries = Entries<RendererProps>;
export type RendererEntries = Entries<RendererState>;

@@ -56,7 +56,8 @@ export type Context = {

frame?: Image;
rendererProps: RendererProps;
queuedRendererEvents: RendererEntries;
stagedRendererEvents: RendererEntries;
rendererState: RendererState;
queuedRendererCommands: RendererEntries;
stagedRendererCommands: RendererEntries;
viewport: ActorRefFrom<typeof viewportMachine>;
maxImageBytes: number;
imageScale: number;
// computed image values

@@ -66,3 +67,5 @@ toRendererCoordinateSystem: ReadonlyMat4;

imageIndexClipBounds?: ReadOnlyDimensionBounds;
loadedImageIndexBounds?: ReadOnlyDimensionBounds;
clipBounds: Bounds;
loadedImageClipBounds: Bounds;
imageWorldToIndex: ReadonlyMat4;

@@ -78,3 +81,3 @@ };

type: 'updateRenderer';
props: Partial<RendererProps>;
state: Partial<RendererState>;
};

@@ -86,4 +89,4 @@

type SetImage = {
type: 'setImage';
type ImageAssigned = {
type: 'imageAssigned';
image: MultiscaleSpatialImage;

@@ -115,5 +118,10 @@ };

type UpdateImageScaleResult =
| { type: 'noChange' }
| { type: 'coarserScale'; imageScale: number }
| { type: 'load'; imageScale: number };
type UpdateImageScaleDone = {
type: 'xstate.done.actor.updateImageScale';
output: number;
output: UpdateImageScaleResult;
};

@@ -125,3 +133,3 @@

| RenderEvent
| SetImage
| ImageAssigned
| SlowFps

@@ -133,4 +141,5 @@ | FastFps

| { type: 'setResolution'; resolution: [number, number] }
| { type: 'setClipBounds'; clipBounds: Bounds; imageScale?: number }
| { type: 'setImageScale'; imageScale: number };
| { type: 'setClipBounds'; clipBounds: Bounds }
| { type: 'setImageScale'; imageScale: number }
| { type: 'sendCommands' };

@@ -147,6 +156,6 @@ type ActionArgs = { event: Event; context: Context };

const image = getImage(context);
if (context.rendererProps.imageScale === undefined)
if (context.rendererState.imageScale === undefined)
throw new Error('imageScale not found');
const currentScale = context.rendererProps.imageScale;
const currentScale = context.rendererState.imageScale;
const scaleChange = event.type === 'slowFps' ? 1 : -1;

@@ -160,359 +169,439 @@ const targetScale = currentScale + scaleChange;

targetScale: number,
clipBounds: Bounds,
) => {
const voxelCount = await getVoxelCount(image, targetScale);
const voxelCount = await getVoxelCount(image, targetScale, clipBounds);
return getBytes(image, voxelCount);
};
export const remoteMachine = createMachine({
types: {} as {
context: Context;
events: Event;
},
id: 'remote',
context: ({ input }: { input: { viewport: Viewport } }) => ({
rendererProps: {
density: 30,
cameraPose: mat4.create(),
renderSize: [1, 1] as [number, number],
normalizedClipBounds: [0, 1, 0, 1, 0, 1] as Bounds,
},
queuedRendererEvents: [],
stagedRendererEvents: [],
const EPSILON = 0.000001;
// computed async image values
toRendererCoordinateSystem: mat4.create(),
imageWorldBounds: createBounds(),
clipBounds: createBounds(),
imageWorldToIndex: mat4.create(),
const checkBoundsBigger = (
fullImage: Bounds,
benchmark: Bounds,
sample: Bounds,
) => {
// clamp rendered bounds to max size of image
sample.forEach((b, i) => {
sample[i] =
i % 2
? Math.min(b, fullImage[i]) // high bound case
: Math.max(b, fullImage[i]); // low bound case
});
maxImageBytes: MAX_IMAGE_BYTES_DEFAULT,
...input,
}),
type: 'parallel',
states: {
imageProcessor: {
initial: 'idle',
on: {
setImage: '.updatingScale',
setImageScale: '.updatingScale',
return benchmark.some((loaded, i) => {
return i % 2
? sample[i] - loaded > EPSILON // high bound case: currentBounds[i] > loadedBound
: loaded - sample[i] > EPSILON; // low bound case: currentBounds[i] < loadedBound
});
};
export const remoteMachine = createMachine(
{
types: {} as {
context: Context;
events: Event;
},
id: 'remote',
context: ({ input }: { input: { viewport: Viewport } }) => ({
rendererState: {
density: 30,
cameraPose: mat4.create(),
renderSize: [1, 1] as [number, number],
normalizedClipBounds: [0, 1, 0, 1, 0, 1] as Bounds,
},
states: {
idle: {},
updatingScale: {
// Ensure imageScale is not the same as before and fits in memory
id: 'updateImageScale',
invoke: {
input: ({ context, event }) => {
const image = getImage(context);
const getImageScale = () => {
if (event.type === 'setImageScale') return event.imageScale;
if (event.type === 'setImage') return image.coarsestScale;
throw new Error('Unexpected event type: ' + event.type);
};
const imageScale = getImageScale();
return {
context,
imageScale,
};
},
src: fromPromise(async ({ input: { imageScale, context } }) => {
const image = getImage(context);
queuedRendererCommands: [],
stagedRendererCommands: [],
if (imageScale === context.rendererProps.imageScale) return;
imageScale: 0,
// computed async image values
toRendererCoordinateSystem: mat4.create(),
imageWorldBounds: createBounds(),
clipBounds: createBounds(),
loadedImageClipBounds: createBounds(),
imageWorldToIndex: mat4.create(),
const imageBytes = await computeBytes(image, imageScale);
if (imageBytes > context.maxImageBytes) return;
return imageScale;
maxImageBytes: MAX_IMAGE_BYTES_DEFAULT,
...input,
}),
type: 'parallel',
states: {
imageProcessor: {
initial: 'idle',
on: {
imageAssigned: '.updatingScale',
setImageScale: '.updatingScale',
},
states: {
idle: {},
raiseCoarserScale: {
entry: raise(({ event }) => {
const result = (event as UpdateImageScaleDone).output;
if (result.type === 'coarserScale') {
return {
type: 'setImageScale' as const,
imageScale: result.imageScale,
};
}
throw new Error('Unexpected result type: ' + result.type);
}),
onDone: {
guard: ({ event }) => event.output !== undefined,
target: 'updatingComputedValues',
},
},
},
updatingComputedValues: {
// For new image scale, compute imageWorldBounds, imageWorldToIndex, toRendererCoordinateSystem
invoke: {
src: 'imageProcessor',
input: ({ context, event }) => {
const image = getImage(context);
const imageScale = (event as UpdateImageScaleDone).output;
return {
image,
imageScale,
};
},
onDone: {
actions: [
assign({
toRendererCoordinateSystem: ({
event: {
output: { toRendererCoordinateSystem },
},
}) => toRendererCoordinateSystem,
imageWorldBounds: ({
event: {
output: { bounds },
},
}) => bounds,
imageWorldToIndex: ({
event: {
output: { imageWorldToIndex },
},
}) => imageWorldToIndex,
}),
updatingScale: {
// Ensure imageScale is not the same as before and fits in memory
id: 'updateImageScale',
invoke: {
input: ({ context, event }) => {
const image = getImage(context);
const getImageScale = () => {
if (event.type === 'setImageScale') return event.imageScale;
if (event.type === 'imageAssigned')
return image.coarsestScale;
throw new Error('Unexpected event type: ' + event.type);
};
const imageScale = getImageScale();
return {
context,
imageScale,
};
},
src: fromPromise(async ({ input: { imageScale, context } }) => {
const image = getImage(context);
const boundsBiggerThanLoaded = checkBoundsBigger(
context.imageWorldBounds,
context.loadedImageClipBounds,
context.clipBounds,
);
if (
!boundsBiggerThanLoaded &&
imageScale === context.imageScale
)
return { type: 'noChange' }; // no new data to load
// Always try to load base scale
if (imageScale !== image.coarsestScale) {
const imageBytes = await computeBytes(
image,
imageScale,
context.clipBounds,
);
if (imageBytes > context.maxImageBytes)
return { type: 'coarserScale', imageScale: imageScale + 1 };
}
return { type: 'load', imageScale };
}),
onDone: [
{
guard: ({ event }) => event.output.type === 'coarserScale',
target: 'raiseCoarserScale',
},
{
guard: ({ event }) => event.output.type === 'load',
target: 'updatingComputedValues',
},
],
target: 'checkingFirstImage',
},
},
},
checkingFirstImage: {
always: [
{
guard: ({ context }) => context.rendererProps.image === undefined,
target: 'initClipBounds',
},
{ target: 'sendingToRenderer' },
],
},
initClipBounds: {
entry: raise(({ event }) => {
return {
type: 'setClipBounds' as const,
clipBounds: (event as ImageProcessorDone).output.bounds,
imageScale: (event as ImageProcessorDone).output.imageScale,
};
}),
always: 'sendingToRenderer',
},
sendingToRenderer: {
entry: raise(({ event }) => {
const { image, imageScale } = (event as ImageProcessorDone).output;
return {
type: 'updateRenderer' as const,
props: {
image: image.name,
imageScale,
},
};
}),
},
},
},
// root state captures initial rendererProps events even when disconnected
root: {
entry: [
assign({
viewport: ({ spawn }) => spawn(viewportMachine, { id: 'viewport' }),
}),
],
on: {
updateRenderer: {
actions: [
assign({
rendererProps: ({ event: { props }, context }) => {
updatingComputedValues: {
entry: [
assign({
imageScale: ({ event }) => {
const result = (event as UpdateImageScaleDone).output;
if (result.type === 'load') return result.imageScale;
throw new Error('Unexpected result type: ' + result.type);
},
}),
],
// For new image scale, compute imageWorldBounds, imageWorldToIndex, toRendererCoordinateSystem
invoke: {
src: 'imageProcessor',
input: ({ context }) => {
const image = getImage(context);
const { imageScale, clipBounds } = context;
return {
...context.rendererProps,
...props,
image,
imageScale,
clipBounds,
};
},
queuedRendererEvents: ({ event: { props }, context }) => [
...context.queuedRendererEvents,
...(getEntries(props) as RendererEntries),
],
}),
// Trigger a render (if in idle state)
raise({ type: 'render' }),
],
},
cameraPoseUpdated: {
actions: [
raise(({ event }) => {
onDone: {
actions: [
assign({
toRendererCoordinateSystem: ({
event: {
output: { toRendererCoordinateSystem },
},
}) => toRendererCoordinateSystem,
imageWorldBounds: ({
event: {
output: { bounds },
},
}) => bounds,
imageWorldToIndex: ({
event: {
output: { imageWorldToIndex },
},
}) => imageWorldToIndex,
}),
'computeImageIndexClipBounds',
assign({
loadedImageIndexBounds: ({ context }) =>
context.imageIndexClipBounds,
loadedImageClipBounds: ({ context }) => context.clipBounds,
}),
],
target: 'checkingFirstImage',
},
},
},
checkingFirstImage: {
always: [
{
guard: ({ context }) =>
context.rendererState.image === undefined,
target: 'initClipBounds',
},
{ target: 'sendingToRenderer' },
],
},
initClipBounds: {
entry: raise(({ event }) => {
return {
type: 'updateRenderer' as const,
props: { cameraPose: event.pose },
type: 'setClipBounds' as const,
clipBounds: (event as ImageProcessorDone).output.bounds,
};
}),
],
always: 'sendingToRenderer',
},
sendingToRenderer: {
entry: [
raise(({ event }) => {
const { image, imageScale } = (event as ImageProcessorDone)
.output;
return {
type: 'updateRenderer' as const,
state: {
image: image.name,
imageScale,
},
};
}),
'updateNormalizedClipBounds',
],
},
},
setResolution: {
actions: [
raise(({ event }) => {
return {
type: 'updateRenderer' as const,
props: { renderSize: event.resolution },
};
}),
],
},
setClipBounds: {
actions: [
assign({
clipBounds: ({ event: { clipBounds } }) => clipBounds,
}),
raise(
({
context: {
viewport,
clipBounds,
imageWorldToIndex,
rendererProps,
},
// root state captures initial updateRenderer events, even when disconnected
root: {
entry: [
assign({
viewport: ({ spawn }) => spawn(viewportMachine, { id: 'viewport' }),
}),
],
on: {
updateRenderer: {
actions: [
assign({
rendererState: ({ event: { state }, context }) => {
return {
...context.rendererState,
...state,
};
},
event,
}) => {
const imageScale = event.imageScale ?? rendererProps.imageScale;
const { image } = viewport.getSnapshot().context;
if (!image || imageScale === undefined)
throw new Error('image or imageScale not found');
const fullIndexBounds = image.getIndexBounds(imageScale);
const imageIndexClipBounds = worldBoundsToIndexBounds({
bounds: clipBounds,
fullIndexBounds,
worldToIndex: imageWorldToIndex,
});
// Compute normalized bounds in image space
const spatialImageBounds = ensuredDims(
[0, 1],
XYZ,
fullIndexBounds,
);
const ranges = Object.fromEntries(
XYZ.map((dim) => [dim, spatialImageBounds.get(dim)![1]]),
);
const normalizedClipBounds = XYZ.flatMap(
(dim) =>
imageIndexClipBounds.get(dim)?.map((v) => v / ranges[dim]),
) as Bounds;
queuedRendererCommands: ({ event: { state }, context }) => [
...context.queuedRendererCommands,
...(getEntries(state) as RendererEntries),
],
}),
// Trigger a render (if in idle state)
raise({ type: 'sendCommands' }),
],
},
cameraPoseUpdated: {
actions: [
raise(({ event }) => {
return {
type: 'updateRenderer' as const,
props: { normalizedClipBounds },
state: { cameraPose: event.pose },
};
}),
],
},
setResolution: {
actions: [
raise(({ event }) => {
return {
type: 'updateRenderer' as const,
state: { renderSize: event.resolution },
};
}),
],
},
setClipBounds: {
actions: [
assign({
clipBounds: ({ event: { clipBounds } }) => clipBounds,
}),
'computeImageIndexClipBounds',
'updateNormalizedClipBounds',
raise(({ context }) => {
return {
type: 'setImageScale' as const,
imageScale: context.imageScale,
};
}),
],
},
},
initial: 'disconnected',
states: {
disconnected: {
on: {
connect: {
actions: [
assign({
serverConfig: ({
event: { config },
}: {
event: ConnectEvent;
}) => {
return config;
},
}),
],
target: 'connecting',
},
),
],
},
},
initial: 'disconnected',
states: {
disconnected: {
on: {
connect: {
actions: [
assign({
serverConfig: ({
event: { config },
}: {
event: ConnectEvent;
}) => {
return config;
},
}),
],
target: 'connecting',
},
},
},
connecting: {
invoke: {
id: 'connect',
src: 'connect',
input: ({ context, event }) => ({
context,
event,
}),
onDone: {
actions: assign({
server: ({ event }) => event.output,
// initially, send all props to renderer
queuedRendererEvents: ({ context }) =>
getEntries(context.rendererProps),
connecting: {
invoke: {
id: 'connect',
src: 'connect',
input: ({ context, event }) => ({
context,
event,
}),
target: 'online',
onDone: {
actions: assign({
server: ({ event }) => event.output,
// initially, send all commands to renderer
queuedRendererCommands: ({ context }) =>
getEntries(context.rendererState),
}),
target: 'online',
},
},
},
},
online: {
type: 'parallel',
states: {
fpsWatcher: {
invoke: {
id: 'fpsWatcher',
src: fpsWatcher,
online: {
type: 'parallel',
states: {
fpsWatcher: {
invoke: {
id: 'fpsWatcher',
src: fpsWatcher,
},
},
},
imageScaleUpdater: {
on: {
slowFps: {
actions: raise(({ context, event }) => {
return {
type: 'setImageScale' as const,
imageScale: getTargetScale({ context, event }),
};
}),
imageScaleUpdater: {
on: {
slowFps: {
actions: raise(({ context, event }) => {
return {
type: 'setImageScale' as const,
imageScale: getTargetScale({ context, event }),
};
}),
},
fastFps: {
actions: raise(({ context, event }) => {
return {
type: 'setImageScale' as const,
imageScale: getTargetScale({ context, event }),
};
}),
},
},
fastFps: {
actions: raise(({ context, event }) => {
return {
type: 'setImageScale' as const,
imageScale: getTargetScale({ context, event }),
};
}),
},
},
},
renderLoop: {
initial: 'render',
states: {
render: {
invoke: {
id: 'renderer',
src: 'renderer',
input: ({ context }: { context: Context }) => ({
events: [...context.stagedRendererEvents],
context,
}),
onDone: {
actions: [
assign({
frame: ({ event }) => {
return event.output.frame;
},
}),
sendTo('fpsWatcher', ({ event }) => ({
type: 'newSample',
renderTime: event.output.renderTime,
})),
],
target: 'idle',
commandLoop: {
initial: 'sendingCommands',
states: {
sendingCommands: {
invoke: {
id: 'commandSender',
src: 'commandSender',
input: ({ context }: { context: Context }) => ({
commands: [...context.stagedRendererCommands],
context,
}),
onDone: {
target: 'idle',
},
onError: {
actions: (e) =>
console.error(
`Error while sending commands.`,
(e.event.data as Error).stack ?? e.event.data,
),
target: 'idle', // soldier on
},
},
onError: {
actions: (e) =>
console.error(
`Error while updating renderer.`,
(e.event.data as Error).stack ?? e.event.data,
),
target: 'idle', // soldier on
},
idle: {
always: {
// More commands while sending commands? Then send commands.
guard: ({ context }) =>
context.queuedRendererCommands.length > 0,
target: 'sendingCommands',
},
on: {
sendCommands: { target: 'sendingCommands' },
},
exit: assign({
// consumes queue in prep for renderer
stagedRendererCommands: ({ context }) => [
...context.queuedRendererCommands,
],
queuedRendererCommands: [],
}),
},
},
idle: {
always: {
// Renderer props changed while rendering? Then render.
guard: ({ context }) =>
context.queuedRendererEvents.length > 0,
target: 'render',
},
renderLoop: {
initial: 'render',
states: {
render: {
invoke: {
id: 'renderer',
src: 'renderer',
input: ({ context }: { context: Context }) => ({
context,
}),
onDone: {
actions: [
assign({
frame: ({ event }) => {
return event.output.frame;
},
}),
sendTo('fpsWatcher', ({ event }) => ({
type: 'newSample',
renderTime: event.output.renderTime,
})),
],
target: 'idle',
},
onError: {
actions: (e) =>
console.error(
`Error while updating renderer.`,
(e.event.data as Error).stack ?? e.event.data,
),
target: 'idle', // soldier on
},
},
},
on: {
render: { target: 'render' },
idle: {
on: {
render: { target: 'render' },
},
},
exit: assign({
// consumes queue in prep for renderer
stagedRendererEvents: ({ context }) => [
...context.queuedRendererEvents,
],
queuedRendererEvents: [],
}),
},

@@ -526,2 +615,57 @@ },

},
});
{
actions: {
computeImageIndexClipBounds: assign({
imageIndexClipBounds: ({
context: { viewport, imageWorldToIndex, clipBounds, imageScale },
}) => {
const { image } = viewport.getSnapshot().context;
if (!image || imageScale === undefined)
throw new Error('image or imageScale not found');
const fullIndexBounds = image.getIndexBounds(imageScale);
return worldBoundsToIndexBounds({
bounds: clipBounds,
fullIndexBounds,
worldToIndex: imageWorldToIndex,
});
},
}),
updateNormalizedClipBounds: raise(
({
context: {
viewport,
imageIndexClipBounds,
loadedImageIndexBounds,
imageScale,
},
}) => {
const { image } = viewport.getSnapshot().context;
if (!image || imageScale === undefined)
throw new Error('image or imageScale not found');
if (!imageIndexClipBounds)
throw new Error('imageIndexClipBounds not found');
if (!loadedImageIndexBounds)
throw new Error('loadedImageIndexBounds not found');
// Compute normalized bounds in loaded image space
const spatialImageBounds = ensuredDims(
[0, 1],
XYZ,
loadedImageIndexBounds,
);
const normalizedClipBounds = XYZ.flatMap((dim) => {
const [floor, top] = spatialImageBounds.get(dim)!;
const range = top - floor;
return imageIndexClipBounds
.get(dim)!
.map((v) => (v - floor) / range);
}) as Bounds;
return {
type: 'updateRenderer' as const,
state: { normalizedClipBounds },
};
},
),
},
},
);

@@ -6,2 +6,6 @@ import { createActor, fromPromise } from 'xstate';

import { RendererEntries, remoteMachine, Context } from './remote-machine.js';
import { XYZ } from '@itk-viewer/io/dimensionUtils.js';
import { worldBoundsToIndexBounds } from '@itk-viewer/io/MultiscaleSpatialImage.js';
import { Bounds } from '@itk-viewer/io/types.js';
import { transformBounds } from '@itk-viewer/io/transformBounds.js';

@@ -27,3 +31,3 @@ export type { Image } from '@itk-wasm/htj2k';

context: MachineContext;
events: RendererEntries;
commands: RendererEntries;
};

@@ -73,2 +77,4 @@

type Command = [string, unknown];
const makeCameraPoseCommand = (

@@ -84,5 +90,29 @@ toRendererCoordinateSystem: ReadonlyMat4,

);
return ['cameraPose', mat4ToLookAt(transform)];
return ['cameraPose', mat4ToLookAt(transform)] as Command;
};
const makeLoadImageCommand = ([type, payload]: Command, context: Context) => {
const { imageIndexClipBounds } = context;
if (!imageIndexClipBounds) throw new Error('No image index clip bounds');
const image_path = type === 'image' ? payload : context.rendererState.image;
const multiresolution_level =
type === 'imageScale' ? payload : context.rendererState.imageScale;
const channelRange = imageIndexClipBounds.get('c') ?? [0, 0];
const cDelta = channelRange[1] - channelRange[0];
const channels = Array.from(
{ length: cDelta + 1 }, // +1 for inclusive
(_, i) => i + channelRange[0],
);
const region = XYZ.flatMap((dim) => imageIndexClipBounds.get(dim));
return [
'loadImage',
{
image_path,
multiresolution_level,
channels,
region,
},
] as Command;
};
export const createHyphaMachineConfig: () => RemoteMachineOptions = () => {

@@ -96,5 +126,5 @@ let decodeWorker: Worker | null = null;

),
renderer: fromPromise(
async ({ input: { context, events } }: { input: RendererInput }) => {
const commands = events
commandSender: fromPromise(
async ({ input: { context, commands } }: { input: RendererInput }) => {
const { commands: translatedCommands } = commands
.map(([key, value]) => {

@@ -107,24 +137,9 @@ if (key === 'cameraPose') {

}
if (key === 'image') {
const { imageScale: multiresolution_level } =
context.rendererProps;
return [
'loadImage',
{ image_path: value, multiresolution_level },
];
if (key === 'image' || key === 'imageScale') {
return makeLoadImageCommand([key, value], context);
}
if (key === 'imageScale') {
const { image: image_path } = context.rendererProps;
return [
'loadImage',
{ image_path, multiresolution_level: value },
];
}
return [key, value];
})
.flatMap((event) => {
const [type] = event;
.flatMap((command) => {
const [type] = command;
if (type === 'loadImage') {

@@ -134,14 +149,36 @@ // Resend camera pose after load image.

return [
event,
command as Command,
makeCameraPoseCommand(
context.toRendererCoordinateSystem,
context.rendererProps.cameraPose,
context.rendererState.cameraPose,
),
];
}
return [event];
});
return [command as Command];
})
.reduceRight(
// filter duplicate commands
({ commands, seenCommands }, command) => {
const [type] = command;
if (seenCommands.has(type)) {
return { commands, seenCommands };
}
seenCommands.add(type);
return { commands: [command, ...commands], seenCommands };
},
{
commands: [] as Array<Command>,
seenCommands: new Set<string>(),
},
);
const { server } = context;
server.updateRenderer(commands);
context.server.updateRenderer(translatedCommands);
},
),
renderer: fromPromise(
async ({
input: {
context: { server },
},
}) => {
const { frame: encodedImage, renderTime } = await server.render();

@@ -158,31 +195,45 @@ const { image: frame, webWorker } = await decode(

// Computes world bounds and transform from VTK to Agave coordinate system
imageProcessor: fromPromise(async ({ input: { image, imageScale } }) => {
const bounds = await image.getWorldBounds(imageScale);
imageProcessor: fromPromise(
async ({ input: { image, imageScale, clipBounds } }) => {
const indexToWorld = await image.scaleIndexToWorld(imageScale);
const imageWorldToIndex = mat4.invert(mat4.create(), indexToWorld);
const indexToWorld = await image.scaleIndexToWorld(imageScale);
const imageWorldToIndex = mat4.invert(mat4.create(), indexToWorld);
const fullIndexBounds = image.getIndexBounds(imageScale);
const byDim = worldBoundsToIndexBounds({
bounds: clipBounds,
fullIndexBounds,
worldToIndex: imageWorldToIndex,
});
// Remove image origin offset to world origin
const imageOrigin = vec3.fromValues(bounds[0], bounds[2], bounds[4]);
const transform = mat4.fromTranslation(mat4.create(), imageOrigin);
// loaded image world bounds
const bounds = transformBounds(
indexToWorld,
XYZ.flatMap((dim) => byDim.get(dim)) as Bounds,
);
// match Agave by normalizing to largest dim
const wx = bounds[1] - bounds[0];
const wy = bounds[3] - bounds[2];
const wz = bounds[5] - bounds[4];
const maxDim = Math.max(wx, wy, wz);
const scale = vec3.fromValues(maxDim, maxDim, maxDim);
mat4.scale(transform, transform, scale);
// Remove image origin offset to world origin
const imageOrigin = vec3.fromValues(bounds[0], bounds[2], bounds[4]);
const transform = mat4.fromTranslation(mat4.create(), imageOrigin);
// invert to go from VTK to Agave
mat4.invert(transform, transform);
// match Agave by normalizing to largest dim
const wx = bounds[1] - bounds[0];
const wy = bounds[3] - bounds[2];
const wz = bounds[5] - bounds[4];
const maxDim = Math.max(wx, wy, wz);
const scale = vec3.fromValues(maxDim, maxDim, maxDim);
mat4.scale(transform, transform, scale);
return {
toRendererCoordinateSystem: transform,
bounds,
image,
imageScale,
imageWorldToIndex,
};
}),
// invert to go from VTK to Agave
mat4.invert(transform, transform);
const fullWorldBounds = await image.getWorldBounds(imageScale);
return {
toRendererCoordinateSystem: transform,
bounds: fullWorldBounds,
image,
imageScale,
imageWorldToIndex,
};
},
),
},

@@ -189,0 +240,0 @@ };

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc