sync: 4환경 수렴본 main 반영 (커밋 34건) #10

Merged
eomsangdon merged 34 commits from main_laptop_1 into main 2026-09-02 17:24:19 +09:00
2 changed files with 20 additions and 149 deletions
Showing only changes of commit 475fb73505 - Show all commits
+16 -1
View File
@@ -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
? {
@@ -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<PlanCurve>,
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<WingTrimLine>,
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;
}