From 2d897164cc9dee2b377f87dd2ce7d61b0b39abdf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 27 Aug 2026 22:13:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05):=20BOX=EC=95=94=EA=B1=B0=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EC=9E=90=EB=A6=AC=20=EC=9B=90=EC=A7=80?= =?UTF-8?q?=EB=B0=98=EC=9D=84=20=EC=9E=AC=EB=85=B8=EC=B6=9C=ED=95=9C?= =?UTF-8?q?=EB=8B=A4=20(=EC=98=81=EC=97=AD=20=EB=BA=84=EC=85=88=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BOX암거(측점 200.92) 자리에서 구조물이 들어와도 원지반이 드러나지 않던 문제. 원지반 재노출 공식은 `원본 성토선 − (구조물 ∪ 변형 성토선)`인데, BOX는 변형 성토선이 원본과 기본값에서 정확히 collinear가 되어 결과가 공집합이었다. - Station_Structure: StructureSilhouette.endsAtStructure 신설, boxSilhouettes만 true. BOX 실루엣은 "구체 밖으로는 잇지 않는다" 원칙대로 구체 최상단에서 끊긴다. - Plan: endsAtStructure면 slope-deformed를 trimSlopeToGround로 지반까지 외삽하지 않는다. 종전엔 무조건 최대 30m 외삽해 원본 성토선과 겹쳤다. 기하(낙차 0.5m)로 판정하지 않는 이유는 기슭막이 유출측 회귀 때문이다(실측 84.3 우측 5.10 -> 5.69m). - Clip: 재노출 후보에 BOX의 wing-box(날개 사다리꼴) 추가 — 절취 소스에는 있고 되살릴 목록에는 없어 그 자리가 성토면도 지반도 없는 구멍이었다. 세월교는 제외 (그 자리는 되메움 패치가 덮는다). __corridorHoles에 exposeCells/exposeRatio 추가 — 고리 수만으로는 되살릴 영역이 공집합인지 알 수 없었다. - Structures/Structures_Box/Plan_Footprint: planOutline 표식(상판+에이프런)과 outlineLoop 신설. BOX structure 탑뷰 커브를 부재별 7고리가 아니라 아령 외곽선 한 줄로 낸다 — 부재별로 내면 날개벽이 패널 두께 슬리버로 남는다. 기하 불리언은 쓰지 않는다(BOX 부재는 전부 같은 고정축 위 평행 링). - BUILD_VERSION 80 -> 82. 실측(공용 브라우저, git stash 로 코드만 교체): - slope-deformed bbox 200.92 좌 2.85x4.46 -> 2.44x1.02, 우 2.76x3.76 -> 2.42x0.88. 세월교 149.73·배수관 84.3/275.71 은 소수점까지 불변 = 무회귀. - structure 고리 200.92 좌우 각 7 -> 1(면적 5.18 = 상판 1.79 + 에이프런 3.39 정확 일치). 슬리버 0.40x2, 0.15x2 소멸. - __corridorHoles.original 8 -> 12, covered 44 -> 32. - 지형 유지 삼각형 477,153 -> 477,613. - tsc --noEmit 0. 미해결: 날개벽 안쪽 검은 슬리버가 화면에 남아 있고 면이 삐져나온다(사용자 확인). 다음 커밋에서 접근을 바꾼다 — 영역 뺄셈 대신 성토 격자의 groundZ(원지반 표고)를 역마스크로 복사해 도려낸 셀에 그대로 깐다. 이 커밋은 그 되돌림 지점이다. Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_UI_Corridor.ts | 23 ++++- B05_Profile/B05_Profile_UI_Corridor_Clip.ts | 41 ++++++++- B05_Profile/B05_Profile_UI_Corridor_Plan.ts | 18 +++- .../B05_Profile_UI_Corridor_Plan_Footprint.ts | 89 +++++++++++++++++++ ...5_Profile_UI_Corridor_Station_Structure.ts | 19 ++++ .../B05_Profile_UI_Corridor_Structures.ts | 16 ++++ .../B05_Profile_UI_Corridor_Structures_Box.ts | 5 ++ 7 files changed, 203 insertions(+), 8 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index 63dbb273..99ba887b 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -162,7 +162,28 @@ function fnv1a(text: string): string { // 벽 아래 다단·집수정 성토선만 있어 **기슭막이 상단부가 통째로 비었다** // (2026-08-27 사용자). 새 면이 아니라 실루엣이 이미 쓰던 `designTrim` 사면선 // 그대로다. 벽 루프·집수정 분기가 같은 선을 담으므로 측당 한 번만 등록한다. -const BUILD_VERSION = 80; +// 81 = `slope-deformed` 커브를 **패치 리본과 같은 규칙**으로 끝낸다 — 실루엣 바깥 +// 끝이 그 측점 지반에 안 닿으면(BOX암거: 구체 최상단) 외삽하지 않고 실루엣이 +// 끝나는 자리를 바깥 끝으로 쓴다. 종전에는 이 커브만 무조건 최대 30m 외삽해 +// 성토 끝선까지 갔고, BOX는 기본값에서 변형 성토선이 원본 성토선과 정확히 +// collinear가 되어 `원본 − (구조물 ∪ 변형)` = ∅ — **구체 밖 원지반 재노출이 +// 통째로 미실행**이었다(2026-08-27 사용자 확정: "구체 밖은 원지반이어야 한다"). +// 실루엣이 구조물에서 끝나는지는 기하로 재지 않고 `endsAtStructure` 표식으로 +// 가른다 — 낙차로 재면 기슭막이 유출측처럼 지반보다 바깥으로 나간 실루엣까지 +// 걸려 원래 짧게 잘리던 것이 안 잘린다(실측 84.3 우측 5.10 → 5.69m). +// 아울러 BOX 날개 사다리꼴(`wing-box`)을 원지반 재노출 후보에 넣는다 — 절취 +// 소스에는 있고 되살릴 목록에는 없어 그 자리가 성토면도 지반도 없는 구멍이었다 +// (2026-08-27 사용자: "날개부의 평면 스케치 영역까지 원지반이 같이 노출"). +// 세월교는 제외 — 그 자리는 되메움 패치가 덮는다(`wingRangeAt`, isFord 전용). +// 82 = BOX암거 `structure` 탑뷰 커브를 부재별 7고리가 아니라 **아령 외곽선 한 줄**로 +// 낸다(2026-08-27 사용자: "바닥을 위에서 보면 아령형상이고 제일 외곽에 있는 선 +// 성분을 평면 투영하면 경계"). 상판+에이프런에 `planOutline` 표식을 달고 +// `_Plan_Footprint.ts`가 링 자리마다 편거리 창을 합친다 — 기하 불리언 없음 +// (BOX 부재는 전부 같은 고정축 위 평행 링이다). 종전 부재별 고리에는 날개벽이 +// **패널 두께 슬리버**로 섞여 있었고(실측 0.40㎡ / bbox 1.69×1.40 ×2), 그 슬리버가 +// "덮인 자리"로 빠져 그 자리만 성토면도 지형도 없는 검은 줄이 됐다. 에이프런은 +// 날개벽 발치를 얹으려고 넓혀 둔 부재라 외곽선이 날개를 이미 품는다. +const BUILD_VERSION = 82; /** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Clip.ts b/B05_Profile/B05_Profile_UI_Corridor_Clip.ts index 242e752a..5315b9f3 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Clip.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Clip.ts @@ -22,6 +22,7 @@ import * as THREE from "three"; import type { ModelBounds } from "./B05_Profile_UI_Markers"; import type { CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build"; +import type { PlanCurve } from "./B05_Profile_UI_Corridor_Plan"; import type { TerrainBandSplit } from "./B05_Profile_UI_Corridor_Split"; import type { P2, PlanRegion, RegionSegment } from "./B05_Profile_UI_Corridor_Region"; import { @@ -385,17 +386,36 @@ function withStructureHoles( build: CorridorBuildResult, bounds: ModelBounds, ): PlanRegion { - const loopsOf = (sources: ReadonlyArray): Array> => { + const loopsOf = ( + sources: ReadonlyArray, + only?: (curve: PlanCurve) => boolean, + ): Array> => { const loops: Array> = []; for (const curve of build.planCurves ?? []) { if (!sources.includes(curve.source)) continue; + if (only && !only(curve)) continue; for (const loop of curve.loops) { if (loop.length >= 4) loops.push(loop.map(([x, y]) => [x, y] as [number, number])); } } return loops; }; - const originalLoops = loopsOf(["slope-original"]); + /** + * BOX암거 **날개 사다리꼴**(`wing-box`)도 되살릴 후보에 넣는다(2026-08-27 사용자: + * "박스암거의 날개부의 평면 스케치 영역까지 원지반이 같이 노출되어야 함"). + * + * 그 자리는 절취 소스(`_Corridor_Cut.ts` `CUT_SOURCES`)에 들어 있어 성토면이 + * 파이는데, 되살릴 후보 목록(`slope-original`)에는 없어 **성토면도 원지반도 없는 + * 구멍**으로 남았다. 지우는 이름 목록과 되살리는 이름 목록이 어긋난 것이다. + * + * 세월교는 넣지 않는다 — 그 측 날개 자리는 되메움 패치가 덮는다(`_Corridor_Carve.ts` + * `wingRangeAt`, `isFord` 전용). 지형까지 올리면 패치면과 같은 높이에서 겹쳐 + * z-fight가 난다. `axisLocked`가 곧 BOX 표식이다(`_Corridor_Plan.ts` `boxAxis`). + */ + const originalLoops = [ + ...loopsOf(["slope-original"]), + ...loopsOf(["wing-box"], (curve) => curve.axisLocked === true), + ]; if (!originalLoops.length) return strip; const original = new PlanLoopRegion(originalLoops, bounds); if (original.isEmpty) return strip; @@ -404,10 +424,25 @@ function withStructureHoles( const covered = coveredLoops.length ? new PlanLoopRegion(coveredLoops, bounds) : null; const expose: PlanRegion = covered && !covered.isEmpty ? new SubtractedRegion(original, [covered]) : original; - // 검증용 요약(화면 밖 수치 확인) — 고리 수. `__corridorClip`과 같은 용도. + // 검증용 요약(화면 밖 수치 확인) — `__corridorClip`과 같은 용도. **고리 수만으로는 + // 되살릴 영역이 실제로 남았는지 알 수 없다**: BOX암거에서 `covered`가 `original`을 + // 통째로 삼켜 공집합이 됐는데도 이 훅은 정상값을 찍었다(2026-08-27). 되살릴 영역을 + // 격자로 찍어 실적을 함께 담는다 — 코리도 빌드당 한 번이라 비용은 무시할 만하다. + const SAMPLES = 80; + let inside = 0; + for (let i = 0; i < SAMPLES; i += 1) { + const x = original.minX + ((original.maxX - original.minX) * (i + 0.5)) / SAMPLES; + for (let j = 0; j < SAMPLES; j += 1) { + const z = original.minZ + ((original.maxZ - original.minZ) * (j + 0.5)) / SAMPLES; + if (expose.contains({ x, z })) inside += 1; + } + } (window as unknown as { __corridorHoles?: unknown }).__corridorHoles = { original: originalLoops.length, covered: coveredLoops.length, + /** 되살릴 영역에 든 표본 점 수 / 전체 표본(원본 bbox 격자 80×80). 0이면 공집합. */ + exposeCells: inside, + exposeRatio: Number((inside / (SAMPLES * SAMPLES)).toFixed(4)), }; return new SubtractedRegion(strip, [expose]); } diff --git a/B05_Profile/B05_Profile_UI_Corridor_Plan.ts b/B05_Profile/B05_Profile_UI_Corridor_Plan.ts index bcdda845..e1c179cf 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Plan.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Plan.ts @@ -502,10 +502,10 @@ export function buildPlanCurves( const inner = silhouette.widenTo?.offset_m ?? roadEdge; const from = section.chainage_m - silhouette.span.beforeM; const to = section.chainage_m + silhouette.span.afterM; + const stationGround = groundInterpolator(section.samples); // "auto" = 실루엣 가운데가 지반보다 높으면 성토, 낮으면 절토(패치 리본과 같은 판정). let kind: "cut" | "fill" = silhouette.kind === "auto" ? "fill" : silhouette.kind; if (silhouette.kind === "auto") { - const stationGround = groundInterpolator(section.samples); const mid = silhouette.points[Math.floor(silhouette.points.length / 2)]; if (stationGround && mid.elevation_m < stationGround(mid.offset_m) - 0.05) kind = "cut"; } @@ -523,9 +523,19 @@ export function buildPlanCurves( offset_m: point.offset_m, elevation_m: point.elevation_m + shift, })); - const trimmed = trimSlopeToGround(placed, kind, ground); - if (!trimmed) return null; - const outer = trimmed[trimmed.length - 1].offset_m; + // **구조물에서 끝나는 실루엣**(BOX암거 — 구체 최상단 모서리)은 지반까지 + // 연장하지 않는다. 실루엣이 끝나는 자리가 곧 변형 성토면이 끝나는 자리이고, + // 그 바깥은 구조물 솔리드와 **원지반** 몫이다(2026-08-27 사용자 확정). + // 연장하면 변형 성토선이 원본 성토선과 겹쳐 원지반 재노출 공식 + // `원본 − (구조물 ∪ 변형)`이 공집합이 된다(`_Corridor_Clip.ts`). + let outer: number; + if (silhouette.endsAtStructure) { + outer = placed[placed.length - 1].offset_m; + } else { + const trimmed = trimSlopeToGround(placed, kind, ground); + if (!trimmed) return null; + outer = trimmed[trimmed.length - 1].offset_m; + } if ((outer - inner) * sign <= MIN_WIDTH_M) return null; return [at(frame, inner), at(frame, outer)]; }); diff --git a/B05_Profile/B05_Profile_UI_Corridor_Plan_Footprint.ts b/B05_Profile/B05_Profile_UI_Corridor_Plan_Footprint.ts index 0394434e..a5360492 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Plan_Footprint.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Plan_Footprint.ts @@ -98,6 +98,90 @@ function wingSide(rings: ReadonlyArray, axis: StationAxis): Side return routeOffset(axis, middle.cx, middle.cy) >= 0 ? "left" : "right"; } +/** 링 한 줄 + 그 링의 **축 방향 좌표** — 부재끼리 링 자리를 맞추는 기준이다. */ +interface AxisRow extends Row { + t: number; +} + +/** 고정축을 따라 잰 좌표 — 좌향 벡터에 수직인 방향. 부호는 일관되기만 하면 된다. */ +function axisCoord(axis: StationAxis, cx: number, cy: number): number { + return (cx - axis.cx) * axis.leftY - (cy - axis.cy) * axis.leftX; +} + +/** 그 부재의 축 좌표 `t`에서의 편거리 창 — 링 사이는 선형 보간, 범위 밖은 null. */ +function spanAt(rows: ReadonlyArray, t: number): Span | null { + if (t < rows[0].t - EPS_M || t > rows[rows.length - 1].t + EPS_M) return null; + for (let i = 0; i < rows.length - 1; i += 1) { + const a = rows[i]; + const b = rows[i + 1]; + if (t < a.t - EPS_M || t > b.t + EPS_M) continue; + const width = b.t - a.t; + const k = width > EPS_M ? (t - a.t) / width : 0; + return [a.span[0] + (b.span[0] - a.span[0]) * k, a.span[1] + (b.span[1] - a.span[1]) * k]; + } + return rows[rows.length - 1].span; +} + +/** + * **아령 외곽선**(2026-08-27 사용자) — `planOutline` 표식이 붙은 부재(BOX암거 상판 + + * 에이프런)를 하나로 합친 탑뷰 경계 고리. 표식이 없으면 `null`. + * + * 사용자 원문: "박스암거의 경우에는 바닥을 위에서 보면 아령형상이고 제일 외곽에 있는 + * 선 성분을 가져가 평면 투영하면 경계가 되고 이걸 경계로 사용하면 가능할 것 같은데?" + * + * 기하 불리언은 여전히 쓰지 않는다(옛 `unionOutline`이 겹친 스트립에서 무너진 이유는 + * 이 파일 머리말 참조). BOX 부재는 전부 **같은 고정축 위 평행 링**이라, 링 자리마다 + * 각 부재의 편거리 창을 재서 **제일 바깥·제일 안쪽**을 취하면 그게 곧 외곽선이다. + * 퇴화(겹침·포함·맞닿은 변)가 생길 여지가 없다. + */ +function outlineLoop( + set: ReadonlyArray, + side: Side, + roadEdgeOffset: number, + planZ: number, + axis: StationAxis, +): Array<[number, number, number]> | null { + const members: AxisRow[][] = []; + for (const structure of set) { + if (!structure.planOutline) continue; + const rings = structure.rings; + if (!rings || rings.length < 2) continue; + const rows: AxisRow[] = []; + rings.forEach((frame, index) => { + const span = polygonSpan(structure.polygons?.[index] ?? structure.polygon); + if (span) rows.push({ frame, span, t: axisCoord(axis, frame.cx, frame.cy) }); + }); + if (rows.length >= 2) members.push(rows.sort((a, b) => a.t - b.t)); + } + if (!members.length) return null; + // 링 자리는 부재마다 다르다(상판 ±halfSpan·±innerHalf, 에이프런 ±reach·±halfSpan) — + // 전부 모아 그 자리마다 창을 합친다. 프레임은 그 자리에 실제로 링이 있는 부재 것을 + // 그대로 쓴다(전부 같은 축이라 프레임 보간이 필요 없다). + const stops = new Map(); + for (const rows of members) { + for (const row of rows) { + const key = Math.round(row.t / EPS_M) * EPS_M; + if (!stops.has(key)) stops.set(key, row.frame); + } + } + const rows: Row[] = []; + for (const t of [...stops.keys()].sort((a, b) => a - b)) { + let lo = Infinity; + let hi = -Infinity; + for (const member of members) { + const span = spanAt(member, t); + if (!span) continue; + if (span[0] < lo) lo = span[0]; + if (span[1] > hi) hi = span[1]; + } + if (hi - lo <= EPS_M) continue; + const clipped = clipOutsideRoad([lo, hi], side, roadEdgeOffset); + const frame = stops.get(t); + if (clipped && frame) rows.push({ frame, span: clipped }); + } + return runLoop(rows, side, planZ); +} + /** * 구조물 세트의 그 측 탑뷰 외곽 고리들 — **부재마다 따로**, 도로부(노견 안쪽)는 뺀다. * @@ -114,6 +198,11 @@ export function structureFootprintLoops( planZ: number, axis: StationAxis, ): Array> { + // 아령 외곽선을 낼 수 있는 세트(BOX암거)는 **그 한 줄만** 낸다 — 부재별 고리를 + // 함께 내면 날개 패널 슬리버가 다시 섞여 지형 재노출에 구멍을 만든다. + // 저판·측벽은 상판 폭 안, 날개는 에이프런 안이라 외곽선이 전부 품는다. + const outline = outlineLoop(set, side, roadEdgeOffset, planZ, axis); + if (outline) return [outline]; const loops: Array> = []; for (const structure of set) { if (structure.kind === "pipe") continue; diff --git a/B05_Profile/B05_Profile_UI_Corridor_Station_Structure.ts b/B05_Profile/B05_Profile_UI_Corridor_Station_Structure.ts index 35cf6600..c96fb326 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Station_Structure.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Station_Structure.ts @@ -67,6 +67,22 @@ export interface StructureSilhouette { * 솟는다. 세월교만 싣는다(다른 구조물은 안쪽 확장 대상이 아니다). */ floorTopZ?: number; + /** + * 실루엣이 **구조물에서 끝나는가**(2026-08-27 사용자 확정 — BOX암거 "구체 밖은 + * 원지반이어야 한다"). BOX는 구체 최상단 모서리에서 선을 끊는다 — 2D 횡단도가 + * 거기서 끝나고 그 아래·바깥은 구체·에이프런·날개벽과 **원지반** 몫이기 때문이다. + * + * 이 표식이 붙은 실루엣은 평면 변형 성토선(`_Corridor_Plan.ts` `slope-deformed`)을 + * 낼 때 `trimSlopeToGround`로 지반까지 **연장하지 않는다**. 연장하면 변형 성토선이 + * 원본 성토선과 겹쳐(BOX 기본값에선 정확히 collinear) 원지반 재노출 공식 + * `원본 − (구조물 ∪ 변형)`이 공집합이 되고, 구체 바깥이 성토면도 원지반도 없는 + * 빈 자리로 남는다. + * + * 기하로 판정하지 않는 이유: "바깥 끝이 지반에 닿는가"로 재면 기슭막이 유출측처럼 + * **지반보다 바깥으로 나간** 실루엣까지 걸려, 원래 짧게 잘리던 것이 안 잘린다 + * (실측 84.3 우측 폭 5.10 → 5.69m 확대). 끊는 게 설계인 구조물만 명시적으로 단다. + */ + endsAtStructure?: boolean; } /** 실루엣에서 노견 확장점 찾기 — 첫 점(노견) 표고 그대로 바깥으로 나간 가장 먼 점. */ @@ -122,6 +138,9 @@ export function boxSilhouettes(section: CrossSection): StructureSilhouette[] { widenTo: widenToOf(points, side.role), // 구체 최상단에서 끝나는 성토선 한 줄 — BOX는 다단이 없다. fillRuns: [points], + // 선이 구체 최상단에서 끊긴다 — 그 바깥은 원지반 몫이므로 평면 변형 + // 성토선도 여기서 끝내야 한다(연장하면 원지반 재노출이 상쇄된다). + endsAtStructure: true, }, ]; }); diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts index 834ff200..3126ac41 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts @@ -82,6 +82,22 @@ export interface CorridorStructure { * `"route"` = 측벽 끝단부 자리의 노선 프레임을 탄 날개(세월교). */ wing?: "station" | "route"; + /** + * 탑뷰 **외곽 경계**를 이루는 부재(2026-08-27 사용자) — BOX암거 **상판 + 에이프런**. + * + * 위에서 보면 상판(구체 폭 직사각형)과 양쪽 에이프런(날개 각도만큼 벌어진 + * 사다리꼴)이 **아령 형상**을 이루고, 그 제일 바깥 선이 곧 구조물이 지면을 덮는 + * 경계다(사용자: "바닥을 위에서 보면 아령형상이고 제일 외곽에 있는 선 성분을 + * 평면 투영하면 경계"). 표식이 붙은 부재들은 `_Corridor_Plan_Footprint.ts`가 + * 링별로 편거리 창을 합쳐 **고리 한 줄**로 낸다. + * + * 부재별로 따로 내면 날개벽이 **패널 두께짜리 슬리버**로 남고, 그 슬리버가 + * 지형 재노출 공식에서 "덮인 자리"로 빠져 그 자리만 성토면도 지형도 없는 + * 구멍이 됐다(2026-08-27 실측: 면적 0.40㎡ / bbox 1.69×1.40 대각 슬리버 ×2). + * 에이프런은 "날개벽 발치가 바닥에 얹히도록" 그만큼 넓혀 둔 부재라 날개 + * 풋프린트를 이미 품는다 — 아령 외곽선 하나면 슬리버가 사라진다. + */ + planOutline?: boolean; /** * 성토끝 박스 옆선을 그을 **바깥 방향**(단위벡터, 좌향 기준) — 세월교 전용. * 없으면 기준 측점의 좌향을 쓴다(BOX암거 현행 — 2026-08-26 사용자 "박스 암거는 diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts index d85f6dad..ffeea745 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts @@ -183,6 +183,8 @@ export function boxSolids( kind: "basin", polygons: repeat(layout.topSlab, bodyRings.length), rings: bodyRings, + // 아령 외곽선의 **손잡이** — 에이프런과 합쳐 탑뷰 경계를 이룬다(2026-08-27 사용자). + planOutline: true, }, { chainage_m: chainage, @@ -247,6 +249,9 @@ export function boxSolids( chainage, stationFrame, ), + // 아령 외곽선의 **원판** — 날개벽 발치를 품는 부재라 이것만 있으면 날개 + // 슬리버를 따로 낼 필요가 없다(2026-08-27 사용자). + planOutline: true, }); }