@itk-viewer/viewer
Advanced tools
Comparing version 0.3.0 to 0.4.0
# @itk-viewer/viewer | ||
## 0.4.0 | ||
### Minor Changes | ||
- 99d78de: View-2d slices in IJK image space instead of world XYZ. | ||
- ff46215: Add slice axis GUI controls for View 2D | ||
### Patch Changes | ||
- Updated dependencies [99d78de] | ||
- @itk-viewer/io@0.2.0 | ||
## 0.3.0 | ||
@@ -4,0 +16,0 @@ |
@@ -125,3 +125,3 @@ /// <reference types="gl-matrix/index.js" /> | ||
}; | ||
export declare const reset2d: (pose: Pose, verticalFieldOfView: number, bounds: Bounds, aspect: number) => { | ||
export declare const reset2d: (pose: Pose, verticalFieldOfView: number, pointsToFit: Array<ReadonlyVec3>, aspect: number) => { | ||
center: vec3; | ||
@@ -128,0 +128,0 @@ rotation: quat; |
@@ -1,2 +0,2 @@ | ||
import { addPoint, createBounds, getCorners, getLength, } from '@itk-viewer/utils/bounding-box.js'; | ||
import { addPoint, createBounds, getLength, } from '@itk-viewer/utils/bounding-box.js'; | ||
import { mat4, vec3, quat } from 'gl-matrix'; | ||
@@ -135,13 +135,17 @@ import { assign, createActor, setup } from 'xstate'; | ||
}; | ||
export const reset2d = (pose, verticalFieldOfView, bounds, aspect) => { | ||
const center = vec3.fromValues((bounds[0] + bounds[1]) / 2.0, (bounds[2] + bounds[3]) / 2.0, (bounds[4] + bounds[5]) / 2.0); | ||
export const reset2d = (pose, verticalFieldOfView, pointsToFit, aspect) => { | ||
const center = vec3.create(); | ||
for (let i = 0; i < pointsToFit.length; ++i) { | ||
vec3.add(center, center, pointsToFit[i]); | ||
} | ||
vec3.scale(center, center, 1.0 / pointsToFit.length); | ||
// Get the bounds in view coordinates | ||
const viewBounds = createBounds(); | ||
const visiblePoints = getCorners(bounds); | ||
const viewMat = mat4.create(); | ||
toMat4(viewMat, pose); | ||
for (let i = 0; i < visiblePoints.length; ++i) { | ||
const point = visiblePoints[i]; | ||
vec3.transformMat4(point, point, viewMat); | ||
addPoint(viewBounds, ...point); | ||
const viewSpacePoint = vec3.create(); | ||
for (let i = 0; i < pointsToFit.length; ++i) { | ||
const point = pointsToFit[i]; | ||
vec3.transformMat4(viewSpacePoint, point, viewMat); | ||
addPoint(viewBounds, viewSpacePoint[0], viewSpacePoint[1], viewSpacePoint[2]); | ||
} | ||
@@ -148,0 +152,0 @@ const xLength = getLength(viewBounds, 0); |
@@ -6,11 +6,11 @@ import { Actor, AnyActorRef } from 'xstate'; | ||
import { ViewportActor } from './viewport.js'; | ||
export declare const AXIS: { | ||
readonly X: "x"; | ||
readonly Y: "y"; | ||
readonly Z: "z"; | ||
export declare const Axis: { | ||
readonly I: "I"; | ||
readonly J: "J"; | ||
readonly K: "K"; | ||
}; | ||
export type Axis = ValueOf<typeof AXIS>; | ||
export type AxisType = ValueOf<typeof Axis>; | ||
export declare const view2d: import("xstate").StateMachine<{ | ||
slice: number; | ||
axis: Axis; | ||
axis: AxisType; | ||
scale: number; | ||
@@ -62,2 +62,5 @@ image: MultiscaleSpatialImage | undefined; | ||
} | { | ||
type: 'setAxis'; | ||
axis: AxisType; | ||
} | { | ||
type: 'setScale'; | ||
@@ -116,8 +119,9 @@ scale: number; | ||
slice: number; | ||
axis: Axis; | ||
axis: AxisType; | ||
}>, { | ||
[k: string]: unknown; | ||
type: string; | ||
}> | import("xstate").ActorRef<import("xstate").PromiseSnapshot<"x" | "y" | "z", { | ||
}> | import("xstate").ActorRef<import("xstate").PromiseSnapshot<"I" | "J" | "K", { | ||
image: MultiscaleSpatialImage; | ||
scale: number; | ||
}>, { | ||
@@ -136,3 +140,3 @@ [k: string]: unknown; | ||
slice: number; | ||
axis: Axis; | ||
axis: AxisType; | ||
}>; | ||
@@ -142,4 +146,5 @@ id: string | undefined; | ||
src: "findDefaultAxis"; | ||
logic: import("xstate").PromiseActorLogic<"x" | "y" | "z", { | ||
logic: import("xstate").PromiseActorLogic<"I" | "J" | "K", { | ||
image: MultiscaleSpatialImage; | ||
scale: number; | ||
}>; | ||
@@ -165,2 +170,5 @@ id: string | undefined; | ||
} | { | ||
type: 'setAxis'; | ||
axis: AxisType; | ||
} | { | ||
type: 'setScale'; | ||
@@ -220,3 +228,3 @@ scale: number; | ||
slice: number; | ||
axis: Axis; | ||
axis: AxisType; | ||
}>; | ||
@@ -226,4 +234,5 @@ id: string | undefined; | ||
src: "findDefaultAxis"; | ||
logic: import("xstate").PromiseActorLogic<"x" | "y" | "z", { | ||
logic: import("xstate").PromiseActorLogic<"I" | "J" | "K", { | ||
image: MultiscaleSpatialImage; | ||
scale: number; | ||
}>; | ||
@@ -230,0 +239,0 @@ id: string | undefined; |
@@ -1,17 +0,26 @@ | ||
import { assign, enqueueActions, fromPromise, setup, } from 'xstate'; | ||
import { assign, enqueueActions, fromPromise, setup, stateIn, } from 'xstate'; | ||
import { ensure3dDirection, } from '@itk-viewer/io/MultiscaleSpatialImage.js'; | ||
import { reset2d } from './camera.js'; | ||
import { quat, vec3 } from 'gl-matrix'; | ||
export const AXIS = { | ||
X: 'x', | ||
Y: 'y', | ||
Z: 'z', | ||
import { mat3, mat4, quat, vec3 } from 'gl-matrix'; | ||
import { XYZ, ensuredDims } from '@itk-viewer/io/dimensionUtils.js'; | ||
import { getCorners } from '@itk-viewer/utils/bounding-box.js'; | ||
export const Axis = { | ||
I: 'I', | ||
J: 'J', | ||
K: 'K', | ||
}; | ||
const axisToIndex = { | ||
x: 0, | ||
y: 1, | ||
z: 2, | ||
I: 0, | ||
J: 1, | ||
K: 2, | ||
}; | ||
// To MultiScaleImage dimension | ||
const axisToDim = { | ||
I: 'x', | ||
J: 'y', | ||
K: 'z', | ||
}; | ||
const viewContext = { | ||
slice: 0.5, | ||
axis: AXIS.Z, | ||
axis: Axis.K, | ||
scale: 0, | ||
@@ -23,28 +32,36 @@ image: undefined, | ||
}; | ||
const toRotation = (axis) => { | ||
// Default to z axis where +Z goes into screen and +Y is down on screen | ||
let vec = vec3.fromValues(1, 0, 0); | ||
let angle = Math.PI; | ||
if (axis == 'x') { | ||
vec = vec3.fromValues(0, 1, 0); | ||
angle = Math.PI / 2; | ||
const toRotation = (direction, axis) => { | ||
const direction3d = ensure3dDirection(direction); | ||
// ITK (and VTKMath) uses row-major index axis, but gl-matrix uses column-major. Transpose. | ||
mat3.transpose(direction3d, direction3d); | ||
const rotation = quat.create(); | ||
if (axis == Axis.I) { | ||
quat.fromEuler(rotation, 0, 90, 0); | ||
const roll = quat.fromEuler(quat.create(), 0, 0, 90); | ||
quat.multiply(rotation, rotation, roll); | ||
} | ||
else if (axis == 'y') { | ||
angle = Math.PI / 2; | ||
else if (axis == Axis.J) { | ||
quat.fromEuler(rotation, 90, 0, 0); | ||
} | ||
const rotation = quat.create(); | ||
quat.setAxisAngle(rotation, vec, angle); | ||
else { | ||
quat.fromEuler(rotation, 0, 0, 180); | ||
} | ||
const sliceAxisRotation = mat3.fromQuat(mat3.create(), rotation); | ||
mat3.multiply(direction3d, direction3d, sliceAxisRotation); | ||
quat.fromMat3(rotation, direction3d); | ||
quat.normalize(rotation, rotation); | ||
return rotation; | ||
}; | ||
const computeMinSizeAxis = (bounds) => { | ||
const xSize = Math.abs(bounds[1] - bounds[0]); | ||
const ySize = Math.abs(bounds[3] - bounds[2]); | ||
const zSize = Math.abs(bounds[5] - bounds[4]); | ||
if (xSize < ySize && xSize < zSize) { | ||
return AXIS.X; | ||
const computeMinSizeAxis = (spacing, size) => { | ||
const imageSpaceSize = size.map((s, i) => s * spacing[i]); | ||
const iSize = imageSpaceSize[0]; | ||
const jSize = imageSpaceSize[1]; | ||
const kSize = imageSpaceSize[2]; | ||
if (iSize < jSize && iSize < kSize) { | ||
return Axis.I; | ||
} | ||
if (ySize < xSize && ySize < zSize) { | ||
return AXIS.Y; | ||
if (jSize < iSize && jSize < kSize) { | ||
return Axis.J; | ||
} | ||
return AXIS.Z; | ||
return Axis.K; | ||
}; | ||
@@ -55,23 +72,16 @@ export const view2d = setup({ | ||
imageBuilder: fromPromise(async ({ input: { image, scale, slice, axis }, }) => { | ||
const worldBounds = await image.getWorldBounds(scale); | ||
let sliceWorldPos = 0; | ||
if (axis === 'x') { | ||
const xWidth = worldBounds[1] - worldBounds[0]; | ||
sliceWorldPos = worldBounds[0] + xWidth * slice; // world X pos | ||
worldBounds[0] = sliceWorldPos; | ||
worldBounds[1] = sliceWorldPos; | ||
const normalizedImageBounds = [0, 1, 0, 1, 0, 1]; | ||
if (axis === Axis.I) { | ||
normalizedImageBounds[0] = slice; | ||
normalizedImageBounds[1] = slice; | ||
} | ||
else if (axis === 'y') { | ||
const yWidth = worldBounds[3] - worldBounds[2]; | ||
sliceWorldPos = worldBounds[2] + yWidth * slice; | ||
worldBounds[2] = sliceWorldPos; | ||
worldBounds[3] = sliceWorldPos; | ||
else if (axis === Axis.J) { | ||
normalizedImageBounds[2] = slice; | ||
normalizedImageBounds[3] = slice; | ||
} | ||
else if (axis === 'z') { | ||
const zWidth = worldBounds[5] - worldBounds[4]; | ||
sliceWorldPos = worldBounds[4] + zWidth * slice; | ||
worldBounds[4] = sliceWorldPos; | ||
worldBounds[5] = sliceWorldPos; | ||
else if (axis === Axis.K) { | ||
normalizedImageBounds[4] = slice; | ||
normalizedImageBounds[5] = slice; | ||
} | ||
const builtImage = (await image.getImage(scale, worldBounds)); | ||
const builtImage = (await image.getImageInImageSpace(scale, normalizedImageBounds)); | ||
if (builtImage.imageType.dimension === 2) { | ||
@@ -82,14 +92,34 @@ return { builtImage, sliceIndex: 0 }; | ||
// find index of slice in builtImage | ||
const axisIndex = axisToIndex[axis]; | ||
const builtWidthWorld = builtImage.spacing[axisIndex] * builtImage.size[axisIndex]; | ||
const sliceInBuildImageWorld = sliceWorldPos - builtImage.origin[axisIndex]; | ||
const sliceIndexFloat = Math.round(builtImage.size[axisIndex] * | ||
(sliceInBuildImageWorld / builtWidthWorld)); | ||
// Math.round goes up with .5, so we need to clamp to max index | ||
const sliceIndex = Math.max(0, Math.min(sliceIndexFloat, builtImage.size[axisIndex] - 1)); | ||
const indexToWorld = await image.scaleIndexToWorld(scale); | ||
const worldToIndex = mat4.invert(mat4.create(), indexToWorld); | ||
const wholeImageOrigin = [...(await image.scaleOrigin(scale))]; | ||
if (wholeImageOrigin.length == 2) { | ||
wholeImageOrigin[2] = 0; | ||
} | ||
vec3.transformMat4(wholeImageOrigin, wholeImageOrigin, worldToIndex); | ||
const buildImageOrigin = [...builtImage.origin]; | ||
if (buildImageOrigin.length == 2) { | ||
buildImageOrigin[2] = 0; | ||
} | ||
vec3.transformMat4(buildImageOrigin, buildImageOrigin, worldToIndex); | ||
// vector from whole image origin to build image origin | ||
const wholeImageToBuildImageOrigin = vec3.subtract(buildImageOrigin, buildImageOrigin, wholeImageOrigin); | ||
const builtOriginIndex = wholeImageToBuildImageOrigin[axisToIndex[axis]]; | ||
const axisIndexSize = image.scaleInfos[scale].arrayShape.get(axisToDim[axis]) ?? 1; | ||
const fullImageSliceIndex = slice * axisIndexSize; | ||
const sliceIndexInBuildImageFloat = fullImageSliceIndex - builtOriginIndex; | ||
const sliceIndexFloat = Math.round(sliceIndexInBuildImageFloat); | ||
// Math.round goes up with .5, so clamp to max index | ||
const sliceIndex = Math.max(0, Math.min(sliceIndexFloat, builtImage.size[axisToIndex[axis]] - 1)); | ||
return { builtImage, sliceIndex }; | ||
}), | ||
findDefaultAxis: fromPromise(async ({ input: { image }, }) => { | ||
const worldBounds = await image.getWorldBounds(image.coarsestScale); | ||
return computeMinSizeAxis(worldBounds); | ||
findDefaultAxis: fromPromise(async ({ input: { image, scale }, }) => { | ||
const ijkSpacing = await image.scaleSpacing(scale); | ||
if (ijkSpacing.length > 3) { | ||
ijkSpacing.push(0); | ||
} | ||
const shape = image.scaleInfos[scale].arrayShape; | ||
const shape3d = ensuredDims(0, ['x', 'y', 'z'], shape); | ||
const shapeArray = XYZ.map((axis) => shape3d.get(axis)); | ||
return computeMinSizeAxis(ijkSpacing, shapeArray); | ||
}), | ||
@@ -103,3 +133,3 @@ }, | ||
}, | ||
resetCameraPose: async ({ context: { image, camera, viewport, axis } }) => { | ||
resetCameraPose: async ({ context: { image, camera, viewport, axis, scale }, }) => { | ||
if (!image || !camera) | ||
@@ -113,8 +143,18 @@ return; | ||
})(); | ||
const bounds = await image.getWorldBounds(image.coarsestScale); | ||
const { pose: currentPose, verticalFieldOfView } = camera.getSnapshot().context; | ||
const withAxis = { ...currentPose }; | ||
withAxis.rotation = toRotation(axis); | ||
const pose = reset2d(withAxis, verticalFieldOfView, bounds, aspect); | ||
withAxis.rotation = toRotation(image.direction, axis); | ||
const indexToWorld = await image.scaleIndexToWorld(scale); | ||
const indexBounds = image.getIndexExtent(scale); | ||
const corners = getCorners(indexBounds); | ||
// to world space | ||
const pointsToFit = corners.map((corner) => { | ||
return vec3.transformMat4(corner, corner, indexToWorld); | ||
}); | ||
const pose = reset2d(withAxis, verticalFieldOfView, pointsToFit, aspect); | ||
camera.send({ | ||
type: 'setEnableRotation', | ||
enable: true, | ||
}); | ||
camera.send({ | ||
type: 'setPose', | ||
@@ -175,10 +215,34 @@ pose, | ||
}, | ||
setSlice: { | ||
actions: [assign({ slice: ({ event }) => event.slice })], | ||
target: '.buildingImage', | ||
}, | ||
setScale: { | ||
actions: [assign({ scale: ({ event }) => event.scale })], | ||
target: '.buildingImage', | ||
}, | ||
setSlice: [ | ||
// if buildingImage, rebuild image | ||
{ | ||
guard: stateIn('view2d.buildingImage'), | ||
target: '.buildingImage', | ||
actions: [assign({ slice: ({ event }) => event.slice })], | ||
}, | ||
// else eventually going to buildingImage | ||
{ | ||
actions: [assign({ slice: ({ event }) => event.slice })], | ||
}, | ||
], | ||
setAxis: [ | ||
// if buildingImage, rebuild image | ||
{ | ||
guard: stateIn('view2d.buildingImage'), | ||
target: '.buildingImage', | ||
actions: [ | ||
assign({ axis: ({ event }) => event.axis }), | ||
'forwardToSpawned', | ||
'resetCameraPose', | ||
], | ||
}, | ||
// else eventually going to buildingImage | ||
{ | ||
actions: [ | ||
assign({ axis: ({ event }) => event.axis }), | ||
'forwardToSpawned', | ||
'resetCameraPose', | ||
], | ||
}, | ||
], | ||
setViewport: { | ||
@@ -212,7 +276,19 @@ actions: [ | ||
states: { | ||
idle: {}, | ||
idle: { | ||
on: { | ||
setScale: { | ||
actions: [ | ||
assign({ | ||
scale: ({ event }) => { | ||
return event.scale; | ||
}, | ||
}), | ||
], | ||
}, | ||
}, | ||
}, | ||
findingNewImageDefaults: { | ||
invoke: { | ||
input: ({ context }) => { | ||
const { image } = context; | ||
const { image, scale } = context; | ||
if (!image) | ||
@@ -222,2 +298,3 @@ throw new Error('No image available'); | ||
image, | ||
scale, | ||
}; | ||
@@ -234,3 +311,3 @@ }, | ||
enqueue.sendTo(actor, { | ||
type: 'axis', | ||
type: 'setAxis', | ||
axis: context.axis, | ||
@@ -245,2 +322,15 @@ }); | ||
}, | ||
on: { | ||
setScale: { | ||
actions: [ | ||
assign({ | ||
scale: ({ event }) => { | ||
return event.scale; | ||
}, | ||
}), | ||
], | ||
target: '.', | ||
reenter: true, | ||
}, | ||
}, | ||
}, | ||
@@ -275,2 +365,15 @@ buildingImage: { | ||
}, | ||
on: { | ||
setScale: { | ||
actions: [ | ||
assign({ | ||
scale: ({ event }) => { | ||
return event.scale; | ||
}, | ||
}), | ||
], | ||
target: '.', | ||
reenter: true, | ||
}, | ||
}, | ||
}, | ||
@@ -277,0 +380,0 @@ }, |
@@ -42,5 +42,2 @@ /// <reference types="gl-matrix/index.js" /> | ||
params: unknown; | ||
} | { | ||
type: "resetCameraPose"; | ||
params: unknown; | ||
}, { | ||
@@ -62,5 +59,2 @@ type: string; | ||
params: unknown; | ||
} | { | ||
type: "resetCameraPose"; | ||
params: unknown; | ||
}, { | ||
@@ -67,0 +61,0 @@ type: string; |
import { assign, sendParent, setup } from 'xstate'; | ||
import { cameraMachine, reset3d } from './camera.js'; | ||
import { cameraMachine } from './camera.js'; | ||
export const viewportMachine = setup({ | ||
@@ -14,17 +14,2 @@ types: {}, | ||
}, | ||
resetCameraPose: async ({ context: { image, resolution: dims }, self }) => { | ||
const { camera } = self.getSnapshot().children; | ||
if (!image || !camera) | ||
return; | ||
const aspect = (() => { | ||
return dims[1] && dims[0] ? dims[0] / dims[1] : 1; | ||
})(); | ||
const bounds = await image.getWorldBounds(image.coarsestScale); | ||
const { pose: currentPose, verticalFieldOfView } = camera.getSnapshot().context; | ||
const pose = reset3d(currentPose, verticalFieldOfView, bounds, aspect); | ||
camera.send({ | ||
type: 'setPose', | ||
pose, | ||
}); | ||
}, | ||
}, | ||
@@ -65,3 +50,2 @@ }).createMachine({ | ||
}), | ||
'resetCameraPose', | ||
'forwardToSpawned', | ||
@@ -75,3 +59,2 @@ ], | ||
}), | ||
'resetCameraPose', | ||
'forwardToSpawned', | ||
@@ -78,0 +61,0 @@ ], |
{ | ||
"name": "@itk-viewer/viewer", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "Multi-dimensional web-based image, mesh, and point set viewer", | ||
@@ -46,3 +46,3 @@ "type": "module", | ||
"xstate": "5.5.2", | ||
"@itk-viewer/io": "^0.1.8", | ||
"@itk-viewer/io": "^0.2.0", | ||
"@itk-viewer/utils": "^0.1.3" | ||
@@ -49,0 +49,0 @@ }, |
@@ -5,3 +5,2 @@ import { | ||
createBounds, | ||
getCorners, | ||
getLength, | ||
@@ -213,20 +212,25 @@ } from '@itk-viewer/utils/bounding-box.js'; | ||
verticalFieldOfView: number, | ||
bounds: Bounds, | ||
pointsToFit: Array<ReadonlyVec3>, | ||
aspect: number, | ||
) => { | ||
const center = vec3.fromValues( | ||
(bounds[0] + bounds[1]) / 2.0, | ||
(bounds[2] + bounds[3]) / 2.0, | ||
(bounds[4] + bounds[5]) / 2.0, | ||
); | ||
const center = vec3.create(); | ||
for (let i = 0; i < pointsToFit.length; ++i) { | ||
vec3.add(center, center, pointsToFit[i]); | ||
} | ||
vec3.scale(center, center, 1.0 / pointsToFit.length); | ||
// Get the bounds in view coordinates | ||
const viewBounds = createBounds(); | ||
const visiblePoints = getCorners(bounds); | ||
const viewMat = mat4.create(); | ||
toMat4(viewMat, pose); | ||
for (let i = 0; i < visiblePoints.length; ++i) { | ||
const point = visiblePoints[i]; | ||
vec3.transformMat4(point, point, viewMat); | ||
addPoint(viewBounds, ...point); | ||
const viewSpacePoint = vec3.create(); | ||
for (let i = 0; i < pointsToFit.length; ++i) { | ||
const point = pointsToFit[i]; | ||
vec3.transformMat4(viewSpacePoint, point, viewMat); | ||
addPoint( | ||
viewBounds, | ||
viewSpacePoint[0], | ||
viewSpacePoint[1], | ||
viewSpacePoint[2], | ||
); | ||
} | ||
@@ -233,0 +237,0 @@ |
@@ -8,2 +8,3 @@ import { | ||
setup, | ||
stateIn, | ||
} from 'xstate'; | ||
@@ -13,27 +14,36 @@ import { | ||
BuiltImage, | ||
ensure3dDirection, | ||
} from '@itk-viewer/io/MultiscaleSpatialImage.js'; | ||
import { ValueOf } from '@itk-viewer/io/types.js'; | ||
import { ReadonlyBounds } from '@itk-viewer/utils/bounding-box.js'; | ||
import { CreateChild } from './children.js'; | ||
import { Camera, reset2d } from './camera.js'; | ||
import { ViewportActor } from './viewport.js'; | ||
import { quat, vec3 } from 'gl-matrix'; | ||
import { mat3, mat4, quat, vec3 } from 'gl-matrix'; | ||
import { XYZ, ensuredDims } from '@itk-viewer/io/dimensionUtils.js'; | ||
import { Bounds, getCorners } from '@itk-viewer/utils/bounding-box.js'; | ||
export const AXIS = { | ||
X: 'x', | ||
Y: 'y', | ||
Z: 'z', | ||
export const Axis = { | ||
I: 'I', | ||
J: 'J', | ||
K: 'K', | ||
} as const; | ||
export type Axis = ValueOf<typeof AXIS>; | ||
export type AxisType = ValueOf<typeof Axis>; | ||
const axisToIndex = { | ||
x: 0, | ||
y: 1, | ||
z: 2, | ||
I: 0, | ||
J: 1, | ||
K: 2, | ||
} as const; | ||
// To MultiScaleImage dimension | ||
const axisToDim = { | ||
I: 'x', | ||
J: 'y', | ||
K: 'z', | ||
} as const; | ||
const viewContext = { | ||
slice: 0.5, | ||
axis: AXIS.Z as Axis, | ||
axis: Axis.K as AxisType, | ||
scale: 0, | ||
@@ -46,28 +56,38 @@ image: undefined as MultiscaleSpatialImage | undefined, | ||
const toRotation = (axis: Axis) => { | ||
// Default to z axis where +Z goes into screen and +Y is down on screen | ||
let vec = vec3.fromValues(1, 0, 0); | ||
let angle = Math.PI; | ||
if (axis == 'x') { | ||
vec = vec3.fromValues(0, 1, 0); | ||
angle = Math.PI / 2; | ||
} else if (axis == 'y') { | ||
angle = Math.PI / 2; | ||
const toRotation = (direction: Float64Array, axis: AxisType) => { | ||
const direction3d = ensure3dDirection(direction); | ||
// ITK (and VTKMath) uses row-major index axis, but gl-matrix uses column-major. Transpose. | ||
mat3.transpose(direction3d, direction3d); | ||
const rotation = quat.create(); | ||
if (axis == Axis.I) { | ||
quat.fromEuler(rotation, 0, 90, 0); | ||
const roll = quat.fromEuler(quat.create(), 0, 0, 90); | ||
quat.multiply(rotation, rotation, roll); | ||
} else if (axis == Axis.J) { | ||
quat.fromEuler(rotation, 90, 0, 0); | ||
} else { | ||
quat.fromEuler(rotation, 0, 0, 180); | ||
} | ||
const rotation = quat.create(); | ||
quat.setAxisAngle(rotation, vec, angle); | ||
const sliceAxisRotation = mat3.fromQuat(mat3.create(), rotation); | ||
mat3.multiply(direction3d, direction3d, sliceAxisRotation); | ||
quat.fromMat3(rotation, direction3d); | ||
quat.normalize(rotation, rotation); | ||
return rotation; | ||
}; | ||
const computeMinSizeAxis = (bounds: ReadonlyBounds) => { | ||
const xSize = Math.abs(bounds[1] - bounds[0]); | ||
const ySize = Math.abs(bounds[3] - bounds[2]); | ||
const zSize = Math.abs(bounds[5] - bounds[4]); | ||
if (xSize < ySize && xSize < zSize) { | ||
return AXIS.X; | ||
const computeMinSizeAxis = (spacing: Array<number>, size: Array<number>) => { | ||
const imageSpaceSize = size.map((s, i) => s * spacing[i]); | ||
const iSize = imageSpaceSize[0]; | ||
const jSize = imageSpaceSize[1]; | ||
const kSize = imageSpaceSize[2]; | ||
if (iSize < jSize && iSize < kSize) { | ||
return Axis.I; | ||
} | ||
if (ySize < xSize && ySize < zSize) { | ||
return AXIS.Y; | ||
if (jSize < iSize && jSize < kSize) { | ||
return Axis.J; | ||
} | ||
return AXIS.Z; | ||
return Axis.K; | ||
}; | ||
@@ -81,2 +101,3 @@ | ||
| { type: 'setSlice'; slice: number } | ||
| { type: 'setAxis'; axis: AxisType } | ||
| { type: 'setScale'; scale: number } | ||
@@ -96,27 +117,20 @@ | { type: 'setViewport'; viewport: ViewportActor } | ||
scale: number; | ||
slice: number; | ||
axis: Axis; | ||
slice: number; // 0 to 1 for depth on slice axis | ||
axis: AxisType; | ||
}; | ||
}) => { | ||
const worldBounds = await image.getWorldBounds(scale); | ||
let sliceWorldPos = 0; | ||
if (axis === 'x') { | ||
const xWidth = worldBounds[1] - worldBounds[0]; | ||
sliceWorldPos = worldBounds[0] + xWidth * slice; // world X pos | ||
worldBounds[0] = sliceWorldPos; | ||
worldBounds[1] = sliceWorldPos; | ||
} else if (axis === 'y') { | ||
const yWidth = worldBounds[3] - worldBounds[2]; | ||
sliceWorldPos = worldBounds[2] + yWidth * slice; | ||
worldBounds[2] = sliceWorldPos; | ||
worldBounds[3] = sliceWorldPos; | ||
} else if (axis === 'z') { | ||
const zWidth = worldBounds[5] - worldBounds[4]; | ||
sliceWorldPos = worldBounds[4] + zWidth * slice; | ||
worldBounds[4] = sliceWorldPos; | ||
worldBounds[5] = sliceWorldPos; | ||
const normalizedImageBounds = [0, 1, 0, 1, 0, 1] as Bounds; | ||
if (axis === Axis.I) { | ||
normalizedImageBounds[0] = slice; | ||
normalizedImageBounds[1] = slice; | ||
} else if (axis === Axis.J) { | ||
normalizedImageBounds[2] = slice; | ||
normalizedImageBounds[3] = slice; | ||
} else if (axis === Axis.K) { | ||
normalizedImageBounds[4] = slice; | ||
normalizedImageBounds[5] = slice; | ||
} | ||
const builtImage = (await image.getImage( | ||
const builtImage = (await image.getImageInImageSpace( | ||
scale, | ||
worldBounds, | ||
normalizedImageBounds, | ||
)) as BuiltImage; | ||
@@ -127,17 +141,38 @@ | ||
} | ||
// buildImage could be larger than slice if cached so | ||
// find index of slice in builtImage | ||
const axisIndex = axisToIndex[axis]; | ||
const builtWidthWorld = | ||
builtImage.spacing[axisIndex] * builtImage.size[axisIndex]; | ||
const sliceInBuildImageWorld = | ||
sliceWorldPos - builtImage.origin[axisIndex]; | ||
const sliceIndexFloat = Math.round( | ||
builtImage.size[axisIndex] * | ||
(sliceInBuildImageWorld / builtWidthWorld), | ||
const indexToWorld = await image.scaleIndexToWorld(scale); | ||
const worldToIndex = mat4.invert(mat4.create(), indexToWorld); | ||
const wholeImageOrigin = [...(await image.scaleOrigin(scale))] as vec3; | ||
if (wholeImageOrigin.length == 2) { | ||
wholeImageOrigin[2] = 0; | ||
} | ||
vec3.transformMat4(wholeImageOrigin, wholeImageOrigin, worldToIndex); | ||
const buildImageOrigin = [...builtImage.origin] as vec3; | ||
if (buildImageOrigin.length == 2) { | ||
buildImageOrigin[2] = 0; | ||
} | ||
vec3.transformMat4(buildImageOrigin, buildImageOrigin, worldToIndex); | ||
// vector from whole image origin to build image origin | ||
const wholeImageToBuildImageOrigin = vec3.subtract( | ||
buildImageOrigin, | ||
buildImageOrigin, | ||
wholeImageOrigin, | ||
); | ||
// Math.round goes up with .5, so we need to clamp to max index | ||
const builtOriginIndex = | ||
wholeImageToBuildImageOrigin[axisToIndex[axis]]; | ||
const axisIndexSize = | ||
image.scaleInfos[scale].arrayShape.get(axisToDim[axis]) ?? 1; | ||
const fullImageSliceIndex = slice * axisIndexSize; | ||
const sliceIndexInBuildImageFloat = | ||
fullImageSliceIndex - builtOriginIndex; | ||
const sliceIndexFloat = Math.round(sliceIndexInBuildImageFloat); | ||
// Math.round goes up with .5, so clamp to max index | ||
const sliceIndex = Math.max( | ||
0, | ||
Math.min(sliceIndexFloat, builtImage.size[axisIndex] - 1), | ||
Math.min(sliceIndexFloat, builtImage.size[axisToIndex[axis]] - 1), | ||
); | ||
@@ -149,10 +184,17 @@ return { builtImage, sliceIndex }; | ||
async ({ | ||
input: { image }, | ||
input: { image, scale }, | ||
}: { | ||
input: { | ||
image: MultiscaleSpatialImage; | ||
scale: number; | ||
}; | ||
}) => { | ||
const worldBounds = await image.getWorldBounds(image.coarsestScale); | ||
return computeMinSizeAxis(worldBounds); | ||
const ijkSpacing = await image.scaleSpacing(scale); | ||
if (ijkSpacing.length > 3) { | ||
ijkSpacing.push(0); | ||
} | ||
const shape = image.scaleInfos[scale].arrayShape; | ||
const shape3d = ensuredDims(0, ['x', 'y', 'z'], shape); | ||
const shapeArray = XYZ.map((axis) => shape3d.get(axis)!); | ||
return computeMinSizeAxis(ijkSpacing, shapeArray); | ||
}, | ||
@@ -167,3 +209,5 @@ ), | ||
}, | ||
resetCameraPose: async ({ context: { image, camera, viewport, axis } }) => { | ||
resetCameraPose: async ({ | ||
context: { image, camera, viewport, axis, scale }, | ||
}) => { | ||
if (!image || !camera) return; | ||
@@ -176,12 +220,23 @@ const aspect = (() => { | ||
const bounds = await image.getWorldBounds(image.coarsestScale); | ||
const { pose: currentPose, verticalFieldOfView } = | ||
camera.getSnapshot().context; | ||
const withAxis = { ...currentPose }; | ||
withAxis.rotation = toRotation(image.direction, axis); | ||
withAxis.rotation = toRotation(axis); | ||
const indexToWorld = await image.scaleIndexToWorld(scale); | ||
const indexBounds = image.getIndexExtent(scale); | ||
const corners = getCorners(indexBounds); | ||
const pose = reset2d(withAxis, verticalFieldOfView, bounds, aspect); | ||
// to world space | ||
const pointsToFit = corners.map((corner) => { | ||
return vec3.transformMat4(corner, corner, indexToWorld); | ||
}); | ||
const pose = reset2d(withAxis, verticalFieldOfView, pointsToFit, aspect); | ||
camera.send({ | ||
type: 'setEnableRotation', | ||
enable: true, | ||
}); | ||
camera.send({ | ||
type: 'setPose', | ||
@@ -246,10 +301,34 @@ pose, | ||
}, | ||
setSlice: { | ||
actions: [assign({ slice: ({ event }) => event.slice })], | ||
target: '.buildingImage', | ||
}, | ||
setScale: { | ||
actions: [assign({ scale: ({ event }) => event.scale })], | ||
target: '.buildingImage', | ||
}, | ||
setSlice: [ | ||
// if buildingImage, rebuild image | ||
{ | ||
guard: stateIn('view2d.buildingImage'), | ||
target: '.buildingImage', | ||
actions: [assign({ slice: ({ event }) => event.slice })], | ||
}, | ||
// else eventually going to buildingImage | ||
{ | ||
actions: [assign({ slice: ({ event }) => event.slice })], | ||
}, | ||
], | ||
setAxis: [ | ||
// if buildingImage, rebuild image | ||
{ | ||
guard: stateIn('view2d.buildingImage'), | ||
target: '.buildingImage', | ||
actions: [ | ||
assign({ axis: ({ event }) => event.axis }), | ||
'forwardToSpawned', | ||
'resetCameraPose', | ||
], | ||
}, | ||
// else eventually going to buildingImage | ||
{ | ||
actions: [ | ||
assign({ axis: ({ event }) => event.axis }), | ||
'forwardToSpawned', | ||
'resetCameraPose', | ||
], | ||
}, | ||
], | ||
setViewport: { | ||
@@ -282,10 +361,23 @@ actions: [ | ||
states: { | ||
idle: {}, | ||
idle: { | ||
on: { | ||
setScale: { | ||
actions: [ | ||
assign({ | ||
scale: ({ event }) => { | ||
return event.scale; | ||
}, | ||
}), | ||
], | ||
}, | ||
}, | ||
}, | ||
findingNewImageDefaults: { | ||
invoke: { | ||
input: ({ context }) => { | ||
const { image } = context; | ||
const { image, scale } = context; | ||
if (!image) throw new Error('No image available'); | ||
return { | ||
image, | ||
scale, | ||
}; | ||
@@ -302,3 +394,3 @@ }, | ||
enqueue.sendTo(actor, { | ||
type: 'axis', | ||
type: 'setAxis', | ||
axis: context.axis, | ||
@@ -313,2 +405,15 @@ }); | ||
}, | ||
on: { | ||
setScale: { | ||
actions: [ | ||
assign({ | ||
scale: ({ event }) => { | ||
return event.scale; | ||
}, | ||
}), | ||
], | ||
target: '.', | ||
reenter: true, | ||
}, | ||
}, | ||
}, | ||
@@ -342,2 +447,15 @@ buildingImage: { | ||
}, | ||
on: { | ||
setScale: { | ||
actions: [ | ||
assign({ | ||
scale: ({ event }) => { | ||
return event.scale; | ||
}, | ||
}), | ||
], | ||
target: '.', | ||
reenter: true, | ||
}, | ||
}, | ||
}, | ||
@@ -344,0 +462,0 @@ }, |
@@ -5,3 +5,3 @@ import { Actor, AnyActorRef, assign, sendParent, setup } from 'xstate'; | ||
import { MultiscaleSpatialImage } from '@itk-viewer/io/MultiscaleSpatialImage.js'; | ||
import { cameraMachine, Camera, reset3d } from './camera.js'; | ||
import { cameraMachine, Camera } from './camera.js'; | ||
import { CreateChild } from './children.js'; | ||
@@ -52,18 +52,2 @@ | ||
}, | ||
resetCameraPose: async ({ context: { image, resolution: dims }, self }) => { | ||
const { camera } = self.getSnapshot().children; | ||
if (!image || !camera) return; | ||
const aspect = (() => { | ||
return dims[1] && dims[0] ? dims[0] / dims[1] : 1; | ||
})(); | ||
const bounds = await image.getWorldBounds(image.coarsestScale); | ||
const { pose: currentPose, verticalFieldOfView } = | ||
camera.getSnapshot().context; | ||
const pose = reset3d(currentPose, verticalFieldOfView, bounds, aspect); | ||
camera.send({ | ||
type: 'setPose', | ||
pose, | ||
}); | ||
}, | ||
}, | ||
@@ -109,3 +93,2 @@ }).createMachine({ | ||
}), | ||
'resetCameraPose', | ||
'forwardToSpawned', | ||
@@ -120,3 +103,2 @@ ], | ||
}), | ||
'resetCameraPose', | ||
'forwardToSpawned', | ||
@@ -123,0 +105,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
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
169201
2866
+ Added@itk-viewer/io@0.2.0(transitive)
- Removed@itk-viewer/io@0.1.8(transitive)
Updated@itk-viewer/io@^0.2.0