Files
Aislo/B07_DesignDetail/openwebcad/src/tools/array-tool.helpers.ts
T
eomsangdonandClaude Opus 5.5 1d5cdff0a5 feat(CAD): 337-5 라 옮기기 무리 2 단계 — COPY · MIRROR · STRETCH · ARRAY · PEDIT 고르기
- COPY: 기준점 [변위(D)/모드(O)/다중(M)] · 두 번째 점 [배열(A)] → [배열(A)/종료(E)/명령 취소(U)] <종료> 되풀이 · 첫 빈 Enter = 변위 · COPYMODE 기억 · A 항목 수 · F 맞춤
- MIRROR: 대칭선 두 점 → 원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오> · Esc = 안 만듦
- STRETCH: MOVE 틀 + 걸침 창 안 꼭짓점만(선 · 폴리선 토막 · 사각형) · 창 안 통째 · 하나씩 고른 것 = 이동 · STRETCH.DISP
- ARRAY: 유형 [직사각형(R)/원형(PO)] <직사각형>(ARRAYTYPE) → 편집 물음 되풀이(COU · S · COL · R · X / I · A · F · ROT · X) · 미리보기 4 × 3 · 6 개 360° · 처음 간격 범위 1.5 배 · Esc = 안 만듦(N17 없어짐)
- PEDIT: 공용 고르기 상태(객체 선택 · 빈 Enter · Esc = 끝 · 묶은 뒤 끝)
- 시험 transform-acad 29 + ALL · L · P 1(선택 도구 처리 뒤 돎)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-16 00:25:20 +09:00

170 lines
6.3 KiB
TypeScript

