fix: make circular and spiral presets truthful
This commit is contained in:
@@ -40,13 +40,16 @@ aperture 크기를 입력하는 것만으로 기존 좌표가 자동 확대·축
|
||||
| --- | --- | --- |
|
||||
| 격자 | 행·열, 경계 여백 | 아래 행부터 위로, 각 행은 왼쪽에서 오른쪽으로 생성 |
|
||||
| 선형 | 마이크 수, x/y 축 | 선택한 중심축을 따라 일정 간격으로 생성 |
|
||||
| 원형 | 마이크 수 | 사각 경계는 사각 둘레, 타원 경계는 타원 둘레에 균일 배치 |
|
||||
| 나선 | 마이크 수 | golden angle을 사용하는 결정론적 Fermat 나선 |
|
||||
| 원형 링 | 마이크 수 | 경계의 짧은 유효 반지름에 맞춘 실제 내접원 위에 동일 각도 배치 |
|
||||
| Archimedean 나선 | 마이크 수, 회전 수 | 중심에서 시작해 <code>r∝θ</code>로 커지는 한 줄 곡선형 나선 |
|
||||
| Fermat 나선 | 마이크 수 | golden angle과 <code>r∝√n</code>을 쓰는 면적 균등 해바라기형 배열 |
|
||||
| 균일 랜덤 | 마이크 수, seed, 최소 간격 | 경계 내부에 seed로 재현되는 좌표 생성 |
|
||||
|
||||
**경계 여백**은 preset 좌표를 aperture 가장자리에서 안쪽으로 띄웁니다. 랜덤 배열은 요청한 최소
|
||||
간격을 만족하는 배치를 제한된 횟수 안에 찾지 못하면 현재 배열을 바꾸지 않고 오류를 표시합니다.
|
||||
같은 크기·형상·수·seed·간격을 사용하면 같은 랜덤 좌표가 생성됩니다.
|
||||
원형과 두 나선은 사각/타원 경계 자체를 변형해 따라가지 않고, 어느 경계에도 들어가는 실제 내접원을
|
||||
기준으로 만듭니다. 따라서 비정사각 aperture에서도 원이 타원이나 사각 둘레로 바뀌지 않습니다.
|
||||
|
||||
프리셋 생성은 현재 마이크 목록 전체를 교체합니다. 필요한 기존 좌표나 채널 보정이 있다면 먼저
|
||||
Array JSON을 저장하세요.
|
||||
|
||||
@@ -339,8 +339,9 @@
|
||||
<select id="preset-type">
|
||||
<option value="grid">격자</option>
|
||||
<option value="linear">선형</option>
|
||||
<option value="ring">원형</option>
|
||||
<option value="spiral">나선</option>
|
||||
<option value="ring">원형 링</option>
|
||||
<option value="archimedean">나선 · Archimedean</option>
|
||||
<option value="spiral">Fermat 나선 · 해바라기</option>
|
||||
<option value="random">균일 랜덤</option>
|
||||
</select>
|
||||
</label>
|
||||
@@ -356,6 +357,10 @@
|
||||
<option value="y">세로 (y)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field" id="preset-turns-field">
|
||||
<span>나선 회전 수</span>
|
||||
<input id="preset-turns" type="number" min="0.25" max="32" step="0.25" value="2.5" inputmode="decimal" />
|
||||
</label>
|
||||
<label class="field" id="preset-columns-field">
|
||||
<span>열</span>
|
||||
<input id="preset-columns" type="number" min="1" max="1024" step="1" value="4" inputmode="numeric" />
|
||||
|
||||
@@ -38,7 +38,8 @@ const requiredIds = [
|
||||
"array-builder-split-handle", "array-input", "export-button", "array-id",
|
||||
"array-name", "array-description", "aperture-width", "aperture-height",
|
||||
"aperture-shape", "preset-type", "preset-count", "preset-columns",
|
||||
"preset-rows", "preset-seed", "preset-minimum-spacing",
|
||||
"preset-rows", "preset-turns", "preset-seed", "preset-minimum-spacing",
|
||||
"preset-hint",
|
||||
"generate-preset-button", "array-canvas", "select-tool-button",
|
||||
"add-tool-button", "array-view-2d-button", "array-view-3d-button",
|
||||
"canvas-front-badge", "fit-view-button", "zoom-in-button", "zoom-out-button",
|
||||
@@ -1129,6 +1130,7 @@ function syncPresetControls() {
|
||||
const grid = type === "grid";
|
||||
const random = type === "random";
|
||||
const linear = type === "linear";
|
||||
const archimedean = type === "archimedean";
|
||||
for (const element of [elements["preset-columns"], elements["preset-rows"]]) {
|
||||
element.closest(".field")?.toggleAttribute("hidden", !grid);
|
||||
}
|
||||
@@ -1136,6 +1138,16 @@ function syncPresetControls() {
|
||||
elements["preset-seed"].closest(".field")?.toggleAttribute("hidden", !random);
|
||||
elements["preset-minimum-spacing"].closest(".field")?.toggleAttribute("hidden", !random);
|
||||
elements["preset-axis"]?.closest(".field")?.toggleAttribute("hidden", !linear);
|
||||
elements["preset-turns"].closest(".field")?.toggleAttribute("hidden", !archimedean);
|
||||
const hints = {
|
||||
grid: "격자는 아래에서 위로, 각 행은 왼쪽에서 오른쪽 순서로 채널을 만듭니다.",
|
||||
linear: "선택한 x/y 축 위에 마이크를 같은 간격으로 놓습니다.",
|
||||
ring: "짧은 유효 반지름에 맞춘 실제 내접원 위에 같은 각도 간격으로 놓습니다.",
|
||||
archimedean: "r∝θ인 한 줄 나선입니다. 회전 수를 늘리면 같은 반경 안에서 더 많이 감깁니다.",
|
||||
spiral: "golden angle과 r∝√n을 쓰는 면적 균등 Fermat 배열입니다. 점만 보면 해바라기형으로 보입니다.",
|
||||
random: "seed로 재현되는 균일 랜덤 좌표를 요청한 최소 간격으로 생성합니다.",
|
||||
};
|
||||
elements["preset-hint"].textContent = hints[type] ?? hints.grid;
|
||||
}
|
||||
|
||||
function syncHistoryButtons() {
|
||||
@@ -1277,6 +1289,12 @@ function presetOptions() {
|
||||
options.count = integerInput(elements["preset-count"], "마이크 수", 1, 1024);
|
||||
}
|
||||
if (type === "linear") options.axis = elements["preset-axis"]?.value ?? "x";
|
||||
if (type === "archimedean") {
|
||||
options.turns = finiteInput(elements["preset-turns"], "나선 회전 수", {
|
||||
minimum: 0.25,
|
||||
maximum: 32,
|
||||
});
|
||||
}
|
||||
if (type === "random") {
|
||||
options.seed = integerInput(elements["preset-seed"], "Seed", -2147483648, 4294967295);
|
||||
options.minimumSpacingM = finiteInput(
|
||||
|
||||
+42
-21
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user