파일마다 포맷 폭이 달라(≈80 대 100) 한 줄만 고쳐도 포맷터가 무관한 줄을 대량 재포맷했음. 사용자 지시로 전체를 한 번에 맞춤. 코드 동작 변경 없음 — 포맷만. - 프론트엔드 `.ts/.css/.html` → 저장소 prettier (`.prettierrc`, printWidth 100) - `B07_DesignDetail/openwebcad/**` → 자체 biome (tab 들여쓰기·single quote·lineWidth 100). `biome format` 만 사용 — `biome lint --write` 는 포맷 아닌 코드 수정까지 하므로 제외 - 파이썬 → `ruff format` (엔진 코드는 이미 정합, resources·scratch 스크립트 24개만 변경) 두 포맷터가 서로 되돌리지 않도록 `.prettierignore` 신규 — openwebcad 와 빌드·산출물 폴더를 prettier 대상에서 뺌. `.prettierrc` 에 `endOfLine: "auto"` 추가 — 기본값 `lf` 가 `core.autocrlf=true` 로 받은 CRLF 파일을 매번 전부 다시 써서 `--list-different` 가 실제 포맷 차이를 가리고 있었음. 검증: `tsc --noEmit` 통과(루트·openwebcad 둘 다), pytest 349 passed / 17 skipped / 0 failed, CAD vitest 87건 중 81 passed / 6 failed(laptop-sub 기준선과 동일, 회귀 없음). 포맷터 재실행 시 prettier·biome 모두 변경 0건. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
1.9 KiB
TypeScript
85 lines
1.9 KiB
TypeScript
import type { Point } from '@flatten-js/core';
|
||
import type { StartAndEndpointEntity } from '../App.types.ts';
|
||
import { isPointEqual } from './is-point-equal.ts';
|
||
|
||
/**
|
||
* Check if entities form a closed loop polygon
|
||
*/
|
||
export function isClosedPolygon(entities: StartAndEndpointEntity[]): boolean {
|
||
const uniquePoints: Point[] = [];
|
||
const counts: number[] = [];
|
||
const edges: Array<[number, number]> = [];
|
||
|
||
// Helper to find or add a point to uniquePoints, returning its index
|
||
const findOrAdd = (pt: Point): number => {
|
||
for (let i = 0; i < uniquePoints.length; i++) {
|
||
if (isPointEqual(uniquePoints[i], pt)) {
|
||
return i;
|
||
}
|
||
}
|
||
uniquePoints.push(pt);
|
||
counts.push(0);
|
||
return uniquePoints.length - 1;
|
||
};
|
||
|
||
// 1) Process each segment
|
||
for (const entity of entities) {
|
||
const start = entity.getStartPoint();
|
||
const end = entity.getEndPoint();
|
||
|
||
// 1a) no zero‐length segments
|
||
if (isPointEqual(start, end)) {
|
||
return false;
|
||
}
|
||
|
||
const si = findOrAdd(start);
|
||
const ei = findOrAdd(end);
|
||
|
||
counts[si]++;
|
||
counts[ei]++;
|
||
edges.push([si, ei]);
|
||
}
|
||
|
||
const N = entities.length;
|
||
|
||
// 2) must have exactly N unique points
|
||
if (uniquePoints.length !== N) {
|
||
return false;
|
||
}
|
||
|
||
// 3) each point must appear exactly twice
|
||
if (counts.some((c) => c !== 2)) {
|
||
return false;
|
||
}
|
||
|
||
// 4) build undirected adjacency
|
||
const adj: number[][] = Array.from({ length: N }, () => []);
|
||
for (const [u, v] of edges) {
|
||
adj[u].push(v);
|
||
adj[v].push(u);
|
||
}
|
||
|
||
// 5) each vertex must have degree 2
|
||
if (adj.some((neigh) => neigh.length !== 2)) {
|
||
return false;
|
||
}
|
||
|
||
// 6) connectivity: traverse from 0
|
||
const visited = new Set<number>();
|
||
const stack = [0];
|
||
while (stack.length) {
|
||
const u = stack.pop();
|
||
if (typeof u === 'undefined') {
|
||
break;
|
||
}
|
||
if (!visited.has(u)) {
|
||
visited.add(u);
|
||
for (const v of adj[u]) {
|
||
if (!visited.has(v)) stack.push(v);
|
||
}
|
||
}
|
||
}
|
||
|
||
return visited.size === N;
|
||
}
|