/** ARRAY 계산 · 물음(337 라 · 설계 1-3) — 직사각형 열 × 행 · 원형 항목 · 채울 각도 · 항목 회전 */
import type { Box, Point } from '@flatten-js/core';
import type { OptionSpec, PromptSpec } from '../commands/prompt';
import type { Entity } from '../entities/Entity';
import { cloneKeepingLook } from './move-tool.helpers';
import type { PickedContext } from './transform.helpers';
export interface ArrayValues extends PickedContext {
columns: number;
rows: number;
columnSpacing: number;
rowSpacing: number;
center: Point | null;
items: number;
/** 채울 각도(도 · + 반시계) */
fill: number;
rotateItems: boolean;
}
const opt = (key: string, kw: string, label: string, en = key): OptionSpec => ({
key,
kw,
en,
label,
});
export const ARRAY_OPT = {
RECTANGULAR: opt('RECTANGULAR', 'R', '직사각형'),
POLAR: opt('POLAR', 'PO', '원형'),
PATH: opt('PATH', 'PA', '경로'),
COUNT: opt('COUNT', 'COU', '개수'),
SPACING: opt('SPACING', 'S', '간격두기'),
COLUMNS: opt('COLUMNS', 'COL', '열'),
ROWS: opt('ROWS', 'R', '행'),
POLAR_ROWS: opt('ROWS', 'ROW', '행'),
ITEMS: opt('ITEMS', 'I', '항목'),
ANGLE: opt('ANGLE', 'A', '사이의 각도'),
FILL: opt('FILL', 'F', '채울 각도'),
ROTATE: opt('ROTATE', 'ROT', '항목 회전'),
EXIT: opt('EXIT', 'X', '종료'),
ASSOCIATIVE: opt('ASSOCIATIVE', 'AS', '연관'),
BASE: opt('BASE', 'B', '기준점'),
LEVELS: opt('LEVELS', 'L', '수준'),
AXIS: opt('AXIS', 'A', '회전축'),
};
const O = ARRAY_OPT;
const COUNT_RULE = { int: true, min: 1, max: 32767 } as const;
/** 항목 사이 각 — 채울 각이 한 바퀴면 항목 수로 나눔 · 아니면 사이 수로 */
export const angleBetween = (values: Pick<ArrayValues, 'items' | 'fill'>): number => {
if (Math.abs(values.fill) >= 360) return values.fill / values.items;
return values.items > 1 ? values.fill / (values.items - 1) : 0;
};
const value = (text: string, remember: string, initial: number, format?: 'int' | 'ang') => ({
text,
default: { kind: 'value' as const, remember, initial, ...(format ? { format } : {}) },
});
/** 물음 — 함수 꼴은 지금 값(빈 context 면 처음 값) */
export const ARRAY_PROMPT = {
type: (fallback: string): PromptSpec => ({
text: '배열 유형 입력',
options: [O.RECTANGULAR, O.POLAR],
absent: [O.PATH],
default: { kind: 'option', key: fallback },
}),
center: { text: '배열의 중심점 지정', absent: [O.BASE, O.AXIS] } satisfies PromptSpec,
editRect: {
text: '그립을 선택하여 배열을 편집하거나',
options: [O.COUNT, O.SPACING, O.COLUMNS, O.ROWS, O.EXIT],
absent: [O.ASSOCIATIVE, O.BASE, O.LEVELS],
default: { kind: 'option', key: O.EXIT.key },
} satisfies PromptSpec,
editPolar: {
text: '그립을 선택하여 배열을 편집하거나',
options: [O.ITEMS, O.ANGLE, O.FILL, O.ROTATE, O.EXIT],
absent: [O.ASSOCIATIVE, O.BASE, O.POLAR_ROWS, O.LEVELS],
default: { kind: 'option', key: O.EXIT.key },
} satisfies PromptSpec,
columns: (v: Partial<ArrayValues>): PromptSpec => ({
...value('열 수 입력', 'ARRAY.COLS', v.columns ?? 4, 'int'),
number: COUNT_RULE,
}),
rows: (v: Partial<ArrayValues>): PromptSpec => ({
...value('행 수 입력', 'ARRAY.ROWS', v.rows ?? 3, 'int'),
number: COUNT_RULE,
}),
columnSpacing: (v: Partial<ArrayValues>): PromptSpec =>
value('열 사이의 거리 지정', 'ARRAY.COLDIST', v.columnSpacing ?? 1),
rowSpacing: (v: Partial<ArrayValues>): PromptSpec =>
value('행 사이의 거리 지정', 'ARRAY.ROWDIST', v.rowSpacing ?? 1),
items: (v: Partial<ArrayValues>): PromptSpec => ({
...value('배열의 항목 수 입력', 'ARRAY.ITEMS', v.items ?? 6, 'int'),
number: COUNT_RULE,
}),
angle: (v: Partial<ArrayValues>): PromptSpec =>
value(
'항목 사이의 각도 지정',
'ARRAY.ANGLE',
angleBetween({ items: v.items ?? 6, fill: v.fill ?? 360 }),
'ang'
),
fill: (v: Partial<ArrayValues>): PromptSpec =>
value('채울 각도 지정(+=ccw, -=cw)', 'ARRAY.FILL', v.fill ?? 360, 'ang'),
rotateItems: (v: Partial<ArrayValues>): PromptSpec => ({
text: '배열된 항목을 회전하시겠습니까?',
options: [opt('YES', 'Y', '예'), opt('NO', 'N', '아니오')],
default: { kind: 'option', key: v.rotateItems === false ? 'NO' : 'YES' },
}),
};
/** 고른 것 범위 — 처음 간격 = 폭 · 높이의 1.5 배(0 이면 다른 쪽 · 1) */
export function initialSpacing(originals: Entity[]): { column: number; row: number } {
const box = originals.reduce<Box | null>((all, entity) => {
const next = entity.getBoundingBox();
return all ? all.merge(next) : next;
}, null);
const width = box ? box.xmax - box.xmin : 0;
const height = box ? box.ymax - box.ymin : 0;
const fallback = Math.max(width, height) || 1;
return { column: 1.5 * (width || fallback), row: 1.5 * (height || fallback) };
}
/** 고른 것 범위 가운데 — 항목 회전 끔일 때 옮김 기준 */
function selectionCenter(originals: Entity[]): Point | null {
const boxes = originals.map((entity) => entity.getBoundingBox());
if (!boxes.length) return null;
return boxes.reduce((all, next) => all.merge(next)).center;
}
/** 직사각형 — (0,0) 원본 빼고 열 × 행 사본 */
export function rectangularCopies(values: ArrayValues): Entity[] {
const copies: Entity[] = [];
for (let row = 0; row < values.rows; row++) {
for (let column = 0; column < values.columns; column++) {
if (!row && !column) continue;
for (const entity of values.originals) {
const copy = cloneKeepingLook(entity);
copy.move(column * values.columnSpacing, row * values.rowSpacing);
copies.push(copy);
}
}
}
return copies;
}
/** 원형 — 중심 둘레 항목 수 − 1 사본 · 항목 회전 끔이면 돌지 않고 자리만 */
export function polarCopies(values: ArrayValues, center = values.center): Entity[] {
if (!center) return [];
const step = (angleBetween(values) * Math.PI) / 180;
const anchor = selectionCenter(values.originals);
const copies: Entity[] = [];
for (let index = 1; index < values.items; index++) {
const radians = step * index;
for (const entity of values.originals) {
const copy = cloneKeepingLook(entity);
if (values.rotateItems || !anchor) {
copy.rotate(center, radians);
} else {
const moved = anchor.rotate(radians, center);
copy.move(moved.x - anchor.x, moved.y - anchor.y);
}
copies.push(copy);
}
}
return copies;
}