fix: make circular and spiral presets truthful

This commit is contained in:
2026-08-30 11:18:51 +09:00
parent 9d937160eb
commit 51c7bbd859
8 changed files with 233 additions and 29 deletions
+48 -1
View File
@@ -572,6 +572,53 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
() => documentObject.documentElement.dataset.arrayValid === "true",
"ellipse ring validation",
);
assert(
documentObject.querySelector("#preset-hint").textContent.includes("실제 내접원"),
"ring hint describes a true circle instead of the boundary perimeter",
);
for (let index = 0; index < microphoneCount(documentObject); index += 1) {
microphoneButtons(documentObject)[index].click();
const [x, y] = selectedCoordinates(documentObject);
assertNear(Math.hypot(x, y), 0.215, 1e-8, "visible ring radius " + index);
}
dispatchValue(
windowObject,
documentObject.querySelector("#preset-type"),
"archimedean",
);
assert(
!documentObject.querySelector("#preset-turns").closest(".field").hidden,
"Archimedean preset exposes its turn count",
);
assert(
documentObject.querySelector("#preset-hint").textContent.includes("r∝θ"),
"Archimedean hint identifies the curved spiral law",
);
dispatchValue(windowObject, documentObject.querySelector("#preset-count"), 9);
dispatchValue(windowObject, documentObject.querySelector("#preset-turns"), 1.5);
documentObject.querySelector("#generate-preset-button").click();
await waitFor(() => microphoneCount(documentObject) === 9, "Archimedean generation");
microphoneButtons(documentObject)[0].click();
assertEqual(
selectedCoordinates(documentObject).join(","),
"0,0,0",
"Archimedean spiral visibly starts at the center",
);
microphoneButtons(documentObject).at(-1).click();
const archimedeanEnd = selectedCoordinates(documentObject);
assertNear(archimedeanEnd[0], -0.215, 1e-8, "1.5-turn spiral ends at -x");
assertNear(archimedeanEnd[1], 0, 1e-8, "1.5-turn spiral endpoint y");
dispatchValue(windowObject, documentObject.querySelector("#preset-type"), "spiral");
assert(
documentObject.querySelector("#preset-turns").closest(".field").hidden,
"Fermat preset does not claim an Archimedean turn count",
);
assert(
documentObject.querySelector("#preset-hint").textContent.includes("해바라기형"),
"Fermat hint explains why its points do not look like one connected curve",
);
dispatchValue(windowObject, documentObject.querySelector("#preset-type"), "random");
dispatchValue(windowObject, documentObject.querySelector("#preset-count"), 12);
@@ -585,7 +632,7 @@ test("Array Builder가 preset·직접 편집·검증·표준 JSON round trip을
assertEqual(repeatedRandom, firstRandom, "same seed repeats the visible layout");
documentObject.querySelector("#undo-button").click();
assertEqual(microphoneCount(documentObject), 8, "preset replacement is undoable");
assertEqual(microphoneCount(documentObject), 9, "preset replacement is undoable");
documentObject.querySelector("#redo-button").click();
assertEqual(microphoneCount(documentObject), 12, "preset replacement is redoable");
+110
View File
@@ -67,6 +67,116 @@ test("격자·선형·링·Fermat preset이 개수와 생성 경계를 지킨다
}
});
test("원형과 Fermat preset은 비정사각 경계에서도 실제 내접원 좌표를 만든다", () => {
const aperture = { width: 0.5, height: 0.3 };
const edgeInsetM = 0.02;
const expectedRadius = 0.13;
const count = 12;
for (const type of ["ring", "spiral"]) {
const rectangle = generatePresetMicrophones({
type,
aperture,
shape: "rectangle",
edgeInsetM,
count,
});
const ellipse = generatePresetMicrophones({
type,
aperture,
shape: "ellipse",
edgeInsetM,
count,
});
assertEqual(
JSON.stringify(rectangle),
JSON.stringify(ellipse),
type + " coordinates describe the preset instead of tracing the boundary shape",
);
assert(
rectangle.every((microphone) =>
positionInsideAperture(microphone.position, aperture, "rectangle") &&
positionInsideAperture(microphone.position, aperture, "ellipse")),
type + " inscribed-circle coordinates fit either generation boundary",
);
}
const ring = generatePresetMicrophones({
type: "ring",
aperture,
shape: "rectangle",
edgeInsetM,
count,
});
for (const microphone of ring) {
assertNear(
Math.hypot(microphone.position[0], microphone.position[1]),
expectedRadius,
1e-8,
"ring radius",
);
}
const spiral = generatePresetMicrophones({
type: "spiral",
aperture,
shape: "rectangle",
edgeInsetM,
count,
});
spiral.forEach((microphone, index) => {
assertNear(
Math.hypot(microphone.position[0], microphone.position[1]),
expectedRadius * Math.sqrt((index + 0.5) / count),
1e-8,
"equal-area Fermat radius " + index,
);
});
assertEqual(
generatePresetMicrophones({ type: "spiral", aperture, count: 1 })[0].position.join(","),
"0,0,0",
"single-microphone spiral is centered",
);
});
test("Archimedean preset은 중심부터 내접원까지 선형 반경의 실제 나선을 만든다", () => {
const aperture = { width: 0.5, height: 0.3 };
const edgeInsetM = 0.02;
const maximumRadius = 0.13;
const turns = 2.5;
const count = 11;
const microphones = generatePresetMicrophones({
type: "archimedean",
aperture,
shape: "rectangle",
edgeInsetM,
turns,
count,
});
assertEqual(microphones.length, count, "Archimedean count");
microphones.forEach((microphone, index) => {
assertNear(
Math.hypot(microphone.position[0], microphone.position[1]),
maximumRadius * index / (count - 1),
1e-8,
"Archimedean linear radius " + index,
);
});
assertEqual(microphones[0].position.join(","), "0,0,0", "spiral starts at center");
assertNear(microphones.at(-1).position[0], -maximumRadius, 1e-8, "2.5 turns end at -x");
assertNear(microphones.at(-1).position[1], 0, 1e-8, "2.5 turns end on x axis");
assertThrows(
() => generatePresetMicrophones({
type: "archimedean",
aperture,
turns: 0,
count,
}),
"0.25",
"Archimedean turn lower bound",
);
});
test("타원 격자는 요청한 rows x columns 수를 유지하며 경계 안에 배치된다", () => {
const aperture = { width: 0.45, height: 0.3 };
const microphones = generatePresetMicrophones({