From 0f5b4e4df7935a5da03e78472a427c15541467d2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 18:31:54 +0900 Subject: [PATCH 1/4] auto: 2026-09-03 18:31 (ESD_LAPTOP) --- .../B07_DesignDetail_UI_Cad_Structures.ts | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Cad_Structures.ts b/B07_DesignDetail/B07_DesignDetail_UI_Cad_Structures.ts index 89a249f0..fa0cef2d 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Cad_Structures.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Cad_Structures.ts @@ -323,20 +323,55 @@ function circlePoints(center: XY, radius: number): XY[] { return points; } +/** + * `rect` → 닫힌 네 모서리 점열(회전 transform 적용). + * + * 돌쌓기 돌·돌망태 칸(`B06_Section_UI_Cross_Wall_Hatch.cell()`)은 `rect` 하나에 + * `rotate(벽기울기 cx cy)` 를 걸어 그린다. 수확기가 `rect` 를 안 보던 때는 형태 해칭 + * 80개 중 **49개(돌·칸 전부)가 CAD 에 안 실렸다**(2026-09-03 실측). 모서리 반경(`rx`)은 + * 무시한다 — 도면에서는 각진 칸으로 충분하다. + */ +function rectPoints(element: SVGElement): XY[] { + const x = attr(element, "x"); + const y = attr(element, "y"); + const width = attr(element, "width"); + const height = attr(element, "height"); + const corners: XY[] = [ + [x, y], + [x + width, y], + [x + width, y + height], + [x, y + height], + [x, y], + ]; + const rotate = /rotate\(\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+(-?[\d.]+)\s*\)/.exec( + element.getAttribute("transform") ?? "", + ); + if (!rotate) return corners.map(([cx, cy]) => flip(cx, cy)); + const angle = (Number(rotate[1]) * Math.PI) / 180; + const [ox, oy] = [Number(rotate[2]), Number(rotate[3])]; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return corners.map(([cx, cy]) => { + const dx = cx - ox; + const dy = cy - oy; + return flip(ox + dx * cos - dy * sin, oy + dx * sin + dy * cos); + }); +} + /** 오프스크린 SVG에 그려진 도형을 CAD 엔티티로 옮긴다 (블록 테두리 안으로 자른다). */ function harvest(root: SVGElement, style: Style, frame: number[]): Entity[] { const [fx0, , fx1] = frame; const entities: Entity[] = []; const segments: XY[][] = []; const inside = (px: number): boolean => px >= fx0 && px <= fx1; - const nodes = root.querySelectorAll("polygon, polyline, line, circle, text"); + const nodes = root.querySelectorAll("polygon, polyline, line, circle, rect, text"); for (const element of Array.from(nodes)) { if (element.closest(SKIP_SELECTOR)) continue; const tag = element.tagName.toLowerCase(); // 클립 그룹 안의 해칭은 경계로 **잘라서** 싣는다 — CAD 에는 클립이 없다. const clip = clipPolygonOf(element); - if (tag === "polygon" || tag === "polyline") { - const points = parsePoints(element); + if (tag === "polygon" || tag === "polyline" || tag === "rect") { + const points = tag === "rect" ? rectPoints(element) : parsePoints(element); if (tag === "polygon" && points.length > 2) points.push(points[0]); const runs = clip ? clipPointsToPolygon(points, clip) : [points]; for (const run of runs) { From c21b68f2ced2042de7daffc9fca2e4f53eb2b59b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 19:16:46 +0900 Subject: [PATCH 2/4] =?UTF-8?q?@=20feat(B06):=20=ED=9A=A1=EB=8B=A8=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=A0=9C=EB=AA=A9=ED=96=89=EC=97=90=20?= =?UTF-8?q?=EC=84=B1=ED=86=A0=EC=82=AC=EB=A9=B4=20=EB=B3=B8=EB=9E=98=20?= =?UTF-8?q?=EA=B2=BD=EC=82=AC=EA=B8=B8=EC=9D=B4=20=ED=91=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사면 미교차 경고가 붙는 제목행 우측 끝에 성토사면 경사길이를 표기함. 값은 정본 설계선(`design_line`) 기준이라 기슭막이·집수정이 사면을 끊기 전 **본래 길이**이고, 양측 성토는 좌·우 두 값을 다 냄. - `fillSlopeLengths()` 신설 — 기존 `protectedSpan`·`meetOffset` 재사용, 성토측만 노견 끝(측구가 있으면 측구 바깥)에서 사면 끝까지 `run × √(1+1/n²)`. - 미교차 측점은 `≥` 하한값 — 추세 외삽은 지반이 사면과 나란한 자리에서 수십 m 씩 튀어(740m 측점 101.53m) 길이 표기에 못 씀. - `meetOffset` 교차점을 두 걸음 사이 선형보간으로 냄 — 반폭 맞춤(1m 올림)에는 영향이 없고 길이 표기가 탐색 간격만큼 튀는 것만 막음. Co-Authored-By: Claude Opus 5 (1M context) @ --- .../B06_Section_UI_Cross_Card_Chrome.ts | 32 ++++++++++ B06_Section/B06_Section_UI_Cross_Fit.ts | 59 ++++++++++++++++++- B06_Section/B06_Section_UI_Style_Cross.css | 6 ++ ui_template/ui_template_locale_b2.ts | 6 ++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts index 4d57f093..4f91214c 100644 --- a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts +++ b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts @@ -13,8 +13,36 @@ import { buildRockBoundaryControl, sectionModeLabel, } from "./B06_Section_UI_Cross_Design"; +import { type FillSlopeLength, fillSlopeLengths } from "./B06_Section_UI_Cross_Fit"; import { type DesignChangeHandler, L, stationLabel } from "./B06_Section_UI_Section_Common"; +/** + * 성토사면 경사길이 표기 칸 — 성토측이 없으면 null. + * + * 값은 **구조물이 끊기 전 본래 사면**(정본 설계선 기준)이라, 기슭막이·집수정이 선 측점도 + * 표준 횡단의 사면 길이를 그대로 보여 준다(2026-09-03 사용자 지시). + */ +function fillSlopeInfo(section: CrossSection): HTMLElement | null { + const lengths = fillSlopeLengths(section); + const sides = (["left", "right"] as const).filter((side) => lengths[side] !== null); + if (!sides.length) return null; + const both = sides.length > 1; + const info = document.createElement("span"); + info.className = "b06-cross-card__fillslope"; + info.textContent = `${L("B06_Cross_FillSlope")} ${sides + .map((side) => { + const length = lengths[side] as FillSlopeLength; + // 계산 반폭 안에서 원지반을 못 만난 사면은 거기까지만 잰 하한값이라 「≥」로 구분한다. + const value = `${length.open ? "≥" : ""}${length.lengthM.toFixed(2)}m`; + if (!both) return value; + const label = L(side === "left" ? "B06_Design_Ditch_Left" : "B06_Design_Ditch_Right"); + return `${label} ${value}`; + }) + .join(" · ")}`; + info.title = L("B06_Cross_FillSlope_Tip"); + return info; +} + /** * 카드 제목행을 만들어 카드에 붙인다(설계 조작이 있으면 조작 바까지). * @@ -67,6 +95,10 @@ export function appendCardHeader( ); meta.append(kind); } + // 성토사면 경사길이는 **행 우측 끝**(2026-09-03 사용자 지시) — meta 가 제목행의 오른쪽 + // 끝이므로 그 마지막 칸에 붙인다. + const fillSlope = fillSlopeInfo(section); + if (fillSlope) meta.append(fillSlope); // 단면유형 pill은 지반유형 우측·우측 맞춤으로 배치(1번). 좌측 구분선은 지반유형 세그먼트에 준다(3번). modePill.classList.add("b06-cross-card__mode--right"); diff --git a/B06_Section/B06_Section_UI_Cross_Fit.ts b/B06_Section/B06_Section_UI_Cross_Fit.ts index 3814d143..ac6cd84a 100644 --- a/B06_Section/B06_Section_UI_Cross_Fit.ts +++ b/B06_Section/B06_Section_UI_Cross_Fit.ts @@ -58,6 +58,58 @@ export function toeFitHalfWidth(section: CrossSection): number | null { return Math.min(Math.ceil(extent + 1), MAX_FIT_HALF_WIDTH_M); } +/** + * 성토측 사면의 **본래** 경사길이(m) — 성토가 아닌 측은 null (2026-09-03 사용자 지시). + * + * 「본래」 = 구조물(기슭막이·집수정)이 사면을 끊기 전 표준 횡단 기준. 구조물은 프론트 + * 오버레이라 백엔드 `design_line`을 바꾸지 않으므로, 그 선으로 재면 곧 본래 길이다. + * 구간은 노견 끝(그 측에 측구가 있으면 측구 바깥)부터 설계선이 원지반과 처음 만나는 + * 점까지이고, 그 사이 성토선은 1:n 직선이라 수평 성분만으로 경사길이가 나온다. + */ +export interface FillSlopeLength { + /** 사면 경사길이(m). 미교차(`open`)면 계산 반폭까지의 **하한값**이다. */ + lengthM: number; + /** 설계선 끝(계산 반폭)까지 원지반과 만나지 못한 사면 — 길이를 다 재지 못했다. */ + open: boolean; +} + +export function fillSlopeLengths(section: CrossSection): { + left: FillSlopeLength | null; + right: FillSlopeLength | null; +} { + const lengths: { left: FillSlopeLength | null; right: FillSlopeLength | null } = { + left: null, + right: null, + }; + const design = section.design; + if (!design) return lengths; + const groundAt = groundInterpolator(section.samples); + const designAt = designInterpolator(design.design_line); + const edges = design.road_edges; + if (!groundAt || !designAt || !edges) return lengths; + const lineOffsets = design.design_line.map((point) => point.offset_m); + if (lineOffsets.length < 2) return lengths; + const { protectMax, protectMin } = protectedSpan(design, edges); + const slant = Math.hypot(1, 1 / Math.max(design.fill_slope_ratio, 1e-6)); + for (const side of ["left", "right"] as const) { + const mode = design.section_mode; + if (mode === "both_cut" || mode === `${side}_cut`) continue; + const outward = side === "left" ? 1 : -1; + const start = side === "left" ? protectMax : protectMin; + const limit = side === "left" ? Math.max(...lineOffsets) : Math.min(...lineOffsets); + // 미교차 측점은 **재지 못한 것**이라 계산 반폭까지의 하한값만 준다 — `toeFitHalfWidth`가 + // 쓰는 추세 외삽은 지반이 사면과 거의 나란한 자리에서 수십 m씩 튀어 길이 표기에는 못 쓴다 + // (2026-09-03 실측: 740m 측점 좌측 외삽 101.53m, 실제 반폭 20m까지 고저차 7.16m 유지). + const meet = meetOffset(designAt, groundAt, start, limit, outward); + const end = Math.abs(meet) > Math.abs(limit) ? limit : meet; + lengths[side] = { + lengthM: Math.abs(end - start) * slant, + open: Math.abs(designAt(end) - groundAt(end)) > MEET_TOLERANCE_M, + }; + } + return lengths; +} + /** 노면·노견(+측구) 구간의 바깥 경계 — 교차점 탐색은 여기서부터 바깥으로 간다. */ function protectedSpan( design: CrossDesign, @@ -98,7 +150,12 @@ function meetOffset( const offset = startOffset + outward * Math.min(SCAN_STEP_M * index, span); const diff = designAt(offset) - groundAt(offset); if (Math.abs(diff) <= MEET_TOLERANCE_M) return offset; - if (previousDiff !== 0 && Math.sign(diff) !== Math.sign(previousDiff)) return offset; + if (previousDiff !== 0 && Math.sign(diff) !== Math.sign(previousDiff)) { + // 두 걸음 사이를 선형보간한다 — 반폭 맞춤에는 영향이 없지만(1m 올림), 같은 교차점을 + // 길이로 읽는 성토사면 표기가 탐색 간격(0.1m)만큼 튀는 것을 막는다(2026-09-03). + const previous = offset - outward * SCAN_STEP_M; + return previous + (offset - previous) * (previousDiff / (previousDiff - diff)); + } previousDiff = diff; } return extrapolateMeet(designAt, groundAt, limitOffset, outward); diff --git a/B06_Section/B06_Section_UI_Style_Cross.css b/B06_Section/B06_Section_UI_Style_Cross.css index ee013e19..2cf43267 100644 --- a/B06_Section/B06_Section_UI_Style_Cross.css +++ b/B06_Section/B06_Section_UI_Style_Cross.css @@ -238,6 +238,12 @@ white-space: nowrap; } +/* 성토사면 경사길이 — 제목행 우측 끝(2026-09-03). meta 의 마지막 칸이라 우측 맞춤이다. */ +.b06-cross-card__fillslope { + color: var(--color-text-muted); + white-space: nowrap; +} + /* 지반유형 세그먼트(D-6): 제목행 1행 내 인라인 배치. 좌측 구분선(3번). */ .b06-design__seg--header { flex: 0 0 auto; diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 893a1a3f..5877b4ab 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -348,6 +348,12 @@ export const ui_locales_b2 = { "사면이 계산 반폭 끝까지 원지반과 만나지 않아 절·성토 면적이 그 자리에서 잘렸습니다. 유토곡선·수량도 잘린 값을 씁니다.", "The slope never meets existing ground within the computed half-width, so the cut/fill area is truncated there. The mass haul curve and quantities use the truncated value.", ], + /* 성토사면 경사길이 — 구조물이 사면을 끊기 전 **본래** 길이(2026-09-03 사용자 지시). */ + B06_Cross_FillSlope: ["성토사면", "Fill slope"], + B06_Cross_FillSlope_Tip: [ + "노견 끝(그 측에 측구가 있으면 측구 바깥)부터 성토 사면이 원지반과 만나는 곳까지의 경사길이입니다. 기슭막이·집수정이 사면을 끊기 전 본래 길이이며, 양측 성토면 좌·우를 함께 적습니다. 5m를 넘으면 옹벽·석축 등이 필요합니다(성토_비탈면 §2). 「≥」는 사면이 계산 반폭 안에서 원지반과 만나지 못해 거기까지만 잰 하한값입니다.", + "Slant length of the fill slope, from the shoulder edge (outside the ditch if that side has one) to where it meets existing ground. It is the original slope before any revetment or catch basin cuts it short, and both sides are shown when the section is filled on both. Over 5 m a retaining structure is required. A leading ≥ marks a lower bound: the slope never meets ground within the computed half-width, so only that much could be measured.", + ], B06_MassHaul_CutNatural: ["절토(자연)", "Cut (natural)"], B06_MassHaul_CutCompacted: ["절토(다짐환산)", "Cut (compacted)"], B06_MassHaul_Fill: ["성토", "Fill"], From f772401b0665db530160518e1925bcf2627a04a8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 19:28:10 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(B06):=20=ED=9A=A1=EB=8B=A8=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20pill=20=EC=9D=84=20?= =?UTF-8?q?=EB=B0=B0=EC=88=98=EA=B4=80=20=ED=91=9C=EA=B8=B0=EB=A1=9C=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관종·관경(`파형강관 D1200`) 대신 표시 이름 `배수관` 하나만 적음 — 2026-08-17 확정한 `PIPE_DISPLAY_NAME` 규칙과 같은 표기. 관종·관경은 툴팁에 남김. 판정은 `PIPE_TYPES` 목록으로 하고 저장 라벨은 손대지 않음 — 재진입 이관이 라벨 문자열로 구조물 종류를 되짚기 때문임. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Cross_Card_Chrome.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts index 4f91214c..a8300581 100644 --- a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts +++ b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts @@ -6,6 +6,8 @@ * 만든다. 여기 있는 것은 전부 **읽어서 붙이는 값**이라 카드 상태(줌·선택)를 건드리지 않는다. * ========================================================================== */ +import { PIPE_TYPES } from "@config/config_frontend"; +import { PIPE_DISPLAY_NAME } from "../B05_Profile/B05_Profile_UI_IrregularStations"; import type { CrossSection } from "./B06_Section_Api_Fetch"; import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design"; import { @@ -80,11 +82,16 @@ export function appendCardHeader( openSlope.title = L("B06_Cross_SlopeUnclosed_Tip"); meta.append(openSlope); } - if (section.structure) { + const structureName = section.structure; + if (structureName) { const structure = document.createElement("span"); structure.className = "b06-cross-card__structure"; - structure.textContent = section.structure; - structure.title = `구조물: ${section.structure}`; + // 배수관은 관종·관경(`파형강관 D1200`)이 아니라 표시 이름 하나로 적는다 + // (2026-09-03 사용자 지시 — 2026-08-17 확정한 `PIPE_DISPLAY_NAME` 규칙과 같다). + // 관종·관경은 툴팁에 남긴다. + const isPipe = PIPE_TYPES.some((kind) => structureName.startsWith(`${kind} D`)); + structure.textContent = isPipe ? PIPE_DISPLAY_NAME : structureName; + structure.title = `구조물: ${structureName}`; meta.append(structure); } // 측점 종류는 시·종점(BP/EP)만 적는다 — "일반측점"은 대다수라 정보가 없다(2026-08-02 사용자 지시). From b908a0b10d15b229a64d8e06e9e4acfa077b8d6c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 19:35:21 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat(B06):=20=EC=A2=85=EB=8B=A8=EB=A9=B4?= =?UTF-8?q?=EB=8F=84=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EB=9D=BC=EB=B2=A8?= =?UTF-8?q?=EB=8F=84=20=EB=B0=B0=EC=88=98=EA=B4=80=20=ED=91=9C=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관종·관경(`파형강관 D1200`) 대신 `배수관` 을 적음 — 횡단 카드와 같은 규칙. 판정을 `structureDisplayName()`(`_UI_Section_Common`) 한 곳으로 모아 두 렌더러가 같은 문자열을 쓰게 함. 저장 라벨은 그대로 — 재진입 이관이 라벨로 종류를 되짚음. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Cross_Card_Chrome.ts | 15 ++++++++------- B06_Section/B06_Section_UI_Longitudinal.ts | 4 +++- B06_Section/B06_Section_UI_Section_Common.ts | 13 +++++++++++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts index a8300581..d7cd8c5c 100644 --- a/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts +++ b/B06_Section/B06_Section_UI_Cross_Card_Chrome.ts @@ -6,8 +6,6 @@ * 만든다. 여기 있는 것은 전부 **읽어서 붙이는 값**이라 카드 상태(줌·선택)를 건드리지 않는다. * ========================================================================== */ -import { PIPE_TYPES } from "@config/config_frontend"; -import { PIPE_DISPLAY_NAME } from "../B05_Profile/B05_Profile_UI_IrregularStations"; import type { CrossSection } from "./B06_Section_Api_Fetch"; import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design"; import { @@ -16,7 +14,12 @@ import { sectionModeLabel, } from "./B06_Section_UI_Cross_Design"; import { type FillSlopeLength, fillSlopeLengths } from "./B06_Section_UI_Cross_Fit"; -import { type DesignChangeHandler, L, stationLabel } from "./B06_Section_UI_Section_Common"; +import { + type DesignChangeHandler, + L, + stationLabel, + structureDisplayName, +} from "./B06_Section_UI_Section_Common"; /** * 성토사면 경사길이 표기 칸 — 성토측이 없으면 null. @@ -87,10 +90,8 @@ export function appendCardHeader( const structure = document.createElement("span"); structure.className = "b06-cross-card__structure"; // 배수관은 관종·관경(`파형강관 D1200`)이 아니라 표시 이름 하나로 적는다 - // (2026-09-03 사용자 지시 — 2026-08-17 확정한 `PIPE_DISPLAY_NAME` 규칙과 같다). - // 관종·관경은 툴팁에 남긴다. - const isPipe = PIPE_TYPES.some((kind) => structureName.startsWith(`${kind} D`)); - structure.textContent = isPipe ? PIPE_DISPLAY_NAME : structureName; + // (2026-09-03 사용자 지시). 관종·관경은 툴팁에 남긴다. + structure.textContent = structureDisplayName(structureName); structure.title = `구조물: ${structureName}`; meta.append(structure); } diff --git a/B06_Section/B06_Section_UI_Longitudinal.ts b/B06_Section/B06_Section_UI_Longitudinal.ts index 5cccae88..0db4f492 100644 --- a/B06_Section/B06_Section_UI_Longitudinal.ts +++ b/B06_Section/B06_Section_UI_Longitudinal.ts @@ -17,6 +17,7 @@ import { longitudinalMaxChainage, niceTickStep, stationLabel, + structureDisplayName, svgElement, svgText, validElevation, @@ -327,7 +328,8 @@ export function createLongitudinalProfile( // 이미 쓰고 있어 서로 겹쳤다(2026-08-02 사용자 지시). if (station.structure) { structureLabels.push( - svgText(station.structure, { + // 배수관은 관종·관경 대신 표시 이름 하나로 적는다(2026-09-03 — 횡단 카드와 같은 규칙). + svgText(structureDisplayName(station.structure), { x: stationX, y: heightPx - padBottom - 5, "text-anchor": "middle", diff --git a/B06_Section/B06_Section_UI_Section_Common.ts b/B06_Section/B06_Section_UI_Section_Common.ts index 9f8016fb..09a7c62a 100644 --- a/B06_Section/B06_Section_UI_Section_Common.ts +++ b/B06_Section/B06_Section_UI_Section_Common.ts @@ -7,6 +7,8 @@ * 이 모듈을 참조한다. 색상 하드코딩 금지 원칙은 그대로이며 여기서는 지오메트리 계산만 다룬다. * ========================================================================== */ +import { PIPE_TYPES } from "@config/config_frontend"; +import { PIPE_DISPLAY_NAME } from "../B05_Profile/B05_Profile_UI_IrregularStations"; import type { CrossDesignChange } from "./B06_Section_UI_Cross_Design"; import type { DesignProfile, @@ -56,6 +58,17 @@ export interface YScaleOptions { globalMaxElevation: number; } +/** + * 구조물 라벨의 **표시 이름** — 배수관은 관종·관경(`파형강관 D1200`) 대신 `배수관` 하나로 + * 적는다(2026-08-17 확정한 `PIPE_DISPLAY_NAME` 규칙, 2026-09-03 종·횡단 표기에 적용). + * + * 저장 라벨은 그대로 두어야 한다 — 재진입 이관(`B05_Profile_Structures_Migration`)이 라벨 + * 문자열로 구조물 종류를 되짚는다. 그래서 바꾸는 것은 화면에 찍는 순간뿐이다. + */ +export function structureDisplayName(label: string): string { + return PIPE_TYPES.some((kind) => label.startsWith(`${kind} D`)) ? PIPE_DISPLAY_NAME : label; +} + export function validElevation( sample: SectionSample, ): sample is SectionSample & { elevation_m: number } {