From 86a20cccb4a0241d3c2ec604cce24c83d0207b57 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 27 Aug 2026 10:35:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05):=20=EC=A0=88=EC=B7=A8=20=EC=9C=A4?= =?UTF-8?q?=EA=B3=BD=EC=9D=84=20z=EB=A1=9C=20=EB=B0=80=EC=96=B4=EB=82=B4?= =?UTF-8?q?=20=EC=B8=A1=EB=B2=BD=EC=9D=84=20=EC=84=B8=EC=9A=B0=EA=B3=A0=20?= =?UTF-8?q?=EC=84=B1=ED=86=A0=EB=A9=B4=EC=97=90=20=EB=A8=B8=EC=A7=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 자를 때 쓴 평면 스케치(절취 윤곽)를 z축으로 밀어내 성토면 ↔ 원지반을 잇는 측벽을 세운다(2026-08-27 사용자). 벽은 성토 리본과 **같은 메쉬·같은 재질**에 담아 별도 서피스가 아니라 머지된 한 면으로 그려진다. 구조물이 안착된 성토부 방향(바닥) 서피스는 다음 작업. - Build: 성토 리본에 격자점별 원지반 표고 `groundZ`를 싣는다(행 지반선을 rows에 보관해 flush에서 offset으로 읽는다). 벽의 밑선이다. - Cut: `pushSkirt()` — 절취 윤곽 모서리마다 [서피스 z → 원지반 z] 사각형을 삼각형 둘로 세워 `trimTris`에 함께 담는다. UV는 u=누가거리/2, v=z/2로 빗금이 세로로도 실척으로 이어진다. 높이 0인 구간(성토 끝선)은 건너뛴다. - Corridor: BUILD_VERSION 52 → 53. 검증(실빌드 데이터): - 벽 삼각형 우 417·좌 418(조각 삼각형 합계 1,166·1,135), groundZ 적재 확인. - 벽 높이 우 0.001~2.80m(평균 1.08)·좌 0.003~5.25m(평균 1.61). - ISO 화면에서 세월교·BOX암거 절취부 옆면이 청록 벽으로 막혔다. 바닥은 아직 열림. - npm run typecheck 통과, prettier 적용. 남은 것: 벽이 격자 모서리를 타 6cm×0.2m 계단이 보인다(교점 직선으로 세우면 없어진다). Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_UI_Corridor.ts | 6 +-- B05_Profile/B05_Profile_UI_Corridor_Build.ts | 18 +++++++- B05_Profile/B05_Profile_UI_Corridor_Cut.ts | 47 ++++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index e7ef3041..e05e86d4 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -99,9 +99,9 @@ function fnv1a(text: string): string { // 투영 커브 안쪽 성토면 절단 — 커브 다각형 기준(2026-08-26). // ※ 40에 머물러 있는 동안 기하 수정 3회가 통째로 묻혔다 — 저장본이 그대로 // 복원되어 화면·데이터가 하나도 안 바뀌었다. 기하를 고치면 **반드시** 올릴 것. -// 52 = 날개벽 사다리꼴을 절취 영역에 합치고, 잘린 자리 윤곽을 새 데이텀(+3m)에 -// `cut-merged` 커브로 낸다(2026-08-27). -const BUILD_VERSION = 52; +// 53 = 잘린 자리 측벽 — 절취 윤곽을 z로 밀어내 성토면↔원지반을 잇는 벽을 세우고 +// 성토 메쉬에 합친다(2026-08-27). +const BUILD_VERSION = 53; /** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Build.ts b/B05_Profile/B05_Profile_UI_Corridor_Build.ts index 94f3d8a4..90b3cdae 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Build.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Build.ts @@ -83,6 +83,11 @@ export interface CorridorRibbon { trimTris?: Float64Array; /** `trimTris`와 짝인 UV(삼각형당 6) — 빗금이 실척으로 이어지게 한다. */ trimUvs?: Float32Array; + /** + * 격자점별 **원지반 표고**(rowCount × colCount) — 성토 리본에만 싣는다. + * 잘린 자리 측벽을 성토면에서 원지반까지 세우는 밑선이다(2026-08-27 사용자). + */ + groundZ?: Float64Array; } /** 시·종점 마구리(캡) — 설계선과 지반선 사이를 세로로 봉인하는 스트립(모델 좌표). */ @@ -242,6 +247,8 @@ export function buildCorridor( sections: Map; outerLeft: OffsetPoint; outerRight: OffsetPoint; + /** 그 행 지반선 — 성토 리본에 원지반 표고를 함께 실어 절단면 측벽의 밑선으로 쓴다. */ + ground: ((offset: number) => number) | null; }> = []; const toModel = (row: (typeof rows)[number], point: OffsetPoint): [number, number, number] => [ @@ -537,6 +544,7 @@ export function buildCorridor( sections, outerLeft: outerOf("left"), outerRight: outerOf("right"), + ground: groundRow, }); } } @@ -574,7 +582,10 @@ export function buildCorridor( } const chainages: number[] = []; const positions = new Float64Array((endExclusive - start) * colCount * 3); + // 성토 리본만 원지반 표고를 같이 싣는다 — 잘린 자리 측벽의 밑선(2026-08-27 사용자). + const groundZ = kind === "fill" ? new Float64Array((endExclusive - start) * colCount) : null; let cursor = 0; + let groundCursor = 0; for (let row = start; row < endExclusive; row += 1) { chainages.push(rows[row].chainage_m); rows[row].sections.get(key)!.forEach((point) => { @@ -583,9 +594,14 @@ export function buildCorridor( positions[cursor + 1] = y; positions[cursor + 2] = z; cursor += 3; + if (groundZ) { + // 지반선을 못 읽는 행은 서피스 표고를 그대로 둔다 — 높이 0짜리 벽이 된다. + groundZ[groundCursor] = rows[row].ground?.(point.offset_m) ?? z; + groundCursor += 1; + } }); } - ribbons.push({ kind, side, colCount, chainages, positions }); + ribbons.push({ kind, side, colCount, chainages, positions, ...(groundZ ? { groundZ } : {}) }); // 측구 리본이 노선 중간에서 시작·종료하면 그 자리를 벽으로 막는다. if (kind === "ditch") { if (start > 0) pushDitchCap(rows[start], key, side); diff --git a/B05_Profile/B05_Profile_UI_Corridor_Cut.ts b/B05_Profile/B05_Profile_UI_Corridor_Cut.ts index dab0cb47..f718e9ac 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Cut.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Cut.ts @@ -276,6 +276,52 @@ function dropCollinear(loop: ReadonlyArray<[number, number]>): Array<[number, nu return kept; } +/** + * 잘린 자리 **측벽**(2026-08-27 사용자) — 절취 윤곽을 z로 밀어내 성토면에서 원지반까지 + * 세우고, 성토면과 **같은 메쉬**에 담는다(`trimTris`로 함께 그려진다). + * + * ponytail: 벽은 격자 모서리를 타는 윤곽(계단 6cm × 0.2m)을 그대로 쓴다 — 걸친 셀 안에서 + * 실제 절단선은 교점 직선이라 최대 한 셀만큼 어긋난다. 화면에서 틈이 보이면 그때 + * 교점 직선으로 세우면 된다. + */ +function pushSkirt( + loop: ReadonlyArray<[number, number]>, + ribbon: CorridorRibbon, + tris: number[], + uvs: number[], +): void { + const ground = ribbon.groundZ; + if (!ground) return; + const cols = ribbon.colCount; + const startChainage = ribbon.chainages[0]; + /** 격자점 → [x, y, 서피스 z, 지반 z, u]. */ + const at = (row: number, col: number): [number, number, number, number, number] => { + const i = (row * cols + col) * 3; + return [ + ribbon.positions[i], + ribbon.positions[i + 1], + ribbon.positions[i + 2], + ground[row * cols + col], + (ribbon.chainages[row] - startChainage) / 2, + ]; + }; + const push = (x: number, y: number, z: number, u: number): void => { + tris.push(x, y, z); + uvs.push(u, z / 2); // 빗금은 2m마다 한 타일 — 세로도 실척으로 이어진다. + }; + for (let i = 0; i < loop.length - 1; i += 1) { + const [x1, y1, top1, bottom1, u1] = at(loop[i][0], loop[i][1]); + const [x2, y2, top2, bottom2, u2] = at(loop[i + 1][0], loop[i + 1][1]); + if (top1 - bottom1 < 1e-6 && top2 - bottom2 < 1e-6) continue; // 높이 0이면 벽이 없다. + push(x1, y1, top1, u1); + push(x2, y2, top2, u2); + push(x2, y2, bottom2, u2); + push(x1, y1, top1, u1); + push(x2, y2, bottom2, u2); + push(x1, y1, bottom1, u1); + } +} + /** * 성토 리본에 **셀 마스크**(+ 축 고정 커브가 걸친 셀의 조각 삼각형)를 달아 돌려준다. * 원본 정점은 안 건드린다. 아무것도 안 걸리면 리본을 그대로 통과시킨다. @@ -372,6 +418,7 @@ export function maskFillByPlanCurves( traceMaskLoops(rows, cols, mask).forEach((raw) => { const loop = dropCollinear(raw); if (loop.length < 4 || !planes.length) return; + pushSkirt(loop, ribbon, tris, uvs); const meanRow = loop.reduce((sum, [row]) => sum + row, 0) / loop.length; const chainage = ribbon.chainages[Math.min(rows - 1, Math.round(meanRow))]; const plane = planes.reduce((best, item) =>