Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions js/scanRotationParity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import fixture from "./.generated/engine/parity/scan_rotation_v1.json";
import {
SCAN_QUARTER_TURN_WGSL,
type ScanQuarterTurns,
scanQuarterTurnOutputShape,
scanQuarterTurnSourceIndex,
} from "./.generated/engine/geometry/compute/webgpu/quarter-turn";

describe("shared scan-rotation gold fixture", () => {
it("maps every detector pattern with the canonical row-column convention", () => {
const [scanRows, scanColumns, detectorRows, detectorColumns] = fixture.source.shape;
const detectorPixels = detectorRows * detectorColumns;

for (const testCase of fixture.cases) {
const quarterTurns = testCase.quarter_turns_counterclockwise as ScanQuarterTurns;
const [outputRows, outputColumns] = scanQuarterTurnOutputShape(
scanRows,
scanColumns,
quarterTurns,
);
expect([outputRows, outputColumns, detectorRows, detectorColumns]).toEqual(
testCase.output_shape,
);
for (let outputRow = 0; outputRow < outputRows; outputRow++) {
for (let outputColumn = 0; outputColumn < outputColumns; outputColumn++) {
const sourceScan = scanQuarterTurnSourceIndex(
outputRow,
outputColumn,
scanRows,
scanColumns,
quarterTurns,
);
const outputScan = outputRow * outputColumns + outputColumn;
const sourceStart = sourceScan * detectorPixels;
const outputStart = outputScan * detectorPixels;
expect(
testCase.expected_values.slice(outputStart, outputStart + detectorPixels),
).toEqual(
fixture.source.values.slice(sourceStart, sourceStart + detectorPixels),
);
}
}
}
});

it("keeps the same mapping in the hardware shader contract", () => {
expect(SCAN_QUARTER_TURN_WGSL).toContain("sourceRow = outputColumn");
expect(SCAN_QUARTER_TURN_WGSL).toContain(
"sourceRow = parameters.sourceRows - 1u - outputColumn",
);
expect(SCAN_QUARTER_TURN_WGSL).toContain(
"sourceScan * parameters.wordsPerScan + wordInScan",
);
});
});
55 changes: 55 additions & 0 deletions js/show4dstem/detectorInteraction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { clampDetectorCenter, resizeDetectorFromPointer } from "./detectorInteraction";

describe("Show4DSTEM detector interaction geometry", () => {
it("keeps subpixel detector centers while clamping to the diffraction plane", () => {
expect(clampDetectorCenter(12.25, 18.75, 48, 48)).toEqual({
row: 12.25,
col: 18.75,
});
expect(clampDetectorCenter(-2, 50, 48, 48)).toEqual({ row: 0, col: 47 });
});

it("resizes circle, square, and rectangle detectors from the live pointer", () => {
const common = {
centerRow: 10,
centerCol: 10,
pointerRow: 13,
pointerCol: 14,
radius: 8,
radiusInner: 3,
};

expect(resizeDetectorFromPointer({ ...common, mode: "circle" })).toEqual({ radius: 5 });
expect(resizeDetectorFromPointer({ ...common, mode: "square" })).toEqual({ radius: 4 });
expect(resizeDetectorFromPointer({ ...common, mode: "rect" })).toEqual({
width: 8,
height: 6,
});
expect(resizeDetectorFromPointer({
...common,
mode: "rect",
aspectRatio: 2,
preserveAspect: true,
})).toEqual({ width: 12, height: 6 });
});

it("keeps annular inner and outer radii ordered during live resizing", () => {
const common = {
mode: "annular" as const,
centerRow: 10,
centerCol: 10,
pointerRow: 10,
pointerCol: 12,
radius: 10,
radiusInner: 4,
};

expect(resizeDetectorFromPointer(common)).toEqual({ radius: 5 });
expect(resizeDetectorFromPointer({
...common,
pointerCol: 25,
resizeInner: true,
})).toEqual({ radiusInner: 9 });
});
});
74 changes: 74 additions & 0 deletions js/show4dstem/detectorInteraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export type DetectorRoiMode = "point" | "circle" | "square" | "rect" | "annular" | "off";

export type DetectorResizeGeometry = {
radius?: number;
radiusInner?: number;
width?: number;
height?: number;
};

export function clampDetectorCenter(
row: number,
col: number,
detectorRows: number,
detectorCols: number,
): { row: number; col: number } {
return {
row: Math.max(0, Math.min(detectorRows - 1, row)),
col: Math.max(0, Math.min(detectorCols - 1, col)),
};
}

export function resizeDetectorFromPointer({
mode,
centerRow,
centerCol,
pointerRow,
pointerCol,
radius,
radiusInner,
resizeInner = false,
aspectRatio = null,
preserveAspect = false,
}: {
mode: DetectorRoiMode;
centerRow: number;
centerCol: number;
pointerRow: number;
pointerCol: number;
radius: number;
radiusInner: number;
resizeInner?: boolean;
aspectRatio?: number | null;
preserveAspect?: boolean;
}): DetectorResizeGeometry | null {
const rowDistance = Math.abs(pointerRow - centerRow);
const colDistance = Math.abs(pointerCol - centerCol);

if (resizeInner && mode === "annular") {
return {
radiusInner: Math.max(1, Math.min(radius - 1, Math.hypot(rowDistance, colDistance))),
};
}

if (mode === "rect") {
let width = Math.max(2, colDistance * 2);
let height = Math.max(2, rowDistance * 2);
if (preserveAspect && aspectRatio != null) {
if (width / height > aspectRatio) height = Math.max(2, width / aspectRatio);
else width = Math.max(2, height * aspectRatio);
}
return { width, height };
}

if (mode === "circle" || mode === "square" || mode === "annular") {
const nextRadius = mode === "square"
? Math.max(rowDistance, colDistance)
: Math.hypot(rowDistance, colDistance);
return {
radius: Math.max(mode === "annular" ? radiusInner + 1 : 1, nextRadius),
};
}

return null;
}
Loading