Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | 137x 1074x 1074x 194x 1489x 1489x 1683x 1074x 5403770x 1049475x 1049475x 1049475x 1049475x 1049475x | import { vec3 } from 'gl-matrix';
import type { Point3 } from '../../types';
/**
* Relative tolerance used when testing whether something lies within a slab.
*
* The slab tests below are strict (`<`, not `<=`), because a one-voxel-thick
* annotation places the neighbouring voxel centres *exactly* on the boundary
* and must exclude them. Relative to the voxel thickness, because spacings range from
* microns to centimetres.
*
* See `docs/docs/concepts/cornerstone-tools/annotation/voxel-statistics.md`.
*/
export const SLAB_RELATIVE_EPSILON = 1e-5;
/**
* The tolerance to use for slab tests against a grid with the given voxel
* thickness along the normal.
*
* @param voxelThickness - The voxel thickness along the normal. See
* `getVoxelThicknessAlongNormal`.
*/
export function getSlabEpsilon(voxelThickness: number): number {
return Math.abs(voxelThickness) * SLAB_RELATIVE_EPSILON;
}
/**
* Resolves the reference plane thickness to use, in mm.
*
* Null, undefined, and 0 or less all count as "not recorded" and default to one
* voxel along the normal. See
* `docs/docs/concepts/cornerstone-tools/annotation/voxel-statistics.md`.
*
* @param referencePlaneThickness - The recorded thickness, or null/undefined/0.
* @param voxelThickness - The voxel thickness along the normal, the fallback.
*/
export function resolveReferencePlaneThickness(
referencePlaneThickness: number | null | undefined,
voxelThickness: number
): number {
return Number.isFinite(referencePlaneThickness) &&
(referencePlaneThickness as number) > 0
? (referencePlaneThickness as number)
: voxelThickness;
}
/**
* The half width used to decide which *voxels* an area annotation contains
* (Rule M): `(referencePlaneThickness + voxelThickness) / 2`.
*
* The voxel term widens the slab by half a voxel each side, so a plane exactly
* midway between two voxel centres selects **both** layers. The viewport's own
* slab thickness is deliberately absent.
*
* See `docs/docs/concepts/cornerstone-tools/annotation/voxel-statistics.md`.
*
* @param referencePlaneThickness - The reference plane thickness, resolved.
* @param voxelThickness - The voxel thickness along the normal.
*/
export function getMembershipHalfWidth(
referencePlaneThickness: number,
voxelThickness: number
): number {
return (referencePlaneThickness + voxelThickness) / 2;
}
/**
* The half width used to decide which voxels a *brush fill* writes (Rule F):
* `max(fillDepth, voxelThickness) / 2`.
*
* Rule M and Rule F answer different questions, and a fill must not use Rule M.
* Rule M measures, so it widens the slab by half a voxel on each side and a
* plane midway between two voxel layers reports both layers. That is right for
* an area, because an area must not under-count. It is wrong for a brush: the
* user drew one layer, and the brush would write two.
*
* Rule F therefore takes only the voxels that the fill volume passes through.
* A fill depth of one voxel or less gives `voxelThickness / 2`, which selects
* exactly the voxels whose box the fill plane crosses, and no fill is ever
* thinner than one voxel. A full-thickness view gives `fillDepth / 2`, so the
* fill writes exactly the depth the viewport shows.
*
* See `docs/docs/concepts/cornerstone-tools/annotation/voxel-statistics.md`.
*
* @param fillDepth - The depth the fill covers along the normal, in mm. Zero,
* negative, null and undefined all mean "one voxel".
* @param voxelThickness - The voxel thickness along the normal.
*/
export function getFillHalfWidth(
fillDepth: number | null | undefined,
voxelThickness: number
): number {
const depth =
Number.isFinite(fillDepth) && (fillDepth as number) > 0
? (fillDepth as number)
: 0;
return Math.max(depth, Math.abs(voxelThickness)) / 2;
}
/**
* Which voxels along the normal a slab of a given thickness selects.
*
* - `'overlapping'` - a voxel qualifies when its own box overlaps the slab.
* The slab is therefore widened by half a voxel on each side, and a plane
* halfway between two voxel layers selects both layers. A slab of this kind
* never under-counts, and two consecutive slabs share the voxels on the
* boundary between them.
* - `'centerInside'` - a voxel qualifies when its centre lies inside the slab,
* with no widening, and the slab is never thinner than one voxel. Two
* consecutive slabs of this kind tile the normal: every voxel belongs to
* exactly one of them, and to one of them at least.
*/
export type SlabDepthCoverage = 'overlapping' | 'centerInside';
/**
* The half width along the normal for the given coverage, in mm.
*
* Both rules read the same resolved thickness, so a caller passes one
* thickness and picks the rule with {@link SlabDepthCoverage}. A caller never
* computes a half width of its own.
*
* @param thickness - The resolved slab thickness along the normal, in mm. See
* `resolveReferencePlaneThickness`.
* @param voxelThickness - The voxel thickness along the normal.
* @param coverage - Which voxels the slab selects. Defaults to
* `'overlapping'`.
*/
export function getSlabHalfWidth(
thickness: number,
voxelThickness: number,
coverage: SlabDepthCoverage = 'overlapping'
): number {
return coverage === 'centerInside'
? getFillHalfWidth(thickness, voxelThickness)
: getMembershipHalfWidth(thickness, voxelThickness);
}
/**
* Whether the low end of the depth interval is inclusive for the given
* coverage.
*
* `'centerInside'` needs the half-open interval. An open interval drops both
* of its endpoints, so a voxel centre that lands exactly on a boundary belongs
* to neither of two consecutive slabs, and the two stop tiling.
* `'overlapping'` keeps the open interval, because it selects the voxels on
* both sides of a boundary anyway.
*/
export function isSlabDepthLowInclusive(
coverage: SlabDepthCoverage = 'overlapping'
): boolean {
return coverage === 'centerInside';
}
/**
* The half width used to decide whether an annotation is *displayed* in a
* viewport (Rule D): `(viewportSlabThickness + referencePlaneThickness) / 2`.
*
* Unlike Rule M this uses the viewport slab thickness, because whether
* something is shown legitimately depends on how thick a slab is being viewed.
*
* @param viewportSlabThickness - The viewport slab thickness in mm.
* @param referencePlaneThickness - The reference plane thickness, resolved.
*/
export function getDisplayHalfWidth(
viewportSlabThickness: number,
referencePlaneThickness: number
): number {
return (viewportSlabThickness + referencePlaneThickness) / 2;
}
/**
* The signed distance from `point` to the plane through `planePoint` with the
* given normal, measured along that normal.
*/
export function signedDistanceToPlane(
point: Point3,
planePoint: Point3,
normal: Point3
): number {
return (
(point[0] - planePoint[0]) * normal[0] +
(point[1] - planePoint[1]) * normal[1] +
(point[2] - planePoint[2]) * normal[2]
);
}
/**
* Whether a signed distance falls within a slab of the given half width.
*
* Strict, and tightened by `epsilon` - see {@link SLAB_RELATIVE_EPSILON} for
* why both of those matter.
*/
export function isWithinSlab(
signedDistance: number,
halfWidth: number,
epsilon: number
): boolean {
return Math.abs(signedDistance) < halfWidth - epsilon;
}
/**
* Rule M, depth half: whether a voxel centre is close enough to the annotation
* plane to be included.
*
* This is only the depth test. A voxel is in an area annotation when this
* passes *and* its projection along the normal onto the annotation plane falls
* inside the annotation's 2D shape.
*
* @param voxelCenter - The voxel centre in world coordinates.
* @param planePoint - The annotation plane anchor.
* @param normal - The annotation's view plane normal. Unit length.
* @param referencePlaneThickness - The reference plane thickness, resolved.
* @param voxelThickness - The voxel thickness along the normal.
*/
export function isVoxelCenterInSlab(
voxelCenter: Point3,
planePoint: Point3,
normal: Point3,
referencePlaneThickness: number,
voxelThickness: number
): boolean {
return isWithinSlab(
signedDistanceToPlane(voxelCenter, planePoint, normal),
getMembershipHalfWidth(referencePlaneThickness, voxelThickness),
getSlabEpsilon(voxelThickness)
);
}
/**
* Projects a world point onto the plane through `planePoint` with the given
* normal, along that normal.
*
* @param point - The point to project.
* @param planePoint - A point on the target plane.
* @param normal - The plane normal. Unit length.
* @param out - Optional destination, to avoid allocating per voxel.
*/
export function projectPointOntoPlane(
point: Point3,
planePoint: Point3,
normal: Point3,
out: Point3 = [0, 0, 0]
): Point3 {
const distance = signedDistanceToPlane(point, planePoint, normal);
out[0] = point[0] - distance * normal[0];
out[1] = point[1] - distance * normal[1];
out[2] = point[2] - distance * normal[2];
return out;
}
/**
* Convenience wrapper returning the normalised form of a possibly unnormalised
* normal, without allocating when it is already unit length.
*/
export function asUnitNormal(normal: Point3): Point3 {
const lengthSquared =
normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2];
if (Math.abs(lengthSquared - 1) < Number.EPSILON * 8) {
return normal;
}
return vec3.normalize(vec3.create(), normal as vec3) as Point3;
}
|