fix: harden array builder editing workflows

This commit is contained in:
2026-08-28 23:10:41 +09:00
parent 3714146df9
commit 122b6cf7be
14 changed files with 1148 additions and 165 deletions
+80 -1
View File
@@ -2,6 +2,7 @@ import {
MAX_MICROPHONES,
addMicrophone,
arrayGeometryIssues,
arrayStatistics,
centerMicrophones,
clampPositionToAperture,
createArrayDefinition,
@@ -9,6 +10,7 @@ import {
generatePresetMicrophones,
inferApertureShape,
moveMicrophone,
nextMicrophoneId,
positionInsideAperture,
removeMicrophone,
replaceMicrophonesWithPreset,
@@ -110,6 +112,45 @@ test("seed random preset은 재현되고 최소 간격과 형상 경계를 지
}
});
test("1,024채널 random preset은 공간 index로 고유 좌표를 빠르게 생성한다", () => {
const startedAt = performance.now();
const microphones = generatePresetMicrophones({
type: "random",
aperture: { width: 0.45, height: 0.45 },
shape: "rectangle",
count: 1024,
seed: 20260828,
minimumSpacingM: 0.002,
});
const elapsedMs = performance.now() - startedAt;
assertEqual(microphones.length, 1024, "maximum random channel count");
assertEqual(
new Set(microphones.map((microphone) => microphone.position.join(","))).size,
1024,
"maximum random positions are unique",
);
assert(elapsedMs < 3000, "maximum random generation stays interactive: " + elapsedMs + " ms");
const failedAt = performance.now();
assertThrows(
() => generatePresetMicrophones({
type: "random",
aperture: { width: 0.45, height: 0.45 },
shape: "rectangle",
count: 1024,
seed: 7,
minimumSpacingM: 0.03,
}),
"배치할 수 없습니다",
"dense maximum random failure",
);
const failedElapsedMs = performance.now() - failedAt;
assert(
failedElapsedMs < 3000,
"impossible maximum random layout fails within a bound: " + failedElapsedMs + " ms",
);
});
test("불가능한 random 조건과 최대 채널 초과는 기존 배열을 보존한다", () => {
const arrayDefinition = createArrayDefinition();
const before = JSON.stringify(arrayDefinition.microphones);
@@ -185,7 +226,7 @@ test("좌표 clamp, 중심 정렬과 aperture 맞춤이 명시적으로 동작
assertNear(arrayDefinition.aperture.height, 0.021, 1e-12, "fitted height");
});
test("설명과 좌표에서 원형 생성 형상을 합리적으로 추론한다", () => {
test("문서의 명시적 형상 단서만으로 타원 생성 경계를 추론한다", () => {
const spiral = createArrayDefinition({
description: "Fermat spiral on circular aperture",
presetType: "spiral",
@@ -194,4 +235,42 @@ test("설명과 좌표에서 원형 생성 형상을 합리적으로 추론한
assertEqual(inferApertureShape(spiral), "ellipse", "description inference");
const grid = createArrayDefinition();
assertEqual(inferApertureShape(grid), "rectangle", "grid inference");
grid.description = "Spring-mounted rectangular array";
assertEqual(inferApertureShape(grid), "rectangle", "word boundary avoids spring false positive");
});
test("매우 큰 유한 좌표에서도 배열 중심 통계가 유한하게 유지된다", () => {
const arrayDefinition = createArrayDefinition({ columns: 2, rows: 1 });
arrayDefinition.microphones[0].position = [Number.MAX_VALUE, Number.MAX_VALUE, 0];
arrayDefinition.microphones[1].position = [Number.MAX_VALUE, -Number.MAX_VALUE, 0];
const statistics = arrayStatistics(arrayDefinition);
assert(Number.isFinite(statistics.centroid[0]), "large x centroid remains finite");
assert(Number.isFinite(statistics.centroid[1]), "large y centroid remains finite");
assertEqual(statistics.centroid[0], Number.MAX_VALUE, "large x centroid");
assertEqual(statistics.centroid[1], 0, "balanced y centroid");
});
test("큰 숫자 suffix ID도 마이크 추가를 멈추지 않고 정확히 증가시킨다", () => {
const arrayDefinition = createArrayDefinition({ columns: 1, rows: 1 });
arrayDefinition.microphones[0].id = "mic-9007199254740992";
assertEqual(
nextMicrophoneId(arrayDefinition),
"mic-9007199254740993",
"BigInt suffix increment",
);
assertEqual(
addMicrophone(arrayDefinition, [0.01, 0, 0]).id,
"mic-9007199254740993",
"manual add uses the safe ID",
);
});
test("고정밀 import 좌표는 실제 값이 다르면 중복으로 오인하지 않는다", () => {
const arrayDefinition = createArrayDefinition({ columns: 2, rows: 1 });
arrayDefinition.microphones[0].position = [0.0000000001, 0, 0];
arrayDefinition.microphones[1].position = [0.0000000002, 0, 0];
assert(
!arrayGeometryIssues(arrayDefinition).some((issue) => issue.severity === "error"),
"distinct high-precision positions",
);
});