/** * 선택한 객체들을 하나의 닫힌 경계(점렬)로 잇는다. * HATCH·BOUNDARY·REGION·WIPEOUT이 공유한다. */ import type { Point } from '@flatten-js/core'; import type { Entity } from '../../entities/Entity'; import { dedupeConsecutive, sampleEntityPoints } from './sample-entity'; const DEFAULT_TOLERANCE = 1e-3; const near = (a: Point, b: Point, tolerance: number): boolean => Math.abs(a.x - b.x) <= tolerance && Math.abs(a.y - b.y) <= tolerance; /** * 끝점이 맞닿는 순서대로 이어 붙여 경계를 만든다. * 이미 닫힌 객체(원·사각형) 하나만 골랐다면 그 점렬을 그대로 돌려준다. */ export function entitiesToLoop(entities: Entity[], tolerance = DEFAULT_TOLERANCE): Point[] { const chains = entities .map((entity) => sampleEntityPoints(entity)) .filter((points) => points.length >= 2); if (!chains.length) return []; if (chains.length === 1) return closeLoop(chains[0], tolerance); const remaining = [...chains]; let loop = remaining.shift() as Point[]; while (remaining.length) { const tail = loop[loop.length - 1]; const index = remaining.findIndex( (chain) => near(chain[0], tail, tolerance) || near(chain[chain.length - 1], tail, tolerance) ); if (index === -1) break; // 끊긴 경계 — 여기까지만 잇는다 const [chain] = remaining.splice(index, 1); const ordered = near(chain[0], tail, tolerance) ? chain : [...chain].reverse(); loop = dedupeConsecutive([...loop, ...ordered]); } return closeLoop(loop, tolerance); } /** 시작점과 끝점이 떨어져 있으면 닫는다 */ export function closeLoop(points: Point[], tolerance = DEFAULT_TOLERANCE): Point[] { const cleaned = dedupeConsecutive(points); if (cleaned.length < 3) return cleaned; const first = cleaned[0]; const last = cleaned[cleaned.length - 1]; return near(first, last, tolerance) ? cleaned : [...cleaned, first.clone()]; }