diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index c0ffb7de..7af0fba7 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -37,8 +37,10 @@ export interface CorridorStructureHook { * 3 = 리본에 셀 마스크·축 고정 커브 조각 삼각형 추가 — 2026-08-26. * 4 = 절취 측벽 서피스(`cutWalls`) 추가 — 2026-08-27. * 5 = 측벽에서 빗금(UV)을 뺀다 — 벽은 방향성을 표현하지 않는다(2026-08-27 사용자). + * 6 = 리본의 `patch`·`patchClip` 표식을 담는다 — 안 담아서 저장본으로 다시 연 코리도가 + * 패치인 줄 모르고 지형 스냅에 끌려갔다(2026-09-02). */ -const ENVELOPE_VERSION = 5; +const ENVELOPE_VERSION = 6; interface CorridorEnvelope { version: number; @@ -49,6 +51,13 @@ interface CorridorEnvelope { colCount: number; chainages: number[]; positionsBase64: string; + /** + * 구조물 패치 리본 표식(2026-09-02) — 빌드 때만 쓰는 값이 아니라 **뷰어가 그릴 때도 + * 본다**(`B05_Profile_UI_Viewer.ts` 지형 스냅 제외). 안 담으면 저장본으로 다시 연 + * 코리도가 패치인 줄 몰라 바깥 끝이 원지반까지 끌려 내려간다. + */ + patch?: boolean; + patchClip?: boolean; /** 셀 마스크(2026-08-26 투영 커브 안쪽 지우기) — 없으면 구멍 없는 리본. */ cellMaskBase64?: string; /** 축 고정 커브가 걸친 셀의 조각 삼각형·UV(2026-08-26). */ @@ -142,6 +151,8 @@ function fnv1a(text: string): string { // 날개 사이가 벌어졌고, 바깥선만 자르는 판은 버전을 안 올려 화면에 못 올라갔다.) // 75 = 73 상태로 원복 — 트림 없음. 날개 몫 행은 지반 핀 + 스케치 확장 + 바닥 상단 // Z 초과 삭제까지만 한다(2026-08-27 사용자: "2번 전으로"). +// (76~78의 트림 도구 `wingOuterLines`·`wingTrimAt`·`clipRunToRange` 는 2026-09-02에 +// 제거했다 — 원복 뒤 다시 배선되지 않아 참조가 0이었다. 아래는 이력으로만 남긴다.) // 76 = 날개 몫 행을 **주황 고리의 바깥선**에서 끝낸다(`wingOuterLines`+`wingTrimAt`). // 고리가 그 행까지 모자라면 **끝 마디를 복사해 20m 연장**해서 만나게 한다 // (2026-08-27 사용자). 안쪽은 안 자른다. @@ -280,6 +291,8 @@ function serialize(build: CorridorBuildResult, hash: string): CorridorEnvelope { colCount: ribbon.colCount, chainages: ribbon.chainages, positionsBase64: base64FromFloats(ribbon.positions), + ...(ribbon.patch ? { patch: true } : {}), + ...(ribbon.patchClip ? { patchClip: true } : {}), ...(ribbon.cellMask ? { cellMaskBase64: base64FromBytes(ribbon.cellMask) } : {}), ...(ribbon.trimTris && ribbon.trimUvs ? { @@ -307,6 +320,8 @@ function deserialize(envelope: CorridorEnvelope): CorridorBuildResult { colCount: ribbon.colCount, chainages: ribbon.chainages, positions: floatsFromBase64(ribbon.positionsBase64), + ...(ribbon.patch ? { patch: true } : {}), + ...(ribbon.patchClip ? { patchClip: true } : {}), ...(ribbon.cellMaskBase64 ? { cellMask: bytesFromBase64(ribbon.cellMaskBase64) } : {}), ...(ribbon.trimTrisBase64 && ribbon.trimUvsBase64 ? { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Carve_Wing.ts b/B05_Profile/B05_Profile_UI_Corridor_Carve_Wing.ts index a16b9bf3..534cf72b 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Carve_Wing.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Carve_Wing.ts @@ -8,7 +8,10 @@ * 그대로는 그 부채꼴을 못 덮는다. 여기 도구들이 그 간극을 메운다. * · 종방향 — `wingSpanOf`(직선 투영, BOX암거용) / `wingSpanOnRoute`(노선 실투영, 세월교용) * · 횡방향 — `wingRangeAt`(행 횡단선 × 고리 교점의 안쪽·바깥쪽 끝) - * · 줄 손질 — `extendRunTo` / `prependRunTo` / `clipRunToRange` / `clipRunBelow` + * · 줄 손질 — `extendRunTo` / `prependRunTo` / `clipRunBelow` + * + * 「주황 바깥선 트림」(버전 76~78) 도구는 **뺐다**(2026-09-02) — 그 판이 사용자 지시로 + * 원복된 뒤 다시 배선되지 않아 참조가 0이었다. 이력은 `_Corridor.ts` 버전 주석에 남는다. * ========================================================================== */ import type { RouteFrame } from "./B05_Profile_UI_Corridor_Structures"; @@ -251,150 +254,3 @@ export function extendRunTo(run: OffsetPoint[], targetOffset: number): OffsetPoi }, ]; } - -/** 날개 하나의 트림선 — 바깥선 폴리라인과 그 날개가 노선에서 차지하는 구간. */ -export interface WingTrimLine { - line: Array<[number, number]>; - fromM: number; - toM: number; -} - -/** 트림선이 모자랄 때 **끝 마디를 복사해 늘리는** 길이(m, 2026-08-27 사용자). */ -const WING_TRIM_EXTEND_M = 20; - -/** - * 주황 고리의 **바깥선**만 뽑아 양 끝을 늘린 트림선(2026-08-27 사용자: "주황색 - * 바깥선으로 잘라야 해 / 조금 모자라면 해당선을 복사해서 연장하고 트림"). - * - * `wingBoxLoops`가 내는 고리 차례는 [시작 링, 끝 링, 끝 링→끝선 교점, …성토 끝선…, - * 시작 링→끝선 교점, (닫기)]다. 바깥선 = 인덱스 2 ~ 끝-2 — 안쪽 날개벽 선은 뺀다. - * 고리 종방향 밖 행은 교점이 없어 트림이 안 걸리므로, 양 끝 마디 방향으로 - * `WING_TRIM_EXTEND_M`만큼 **연장한 점을 붙여** 그 행에서도 만나게 한다. - */ -export function wingOuterLines( - curves: ReadonlyArray, - chainageM: number, - side: "left" | "right", - frameAt: (chainageM: number) => RouteFrame | null, -): WingTrimLine[] { - const candidates = routeCandidates(chainageM, frameAt); - if (!candidates.length) return []; - const lines: WingTrimLine[] = []; - for (const curve of curves) { - if (curve.source !== "wing-box" || curve.side !== side) continue; - if (Math.abs(curve.setChainageM - chainageM) > 1e-6) continue; - for (const loop of curve.loops) { - if (loop.length < 5) continue; - const line: Array<[number, number]> = loop - .slice(2, loop.length - 1) - .map((point) => [point[0], point[1]]); - if (line.length < 2) continue; - const stretch = (near: [number, number], far: [number, number]): [number, number] => { - const dx = far[0] - near[0]; - const dy = far[1] - near[1]; - const length = Math.hypot(dx, dy); - if (length < 1e-9) return far; - return [ - far[0] + (dx / length) * WING_TRIM_EXTEND_M, - far[1] + (dy / length) * WING_TRIM_EXTEND_M, - ]; - }; - // 이 날개가 노선에서 차지하는 구간 — **행을 어느 날개 몫으로 볼지** 가른다. - const spans = line.map(([x, y]) => projectChainage(candidates, x, y)); - line.unshift(stretch(line[1], line[0])); - line.push(stretch(line[line.length - 2], line[line.length - 1])); - lines.push({ line, fromM: Math.min(...spans), toM: Math.max(...spans) }); - } - } - return lines; -} - -/** - * 그 행 횡단선이 **그 행을 맡은 날개**의 트림선과 만나는 편거리. 안 만나면 null. - * - * 날개는 **부재별로 구분한다**(2026-08-27 사용자: "구조물별로 구분되어야 함"). - * 세월교는 한 측에 유입·유출 날개가 각각 있어 트림선이 둘인데, 끝 마디를 20m 늘려 - * 놓아 서로의 구간까지 뻗는다. 그대로 합쳐서 가장 바깥을 고르면 유입 날개 선이 - * 유출 날개 행을 잘라 버린다 — 행마다 **누가거리가 가장 가까운 날개 하나만** 쓴다. - */ -export function wingTrimAt( - lines: ReadonlyArray, - side: "left" | "right", - frame: RouteFrame, - at: number, -): number | null { - if (!lines.length) return null; - let owner = lines[0]; - let bestGap = Infinity; - for (const entry of lines) { - const gap = Math.max(entry.fromM - at, at - entry.toM, 0); - if (gap < bestGap) { - bestGap = gap; - owner = entry; - } - } - const sign = side === "left" ? 1 : -1; - let outer: number | null = null; - { - const line = owner.line; - for (let i = 0; i < line.length - 1; i += 1) { - const [ax, ay] = line[i]; - const [bx, by] = line[i + 1]; - const ex = bx - ax; - const ey = by - ay; - const denominator = frame.leftX * ey - frame.leftY * ex; - if (Math.abs(denominator) < 1e-12) continue; - const t = (frame.leftX * (ay - frame.cy) - frame.leftY * (ax - frame.cx)) / denominator; - if (t < -1e-9 || t > 1 + 1e-9) continue; - const o = - Math.abs(frame.leftX) > Math.abs(frame.leftY) - ? (ax + ex * t - frame.cx) / frame.leftX - : (ay + ey * t - frame.cy) / frame.leftY; - if (Math.abs(o) > NEAR_LIMIT_M) continue; - if (o * sign <= 0) continue; - if (outer === null || (o - outer) * sign > 0) outer = o; - } - } - return outer; -} - -/** - * 줄을 편거리 구간 `[from, to]`(부호 기준 안쪽→바깥)만 남기고 **자른다**. 경계를 - * 지나는 마디에는 교차점을 꽂아 격자 계단을 막는다. - * - * 날개 몫 행(구조물 점유 구간 밖)을 **주황 스케치의 바깥선**에서 끝내는 데 쓴다 - * (2026-08-27 사용자: "주황색 바깥선으로 잘라야 해"). 안쪽 경계로는 자르지 않는다 — - * 자르면 구조물 구간과 날개 사이가 벌어진다. 벽 끝점 횡단선으로 자르던 것도 폐기 - * (스케치보다 바깥이라 스케치에 없는 자리까지 판이 남았다). - */ -export function clipRunToRange( - run: OffsetPoint[], - from: number, - to: number, - sign: number, -): OffsetPoint[] { - const inside = (point: OffsetPoint): boolean => - (point.offset_m - from) * sign >= -1e-9 && (to - point.offset_m) * sign >= -1e-9; - const between = (a: OffsetPoint, b: OffsetPoint, offset: number): OffsetPoint => { - const step = b.offset_m - a.offset_m; - const ratio = Math.abs(step) < 1e-12 ? 0 : (offset - a.offset_m) / step; - return { - offset_m: offset, - elevation_m: a.elevation_m + (b.elevation_m - a.elevation_m) * ratio, - }; - }; - const kept: OffsetPoint[] = []; - for (let i = 0; i < run.length; i += 1) { - const point = run[i]; - if (i > 0) { - const previous = run[i - 1]; - const step = point.offset_m - previous.offset_m; - const crossings = [from, to] - .filter((bound) => (previous.offset_m - bound) * (point.offset_m - bound) < 0) - .sort((a, b) => (step >= 0 ? a - b : b - a)); - for (const bound of crossings) kept.push(between(previous, point, bound)); - } - if (inside(point)) kept.push(point); - } - return kept; -}