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
+42 -21
View File
@@ -1,6 +1,13 @@
export const MAX_MICROPHONES = 1024;
export const APERTURE_SHAPES = Object.freeze(["rectangle", "ellipse"]);
export const PRESET_TYPES = Object.freeze(["grid", "linear", "ring", "spiral", "random"]);
export const PRESET_TYPES = Object.freeze([
"grid",
"linear",
"ring",
"archimedean",
"spiral",
"random",
]);
const COORDINATE_SYSTEM = Object.freeze({
handedness: "right",
@@ -136,41 +143,51 @@ function linearPositions(options, bounds, shape) {
});
}
function rectanglePerimeterPosition(ratio, halfWidth, halfHeight) {
const width = halfWidth * 2;
const height = halfHeight * 2;
const perimeter = 2 * (width + height);
let distance = ratio * perimeter;
if (distance <= width) return [-halfWidth + distance, -halfHeight, 0];
distance -= width;
if (distance <= height) return [halfWidth, -halfHeight + distance, 0];
distance -= height;
if (distance <= width) return [halfWidth - distance, halfHeight, 0];
distance -= width;
return [-halfWidth, halfHeight - distance, 0];
function inscribedCircleRadius(bounds) {
return Math.min(bounds.halfWidth, bounds.halfHeight);
}
function ringPositions(options, bounds, shape) {
function ringPositions(options, bounds) {
const count = boundedInteger(options.count ?? 16, "마이크 수", 1, MAX_MICROPHONES);
if (count === 1) return [[0, 0, 0]];
const radius = inscribedCircleRadius(bounds);
return Array.from({ length: count }, (_, index) => {
const ratio = index / count;
if (shape === "rectangle") {
return rectanglePerimeterPosition(ratio, bounds.halfWidth, bounds.halfHeight);
}
const angle = ratio * Math.PI * 2;
return [bounds.halfWidth * Math.cos(angle), bounds.halfHeight * Math.sin(angle), 0];
return [radius * Math.cos(angle), radius * Math.sin(angle), 0];
});
}
function spiralPositions(options, bounds) {
const count = boundedInteger(options.count ?? 32, "마이크 수", 1, MAX_MICROPHONES);
if (count === 1) return [[0, 0, 0]];
const maximumRadius = inscribedCircleRadius(bounds);
return Array.from({ length: count }, (_, index) => {
const radius = Math.sqrt((index + 0.5) / count);
const angle = index * GOLDEN_ANGLE;
return [
bounds.halfWidth * radius * Math.cos(angle),
bounds.halfHeight * radius * Math.sin(angle),
maximumRadius * radius * Math.cos(angle),
maximumRadius * radius * Math.sin(angle),
0,
];
});
}
function archimedeanSpiralPositions(options, bounds) {
const count = boundedInteger(options.count ?? 32, "마이크 수", 1, MAX_MICROPHONES);
if (count === 1) return [[0, 0, 0]];
const turns = finiteNumber(options.turns ?? 2.5, "나선 회전 수");
if (turns < 0.25 || turns > 32) {
throw new RangeError("나선 회전 수는 0.25 이상 32 이하이어야 합니다.");
}
const maximumRadius = inscribedCircleRadius(bounds);
return Array.from({ length: count }, (_, index) => {
const ratio = index / (count - 1);
const angle = ratio * turns * Math.PI * 2;
const radius = maximumRadius * ratio;
return [
radius * Math.cos(angle),
radius * Math.sin(angle),
0,
];
});
@@ -286,7 +303,10 @@ export function generatePresetMicrophones(options = {}) {
const bounds = presetBounds(options.aperture, options.edgeInsetM);
let positions;
if (options.type === "linear") positions = linearPositions(options, bounds, shape);
else if (options.type === "ring") positions = ringPositions(options, bounds, shape);
else if (options.type === "ring") positions = ringPositions(options, bounds);
else if (options.type === "archimedean") {
positions = archimedeanSpiralPositions(options, bounds);
}
else if (options.type === "spiral") positions = spiralPositions(options, bounds);
else if (options.type === "random") positions = randomPositions(options, bounds, shape);
else positions = gridPositions(options, bounds, shape);
@@ -320,6 +340,7 @@ export function createArrayDefinition(options = {}) {
minimumSpacingM: options.minimumSpacingM,
edgeInsetM: options.edgeInsetM,
axis: options.axis,
turns: options.turns,
});
return arrayDefinition;
}