From d849110478d19b40b2c94f2f040c61c8cddc40e0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 14:48:30 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(B06=20=ED=9A=A1=EB=8B=A8):=20=EA=B5=90?= =?UTF-8?q?=EC=B0=A8=EC=A0=90=20=3D=20=EC=82=AC=EB=A9=B4=EC=B2=98=EB=9F=BC?= =?UTF-8?q?=20=E2=80=94=20=EC=98=AC=EB=A6=BC=20=EA=B0=95=EC=A1=B0=20=C2=B7?= =?UTF-8?q?=20=EC=A0=9C=EB=AA=A9=20=E3=80=8C=EC=82=AC=EB=A9=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=E3=80=8D=20=C2=B7=20=EC=A0=88=C2=B7=EC=84=B1=ED=86=A0?= =?UTF-8?q?=20=EC=97=86=EC=9D=8C=20=EC=95=8C=EB=A6=BC=20=C2=B7=20=ED=8C=90?= =?UTF-8?q?=EC=A0=95=20=EC=9B=90=208=20px=20(379-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 보라 선 · 교차점 손잡이 · 암 경계선 마우스 올림 = 고른 모양(고름은 누를 때만) - 손잡이 제목 「사면 이동 (좌/우)」 · 노견 끝 = 원지반(±5 mm · edgeOnGround)이면 값 앞 「절·성토 없음 · 」 - 시험: 정확히 0 자리(멈춘 자리) 경우 — 절·성토 없음 · ◀↺▶ · 방향키 듣개 · 원 8 px Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- .../B06_Section_UI_Cross_Design_Pick.ts | 12 ++++-- .../B06_Section_UI_Style_Cross_Areas.css | 9 ++++- B06_Section/logic/B06_Section_UI_Cross_Fit.ts | 13 +++++++ resources/tester/helper_370_2_toe_stop.cjs | 37 ++++++++++++++++++- resources/tester/test_370_2_toe_stop.py | 17 +++++++-- 5 files changed, 78 insertions(+), 10 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Design_Pick.ts b/B06_Section/B06_Section_UI_Cross_Design_Pick.ts index 1a5cf4395..9ea0e2d1b 100644 --- a/B06_Section/B06_Section_UI_Cross_Design_Pick.ts +++ b/B06_Section/B06_Section_UI_Cross_Design_Pick.ts @@ -37,7 +37,7 @@ import { type LineCellState, type RockCellSpec, } from "./B06_Section_UI_Cross_Line_Cell"; -import { extendStop, toePoints } from "./logic/B06_Section_UI_Cross_Fit"; +import { edgeOnGround, extendStop, toePoints } from "./logic/B06_Section_UI_Cross_Fit"; const SVG_NS = "http://www.w3.org/2000/svg"; const DESIGN_CLASS = "b06-chart__design-cross"; @@ -379,6 +379,7 @@ export function designLineMoves( berm?: BermDepthControl; center?: number; toes?: ReturnType; + bare?: ReturnType; rock?: RockCellSpec; } { const design = section.design; @@ -400,6 +401,7 @@ export function designLineMoves( stopAt: (side, now, next) => extendStop(section.samples, design.road_edges, side, now, next), }, toes: toePoints(section), // 교차점 손잡이(370-2) + bare: edgeOnGround(section.samples, design.road_edges), // 절·성토 없는 쪽(379-1) berm: { set: (side, valueM) => send({ berm_depth: { side, valueM } }), // 끔은 이 측점이 켠 때만 — 다른 카드 소단을 고르며 옛 카드가 풀릴 때 새 켬을 안 지움 @@ -438,6 +440,8 @@ export function attachDesignLinePick(input: { center?: number; /** 있으면 쪽마다 교차점 손잡이(설계선 ∩ 원지반 · 키 `toe:left|right` · 늘 보임 · 370-2). */ toes?: ReturnType; + /** 있으면 쪽마다 노견 끝 = 원지반(절·성토 없음 · 379-1) — 손잡이 값 앞 알림. */ + bare?: ReturnType; /** 있으면 암 경계선 고름(키 `rock` · ▲▼↺ · 세로 끌기) · 안 고른 때 공용 칸 = 암(370-3). */ rock?: RockCellSpec; }): DesignLinePick { @@ -500,7 +504,6 @@ export function attachDesignLinePick(input: { const toe = input.toes?.[at] as { offset_m: number; elevation_m: number }; const hit = centerHit(x(toe.offset_m), toDisplayY(toe.elevation_m)); hit.classList.add(TOE_CLASS); - hit.setAttribute("r", "5"); hits.push(hit); } // 암 경계선(370-3) — 점선(`appendRockBoundaryOverlay`) 위 판정 선 하나(끊긴 토막도 한 path) · 키 rock @@ -631,6 +634,7 @@ export function attachDesignLinePick(input: { const moved = extend > 0 ? ` · 이동 +${extendText(extend)}m` : ""; const toe = toeSide ? input.toes?.[toeSide] : null; const dims = piece ? pieceLabel(design, piece).replace("\n", " · ") : ""; + const bare = toeSide && input.bare?.[toeSide] ? "절·성토 없음 · " : ""; // 379-1 // 아래 띠 공용 칸(370-3) — 안 고르면 암(암 지반만) · 고르면 그 선 제목 · 버튼 · 값 한 줄 const state: LineCellState | null = onRock || (!chosen && rock) @@ -639,8 +643,8 @@ export function attachDesignLinePick(input: { ? { title: "중심점", value: centerPreviewText(0), buttons: sideways(false) } : toe ? { - title: `교차점 이동 ${sideName}`, - value: `노선에서 ${Math.abs(toe.offset_m).toFixed(2)}m${moved}`, + title: `사면 이동 ${sideName}`, + value: `${bare}노선에서 ${Math.abs(toe.offset_m).toFixed(2)}m${moved}`, buttons: shift ? sideways(true) : {}, } : !piece diff --git a/B06_Section/B06_Section_UI_Style_Cross_Areas.css b/B06_Section/B06_Section_UI_Style_Cross_Areas.css index 0fe12440b..12c8a2a21 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Areas.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Areas.css @@ -170,6 +170,8 @@ cursor: pointer; } +/* 마우스 올림 = 고른 모양(379-1) · 고름은 누를 때만 */ +.b06-chart__design-hit:hover, .b06-chart__design-hit.is-active { stroke: color-mix(in srgb, var(--color-royal-amethyst) 45%, transparent); stroke-width: 7; @@ -193,21 +195,26 @@ cursor: ew-resize; } -/* 교차점 손잡이(370-2) — 사면 ∩ 원지반 작은 원 · 늘 보임 · 고르면 채움. */ +/* 교차점 손잡이(370-2) — 사면 ∩ 원지반 작은 원 · 늘 보임 · 고르면 채움 · 올리면 같은 모양(379-1). */ .b06-chart__toe-hit { fill: var(--color-surface); stroke: var(--color-royal-amethyst); stroke-width: 1.5; } +.b06-chart__toe-hit:hover, .b06-chart__toe-hit.is-active { fill: var(--color-royal-amethyst); cursor: pointer; } /* 암 경계선 고름(370-3) — 점선 위 투명 판정 선 · 고르면 경고색 띠 · 세로 끌기. */ +.b06-chart__design-hit.b06-chart__rock-hit:hover, .b06-chart__design-hit.b06-chart__rock-hit.is-active { stroke: color-mix(in srgb, var(--color-warning) 40%, transparent); +} + +.b06-chart__design-hit.b06-chart__rock-hit.is-active { cursor: ns-resize; } diff --git a/B06_Section/logic/B06_Section_UI_Cross_Fit.ts b/B06_Section/logic/B06_Section_UI_Cross_Fit.ts index 0b4250e69..d30b011af 100644 --- a/B06_Section/logic/B06_Section_UI_Cross_Fit.ts +++ b/B06_Section/logic/B06_Section_UI_Cross_Fit.ts @@ -112,6 +112,19 @@ export function extendStop( return next; } +/** 절·성토 없는 쪽(379-1) — 노견 끝 = 원지반(±5 mm · 멈춤 반올림 0.001 m 를 덮음). 지반 모르면 false. */ +export function edgeOnGround( + samples: CrossSection["samples"], + edges: CrossDesign["road_edges"] | undefined, +): Record<"left" | "right", boolean> { + const groundAt = groundInterpolator(samples ?? []); + const on = (side: "left" | "right") => + !!groundAt && + !!edges && + Math.abs(groundAt(edges[side].offset_m) - edges[side].elevation_m) <= 0.005; + return { left: on("left"), right: on("right") }; +} + /** * 성토측 사면의 **본래** 경사길이(m) — 성토가 아닌 측은 null (2026-09-03 사용자 지시). * diff --git a/resources/tester/helper_370_2_toe_stop.cjs b/resources/tester/helper_370_2_toe_stop.cjs index 3d64dda20..c62046e96 100644 --- a/resources/tester/helper_370_2_toe_stop.cjs +++ b/resources/tester/helper_370_2_toe_stop.cjs @@ -107,11 +107,12 @@ class FakeNode { (this.listeners.click ?? []).forEach((fn) => fn(event)); } } +const docKeys = new Set(); // 방향키 듣개(379-1 · 정확히 0 자리) globalThis.document = { createElement: (tag) => new FakeNode(tag), createElementNS: (_ns, tag) => new FakeNode(tag), - addEventListener: () => {}, - removeEventListener: () => {}, + addEventListener: (type, fn) => type === "keydown" && docKeys.add(fn), + removeEventListener: (type, fn) => type === "keydown" && docKeys.delete(fn), }; globalThis.window = { setTimeout, clearTimeout, addEventListener: () => {} }; @@ -302,4 +303,36 @@ out.inherit = { after.handle.setActive(afterIndex); out.inherit.label = cellOf(after.handle).title; +// ④ 정확히 0 자리(379-1) — 노견 끝 = 원지반(±5 mm = 절·성토 없음) · 손잡이 · 누르면 사면처럼 ◀↺▶ · 방향키 +// 조각 0 은 안 봄 — 그림 규칙(노견 끝 → 첫 접점 보호 구간)이 지반 위 짧은 선을 남김(브레인 A) +const zero = section(out.stop.value); +const zeroPicks = []; +const zeroLayer = new FakeNode("g"); +appendCrossDesignOverlay(zeroLayer, zero.design, x, y, zero.samples, undefined); +zeroLayer.children.forEach((el) => el.setAttribute(pick.DESIGN_LINE_ATTR, "")); +const zeroHandle = pick.attachDesignLinePick({ + layer: zeroLayer, + design: zero.design, + x, + toDisplayY: y, + onPick: (i) => zeroPicks.push(i), + ...pick.designLineMoves(zero, () => undefined), +}); +const zeroToes = zeroLayer.children.filter((k) => k.classList.contains("b06-chart__toe-hit")); +const rightToe = zeroToes.find((t) => Number(t.getAttribute("cx")) > x(0)); +docKeys.clear(); +rightToe?.click(); +zeroHandle.setActive(zeroPicks[0]); +const zeroCell = cellOf(zeroHandle); +out.zero = { + bare: pick.designLineMoves(zero, () => undefined).bare, + nearBare: pick.designLineMoves(near, () => undefined).bare, + toe: !!rightToe, + radius: rightToe ? rightToe.getAttribute("r") : null, + title: zeroCell.title, + value: zeroCell.value, + buttons: ["left", "reset", "right"].filter((slot) => zeroCell.button(slot)), + keys: docKeys.size, +}; + process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_370_2_toe_stop.py b/resources/tester/test_370_2_toe_stop.py index e40accd7e..42e9f4e94 100644 --- a/resources/tester/test_370_2_toe_stop.py +++ b/resources/tester/test_370_2_toe_stop.py @@ -1,6 +1,6 @@ """PLAN 370-2 — B06 사면 이동 멈춤 · 교차점 손잡이 · 고름 이어받기(요구 240-14). -짝: `B06_Section/logic/B06_Section_UI_Cross_Fit.ts`(`extendStop` · `toePoints`) · +짝: `B06_Section/logic/B06_Section_UI_Cross_Fit.ts`(`extendStop` · `toePoints` · `edgeOnGround`) · `B06_Section/B06_Section_UI_Cross_Design_Pick.ts`(손잡이 · 키 이어받기). 실제 TS 를 `helper_370_2_toe_stop.cjs` 가 Node 로 돌린다. """ @@ -63,7 +63,7 @@ def test_교차점_손잡이_쪽마다_늘_보임(out) -> None: def test_손잡이를_고르면_그_쪽_사면_좌우_이동(out) -> None: got = out["toePick"] assert got["active"] is True and got["panelOpen"] is True - assert got["title"] == "교차점 이동 (우)" # 아래 띠 공용 칸(370-3) + assert got["title"] == "사면 이동 (우)" # 아래 띠 공용 칸(370-3) assert got["value"].startswith("노선에서 ") assert len(got["sent"]) == 1 and got["sent"][0]["side"] == "right" low, high = out["stop"]["crossing"] @@ -76,4 +76,15 @@ def test_절에서_성으로_넘어가도_고름_유지(out) -> None: assert inherit["cutIndex"] >= 0 and inherit["before"] == inherit["cutIndex"] assert inherit["farMode"] == "left_cut" # 우 = 성토로 넘어감 assert inherit["afterIndex"] is not None - assert inherit["label"] == "교차점 이동 (우)" + assert inherit["label"] == "사면 이동 (우)" + + +def test_정확히_0_자리는_절성토_없음_사면처럼_고름(out) -> None: + """379-1 — 멈춘 자리(노견 끝 = 원지반) 손잡이를 누르면 사면과 같은 조작.""" + zero = out["zero"] + assert zero["bare"]["right"] is True and zero["nearBare"]["right"] is False + assert zero["toe"] is True and zero["radius"] == "8" # 판정 원 8 px + assert zero["title"] == "사면 이동 (우)" + assert zero["value"].startswith("절·성토 없음 · 노선에서 ") + assert zero["buttons"] == ["left", "reset", "right"] # ◀↺▶ + assert zero["keys"] >= 1 # 방향키 듣개 From 18e6d10ebb3144f950c316bf492c5361bdf09116 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 14:54:54 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(B06=20=ED=9A=A1=EB=8B=A8):=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=A2=8C=EC=83=81=20=EB=A9=B4=EC=A0=81=ED=91=9C=20?= =?UTF-8?q?=EC=A7=80=EC=9B=80=20=E2=80=94=20=EA=B0=92=EC=9D=80=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EB=AA=A8=EB=8B=AC=20=ED=91=9C=20=C2=B7=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=EC=B0=BD=20=EC=9C=84=EC=97=90=EC=84=9C=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=20(379-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드 면적 readout(buildAreaReadout · setChipActive) · 그 CSS · 안 쓰게 된 로케일 키 지움 - 구조물 조정창 top = spacing-8(표 높이 띄움 걷음) · 면적 강조 밴드 · 모달 표 그대로 - 시험 test_173 고쳐 씀(표 걷음 · 조정창 자리 · 글자 변수 남김) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- B06_Section/B06_Section_UI_Cross_Areas.ts | 174 +----------------- B06_Section/B06_Section_UI_Cross_Design.ts | 1 - B06_Section/B06_Section_UI_Cross_View.ts | 17 +- B06_Section/B06_Section_UI_Section_View.ts | 2 +- .../B06_Section_UI_Style_Cross_Areas.css | 106 +---------- .../B06_Section_UI_Style_Cross_Controls.css | 8 +- resources/tester/test_173_areas_detail.py | 31 ++-- ui_template/ui_template_locale_b2.ts | 14 -- 8 files changed, 26 insertions(+), 327 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Areas.ts b/B06_Section/B06_Section_UI_Cross_Areas.ts index 6bdaaee94..6985913bd 100644 --- a/B06_Section/B06_Section_UI_Cross_Areas.ts +++ b/B06_Section/B06_Section_UI_Cross_Areas.ts @@ -1,6 +1,6 @@ /* ============================================================================= * B06_Section_UI_Cross_Areas.ts - * 횡단 절·성토 면적 readout과 면적 영역 하이라이트 밴드. + * 횡단 절·성토 면적 영역 하이라이트 밴드(카드 좌상 면적표는 걷음 · 값은 상세 모달 표 · 379-2). * * 절토는 암반 경계선(지면선 평행 복사)을 기준으로 위=토사, 아래=암반으로 갈린다. 경계선 * 위치가 곧 유토곡선 EA/RR/BR 비율을 만들므로, 사용자가 경계선을 올리내리면 여기 표시되는 @@ -16,7 +16,7 @@ * ========================================================================== */ import type { CrossDesign, SectionSample } from "./B06_Section_Api_Fetch"; -import { L, svgElement } from "./B06_Section_UI_Section_Common"; +import { svgElement } from "./B06_Section_UI_Section_Common"; /** * 강조 대상 = 면적표의 칸. `cut_total`은 토사·암반 두 밴드를 함께 켠다. @@ -135,7 +135,7 @@ function bandPolygons( /** * 그 측점 절토의 암종. 분리 필드가 없는 **구 데이터**는 지반유형을 그대로 쓴다 — - * 표(`cutBreakdown`)는 그 규칙으로 RR/BR에 값을 넣는데 여기서만 null로 보면 + * 면적표(상세 모달)는 그 규칙으로 RR/BR에 값을 넣는데 여기서만 null로 보면 * 암반 밴드가 아예 안 만들어져 **표에는 값이 있는데 그림이 안 켜진다**(2026-08-02 사용자 지적). */ function rockKindOf(design: CrossDesign): CrossDesign["cut_rock_kind"] { @@ -246,171 +246,3 @@ export function appendCrossAreaBands( } }; } - -/** 절토 단면적을 도면 표기(EA 토사 / RR 리핑암 / BR 발파암)로 갈라 낸다. */ -function cutBreakdown(design: CrossDesign): { - ea: number; - rr: number; - br: number; - total: number; -} { - const total = design.cut_area_m2; - // 구 데이터(분리 필드 없음)는 절토 전량을 측점 지반유형으로 돌린다 — 유토곡선 폴백과 같은 규칙. - if (design.cut_soil_area_m2 === undefined || design.cut_rock_area_m2 === undefined) { - const ground = design.ground_type; - return { - ea: ground === "soil" ? total : 0, - rr: ground === "ripping_rock" ? total : 0, - br: ground === "blasting_rock" ? total : 0, - total, - }; - } - const rock = design.cut_rock_area_m2; - const kind = rockKindOf(design); - return { - ea: design.cut_soil_area_m2, - rr: kind === "ripping_rock" ? rock : 0, - br: kind === "blasting_rock" ? rock : 0, - total, - }; -} - -/** 표 한 칸. `key`가 없으면 강조 대상이 아닌 칸(성토의 EA/RR/BR 자리)이다. */ -interface AreaCell { - key: CrossAreaKey | null; - value: number | null; - variant: string; -} - -function appendCell( - row: HTMLTableRowElement, - cell: AreaCell, - onSelect: ((key: CrossAreaKey) => void) | undefined, - cells: Map, -): void { - const td = document.createElement("td"); - td.className = `b06-design__area b06-design__area--${cell.variant}`; - const text = cell.value === null ? "—" : cell.value.toFixed(2); - // 값이 0이어도 칸은 남긴다 — 측점마다 항목 수가 달라지면 카드끼리 비교가 안 된다. - if (!cell.key || !onSelect || cell.value === null) { - td.textContent = text; - row.append(td); - return; - } - const button = document.createElement("button"); - button.type = "button"; - button.textContent = text; - button.title = L("B06_Design_Area_Highlight"); - button.setAttribute("aria-pressed", "false"); - button.addEventListener("click", (event) => { - // 카드까지 올라가면 측점 선택이 다시 걸려 카드가 통째로 다시 그려진다 — 강조가 즉시 사라진다. - event.stopPropagation(); - onSelect(cell.key as CrossAreaKey); - }); - cells.set(cell.key, [...(cells.get(cell.key) ?? []), button]); - td.append(button); - row.append(td); -} - -/** - * 절·성토 면적값 오버레이(E-4) — 그래프 중상단에 표(절토/성토 × EA·RR·BR·계)로 표시한다. - * 항목 이름은 도면 표기를 그대로 쓴다(EA 토사 / RR 리핑암 / BR 발파암). - * `onSelect`가 오면 값 칸이 강조 토글 버튼이 된다(선택된 카드에서만 넘어온다). - */ -export function buildAreaReadout( - design: CrossDesign | undefined, - onSelect?: (key: CrossAreaKey) => void, -): { root: HTMLElement; setActive: AreaHighlightSetter } { - if (!design) { - const unset = document.createElement("div"); - unset.className = "b06-cross-card__areas b06-design__area--unset"; - unset.textContent = L("B06_Design_Unset"); - return { root: unset, setActive: () => undefined }; - } - - const table = document.createElement("table"); - table.className = "b06-cross-card__areas"; - const head = document.createElement("tr"); - // 좌상단(행제목 × 열제목 교차) 칸은 단위 자리다 — 값 칸마다 단위를 붙이지 않는다. - for (const [text, tip] of [ - [L("B06_Design_Area_Unit"), ""], - [L("B06_Design_Area_EA"), L("B06_Design_Ground_Soil")], - [L("B06_Design_Area_RR"), L("B06_Design_Ground_Ripping")], - [L("B06_Design_Area_BR"), L("B06_Design_Ground_Blasting")], - [L("B06_Design_Area_Total"), ""], - ]) { - const th = document.createElement("th"); - th.textContent = text; - if (tip) th.title = tip; - head.append(th); - } - const thead = document.createElement("thead"); - thead.append(head); - - const cells = new Map(); - const cut = cutBreakdown(design); - const tbody = document.createElement("tbody"); - const rows: Array<{ label: string; variant: string; cells: AreaCell[] }> = [ - { - label: L("B06_Design_Cut_Area"), - variant: "cut_total", - // 값이 0이어도 누를 수 있다(2026-08-02 사용자 지시) — 그림에 없는 면적이면 강조만 켜지고 - // 밴드는 그대로다. 칸마다 눌리는 자리가 달라지면 어느 칸이 눌리는지 매번 확인해야 한다. - cells: [ - { key: "cut_soil", value: cut.ea, variant: "cut_soil" }, - { key: "cut_rr", value: cut.rr, variant: "cut_rr" }, - { key: "cut_br", value: cut.br, variant: "cut_br" }, - { key: "cut_total", value: cut.total, variant: "cut_total" }, - ], - }, - { - // 성토는 완성 단면(다짐 상태)이라 지반유형으로 갈리지 않는다 — 계 한 칸만 쓴다. - label: L("B06_Design_Fill_Area"), - variant: "fill", - cells: [ - { key: null, value: null, variant: "fill" }, - { key: null, value: null, variant: "fill" }, - { key: null, value: null, variant: "fill" }, - { key: "fill", value: design.fill_area_m2, variant: "fill" }, - ], - }, - ]; - // 사토장이 선 측점만 한 줄 더 — **성토와 합치지 않는다**(확정 ㉠). 없는 측점에 빈 줄을 - // 세우면 어느 측점에 사토장이 있는지 표에서 안 보인다. - const spoilArea = Number(design.spoil_fill_area_m2 ?? 0); - if (spoilArea > 0) { - rows.push({ - label: L("B06_Design_SpoilFill_Area"), - variant: "spoil-fill", - cells: [ - { key: null, value: null, variant: "spoil-fill" }, - { key: null, value: null, variant: "spoil-fill" }, - { key: null, value: null, variant: "spoil-fill" }, - { key: null, value: spoilArea, variant: "spoil-fill" }, - ], - }); - } - for (const row of rows) { - const tr = document.createElement("tr"); - const th = document.createElement("th"); - th.className = `b06-design__area b06-design__area--${row.variant}`; - th.textContent = row.label; - tr.append(th); - for (const cell of row.cells) appendCell(tr, cell, onSelect, cells); - tbody.append(tr); - } - table.append(thead, tbody); - - return { - root: table, - setActive: (key) => { - for (const [cellKey, elements] of cells) { - const active = cellKey === key; - for (const element of elements) { - element.classList.toggle("is-active", active); - element.setAttribute("aria-pressed", String(active)); - } - } - }, - }; -} diff --git a/B06_Section/B06_Section_UI_Cross_Design.ts b/B06_Section/B06_Section_UI_Cross_Design.ts index 5b43ca31a..1a521fa4e 100644 --- a/B06_Section/B06_Section_UI_Cross_Design.ts +++ b/B06_Section/B06_Section_UI_Cross_Design.ts @@ -443,7 +443,6 @@ export function buildDesignControls( scheduleReflow(card); // 암 경계선 제어는 그래프 X축 제목 행으로 이동(E-7, Cross_View에서 배치). - // 절·성토 면적 readout은 그래프 중상단 오버레이로 이동(E-4, Cross_View에서 배치). return { bar, groundSegment }; } diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index c4acae6b0..de5501110 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -13,7 +13,6 @@ import type { CrossSection } from "./B06_Section_Api_Fetch"; import { appendCrossAreaBands, type AreaHighlightSetter, - buildAreaReadout, type CrossAreaKey, } from "./B06_Section_UI_Cross_Areas"; import { @@ -192,7 +191,6 @@ export function createCrossSectionCard( let isSelected = selected; let activeArea: CrossAreaKey | null = selected ? (initialAreaKey ?? null) : null; let setBandActive: AreaHighlightSetter = () => undefined; - let setChipActive: AreaHighlightSetter = () => undefined; let activeRevet: RevetPick | null = revetOffset?.selectedFor(section) ?? null; /** 이 카드에서 **강조**할 벽. 연동으로 이어진 옆 측점이 고른 벽도 켠다 — * 조정창(`activeRevet`)은 고른 카드 하나에만 뜬다(2026-08-30 사용자). */ @@ -226,7 +224,6 @@ export function createCrossSectionCard( if (activeArea === null) return; activeArea = null; setBandActive(null); - setChipActive(null); onAreaSelect?.(section.station_id, null); }, selectCard: () => onSelect(section.station_id), @@ -245,7 +242,6 @@ export function createCrossSectionCard( if (activeRevet !== null && activeArea !== null) { activeArea = null; setBandActive(null); - setChipActive(null); } if (activeRevet !== null) { dropLine(); // 고름 하나만(137-3) @@ -262,7 +258,6 @@ export function createCrossSectionCard( card.classList.toggle("b06-cross-card--selected", nextSelected); activeArea = nextSelected ? (areaKey ?? null) : null; setBandActive(activeArea); - setChipActive(activeArea); // 면적이 켜졌거나 카드가 선택에서 빠지면 구조물 선택은 풀린다(같은 레벨). if ((activeArea !== null || !nextSelected) && activeRevet !== null) { // 전역 선택이 **아직 이 카드 것일 때만** 비운다 — 옆 카드에서 방금 고른 벽까지 @@ -306,7 +301,6 @@ export function createCrossSectionCard( dropLine(); } setBandActive(activeArea); - setChipActive(activeArea); onAreaSelect?.(section.station_id, activeArea); }; /** 보라 선 고름 — 다른 고름(벽 · 본체 · 면적)을 풀고 카드도 고름(137-3). 다시 누르면 풂. */ @@ -319,7 +313,6 @@ export function createCrossSectionCard( if (activeArea !== null) { activeArea = null; setBandActive(null); - setChipActive(null); onAreaSelect?.(section.station_id, null); } if (!isSelected) { @@ -649,9 +642,6 @@ export function createCrossSectionCard( contentBounds, ); scaleCenter(cardZoomStates.get(section.station_id)?.scale ?? 1); - // 절·성토 면적값은 그래프 중상단 오버레이로 표시(E-4). 값 칸은 항상 강조 토글이다. - const readout = buildAreaReadout(section.design, toggleArea); - setChipActive = readout.setActive; // 배수관 · 흙막이 조정창 두 벌(카드 안 · 좌측 사본) — `_Cross_View_Panels`(137-1). const panel = buildCardPanels({ section, @@ -684,12 +674,9 @@ export function createCrossSectionCard( fit: () => stationWidth.request(section.chainage_m, crossHalfWidth ?? maxOffset), }; const zoomBar = buildZoomControls(zoomPan, widthActions); - chartWrap.append(svg, readout.root, zoomBar, panel.root); + chartWrap.append(svg, zoomBar, panel.root); for (const panel of bodies.panels) chartWrap.append(panel.root); - if (activeArea) { - setBandActive(activeArea); - setChipActive(activeArea); - } + if (activeArea) setBandActive(activeArea); card.append(chartWrap); } diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index f2bb7aab8..a7934b7ba 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -455,7 +455,7 @@ export function createSectionView( // CSS auto-fill의 자체 열 수와 JS columnCount가 어긋나면 같은 행 높이 그룹핑이 실제 // 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수 · 폭을 JS가 명시해 일치시킨다. grid.style.gridTemplateColumns = `repeat(${cols}, ${cachedCardWidth}px)`; - grid.dataset.columns = String(cols); // 4열부터 면적표 「계」만(173-3 · CSS) + grid.dataset.columns = String(cols); // 실제 그린 열 수(CSS) // 맹암거 형상 누름 = 그 구조물 id 고름(288-2 · 노면공 포장 범위 표식과 따로 · 같은 id 고름 창구) grid.addEventListener(SHAPE_PICK_EVENT, (event) => { const id = (event as CustomEvent).detail; diff --git a/B06_Section/B06_Section_UI_Style_Cross_Areas.css b/B06_Section/B06_Section_UI_Style_Cross_Areas.css index 12c8a2a21..256898f46 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Areas.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Areas.css @@ -1,117 +1,15 @@ /* ============================================================================= * B06_Section_UI_Style_Cross_Areas.css - * 횡단 카드의 **면적 오버레이 계열** 스타일 — 절·성토 면적 표, 면적 강조 밴드, + * 횡단 카드의 **면적 오버레이 계열** 스타일 — 면적 강조 밴드, * 설계선·암 경계선·포장층, 암 경계선 제어 버튼. * * `_UI_Style_Cross.css`가 700줄을 넘겨, 도면 뼈대(섹션·카드·차트·줌/팬)와 * 그 위에 얹는 면적 표기 계열을 갈랐다. 색은 모두 theme.css 토큰만 쓴다. * ========================================================================== */ -/* 절·성토 면적 오버레이(E-4) — 그래프 **좌측 상단**(축선 바로 안쪽), 배경색으로 그래프 - 선과 겹쳐도 가독. 절토/성토 × EA·RR·BR·계 표라서 칩을 늘어놓을 때보다 가로 폭을 덜 먹는다. - 가운데(left:50%)에 두던 것을 좌측으로만 옮겼다 — 세로 자리는 그대로(2026-08-23 사용자 지시). - 58px = CROSS_PAD.left(Y축 눈금 띠 폭) — 표가 플롯 왼쪽 끝에 맞춰 선다. - 글자 = 그림 칸(컨테이너) 폭 비례(173-3 · 3열 ≈ 0.72rem) · 표 높이 변수도 같은 식 — - 조정창 · 보라 치수가 표 아래에서 시작(`_Cross_Controls`). */ +/* 그림 칸 글자 = 칸(컨테이너) 폭 비례(173-3). 카드 좌상 면적표는 걷음 — 값은 상세 모달 표(379-2). */ .b06-cross-card__chart-wrap { --b06-areas-font: clamp(0.6rem, 0.55rem + 0.7cqw, 0.9rem); - --b06-areas-height: calc(var(--b06-areas-font) * 5); -} - -.b06-cross-card__areas { - position: absolute; - top: var(--spacing-8); - left: 58px; - transform: none; - padding: 2px 6px; - border-radius: var(--radius-inputs); - background: color-mix(in srgb, var(--color-surface) 88%, transparent); - font-size: var(--b06-areas-font); - font-family: var(--font-mono); - white-space: nowrap; - border-collapse: collapse; - pointer-events: none; -} - -.b06-cross-card__areas th, -.b06-cross-card__areas td { - padding: 0 0.45em; - text-align: center; - font-weight: var(--font-weight-regular); -} - -/* 4열부터 「계」만(173-3) — EA · RR · BR 칸(머리 · 본문 2~4째)을 접음 · 열 수 = 그리드 data-columns */ -.b06-section__grid:is([data-columns="4"], [data-columns="5"], [data-columns="6"]) - .b06-cross-card__areas - :is(th, td):nth-child(n + 2):nth-child(-n + 4) { - display: none; -} - -/* 열 제목은 가운데 맞춤 — 값은 우측 정렬이라 자릿수가 흔들려도 머리글은 열 중앙에 선다. */ -.b06-cross-card__areas thead th { - text-align: center; - color: var(--color-text-muted); -} - -/* 행 이름(절토/성토)은 왼쪽 정렬 — 숫자 열과 구분된다. */ -.b06-cross-card__areas tbody th { - text-align: left; -} - -/* 면적 표기 색은 **테마 토큰만** 쓴다. 고정 RGB를 박으면 다크 테마에서 글자가 배경에 - 묻힌다(발파암에 쓰던 검정이 그랬다). 차트 팔레트는 밝기가 두 테마 모두에 맞춰져 있다. */ -.b06-design__area--cut, -.b06-design__area--cut_total { - color: var(--color-danger); -} - -/* 절토 내역 — 토사(EA), 리핑암(RR), 발파암(BR)을 색으로 갈라 둔다. */ -.b06-design__area--cut_soil { - color: var(--color-chart-2); -} - -/* RR은 `--color-slate`를 쓰면 안 된다 — 그 토큰이 곧 `--color-text-secondary`라 - 라이트 테마에서 회색 글자가 "비활성"으로 읽히고, 28% 밴드는 흰 배경에 묻힌다. - `--color-warning`도 못 쓴다(암 경계선 점선이 이미 그 색이라 밴드가 제 경계선과 섞인다). */ -.b06-design__area--cut_rr { - color: var(--color-success); -} - -.b06-design__area--cut_br { - color: var(--color-chart-3); -} - -.b06-design__area--fill { - color: var(--color-chart-0); -} - -/* 사토장 — 성토와 **다른 색**으로 둔다. 같은 색이면 표에서 한 덩어리로 읽힌다. */ -.b06-design__area--spoil-fill { - color: #6d7a20; -} - -.b06-design__area--unset { - color: var(--color-text-muted); -} - -/* 선택된 카드에서만 값 칸이 버튼이 된다 — 부모가 pointer-events: none이라 여기서 되살린다. */ -.b06-cross-card__areas button { - padding: 0 2px; - border: none; - border-radius: var(--radius-inputs); - background: none; - color: inherit; - font-family: inherit; - font-size: inherit; - cursor: pointer; - pointer-events: auto; -} - -/* 켠 칸은 글자색을 유지한 채 같은 색으로 옅게 칠하고 테두리를 둘러 대비를 살린다. */ -.b06-cross-card__areas button.is-active { - background: color-mix(in srgb, currentcolor 18%, transparent); - box-shadow: inset 0 0 0 1px currentcolor; - font-weight: var(--font-weight-medium); } /* 면적 강조 밴드 — 평소엔 클릭 대상으로만 남고, 켜질 때만 채워진다. */ diff --git a/B06_Section/B06_Section_UI_Style_Cross_Controls.css b/B06_Section/B06_Section_UI_Style_Cross_Controls.css index ac2a795b3..25eb1c938 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Controls.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Controls.css @@ -4,15 +4,13 @@ * 넘칠 만큼 길어지면 창 안에서 스크롤한다. z-index는 카드 안 오버레이 중 최상단. */ .b06-structure-panel { position: absolute; - /* 좌측 상단에는 절·성토 면적표가 이미 서 있다 — 조정창을 같은 자리에 두면 표를 덮어 - 수치가 안 보인다(2026-09-12 사용자). 표 높이(머리글+절토+성토 세 줄)만큼 내려 시작한다. - 아래를 기준으로 붙이지 않는 까닭은 종전 그대로다 — 낮은 횡단도에서 창이 잘린다. */ - top: calc(var(--spacing-8) + var(--b06-areas-height, 3.6rem)); + /* 좌측 상단 면적표를 걷어(379-2) 위에서 바로 시작. 아래 기준이 아닌 까닭 = 낮은 횡단도에서 잘림. */ + top: var(--spacing-8); /* Y축·눈금 라벨 띠(CROSS_PAD.left)를 덮지 않게 그림 영역 안쪽에서 시작한다. */ left: calc(var(--b06-plot-left, 58px) + var(--spacing-8)); z-index: 20; display: flex; - max-height: calc(100% - 2 * var(--spacing-8) - var(--b06-areas-height, 3.6rem)); + max-height: calc(100% - 2 * var(--spacing-8)); flex-direction: column; gap: 2px; padding: var(--spacing-8); diff --git a/resources/tester/test_173_areas_detail.py b/resources/tester/test_173_areas_detail.py index f7a695ac6..8b60b99d1 100644 --- a/resources/tester/test_173_areas_detail.py +++ b/resources/tester/test_173_areas_detail.py @@ -1,6 +1,6 @@ """PLAN 173-3 · 173-4 — B06 횡단 카드 면적표 열 연동 · [상세] 그림 테마 색 · 칸 가득. -짝: `B06_Section/B06_Section_UI_Style_Cross_Areas.css`(글자 = 그림 칸 폭 · 4열부터 「계」만) · +짝: `B06_Section/B06_Section_UI_Style_Cross_Areas.css`(글자 = 그림 칸 폭 · 379-2 카드 면적표 걷음) · `_UI_Section_View.ts`(그리드 data-columns · `detailChart`) · `_UI_Cross_Detail.ts`(칸 크기로 새로 그림) · `_UI_Style_Cross_Detail.css`(테마 토큰) · `_UI_Cross_View_Metrics.ts` `fitPlotBase`(Node 헬퍼 `helper_173_detail_fit.cjs` 가 실제 TS 를 돌림). @@ -10,7 +10,6 @@ from __future__ import annotations import json import os -import re import subprocess import pytest @@ -40,16 +39,14 @@ def fit() -> dict: return json.loads(proc.stdout) -# ── 173-3 면적표 ───────────────────────────────────────────── +# ── 173-3 그림 칸 글자 · 379-2 카드 좌상 면적표 걷음 ───────────────── -def test_면적표_글자_그림_칸_폭_비례_높이_변수_같은_식(): +def test_그림_칸_글자_폭_비례_남김(): css = _read("B06_Section_UI_Style_Cross_Areas.css") wrap = css.split(".b06-cross-card__chart-wrap {", 1)[1].split("}", 1)[0] assert "--b06-areas-font: clamp(0.6rem, 0.55rem + 0.7cqw, 0.9rem);" in wrap - assert "--b06-areas-height: calc(var(--b06-areas-font) * 5);" in wrap - table = css.split(".b06-cross-card__areas {", 1)[1].split("}", 1)[0] - assert "font-size: var(--b06-areas-font);" in table + assert "font-size: var(--b06-areas-font);" in css # 남은 쓰는 곳 assert "font-size: 0.72rem" not in css # 3열 그림 칸 ≈ 400 px → 지금 글자(0.72rem)와 거의 같음 · 1열 · 6열은 끝값 @@ -60,16 +57,18 @@ def test_면적표_글자_그림_칸_폭_비례_높이_변수_같은_식(): assert rem(1232) == 0.9 and rem(192) == pytest.approx(0.634) -def test_면적표_4열부터_계만_접기(): +def test_카드_좌상_면적표_걷음_조정창은_위에서(): + """379-2 — 카드 좌상 면적표 지움 · 조정창 top = spacing-8 · 모달 표는 그대로.""" css = _read("B06_Section_UI_Style_Cross_Areas.css") - rule = re.search( - r'\.b06-section__grid:is\(\[data-columns="4"\], \[data-columns="5"\], \[data-columns="6"\]\)\s+' - r"\.b06-cross-card__areas\s+:is\(th, td\):nth-child\(n \+ 2\):nth-child\(-n \+ 4\) \{\n display: none;", - css, - ) - assert rule, "4~6열 EA · RR · BR 접기 규칙 없음" - src = _read("B06_Section_UI_Section_View.ts") - assert "grid.dataset.columns = String(cols);" in src # 실제 그린 열 수(휴대폰 폭 = 1 · 기기대응) + assert "b06-cross-card__areas" not in css and "--b06-areas-height" not in css + assert "buildAreaReadout" not in _read("B06_Section_UI_Cross_Areas.ts") + view = _read("B06_Section_UI_Cross_View.ts") + assert "buildAreaReadout" not in view and "setChipActive" not in view + controls = _read("B06_Section_UI_Style_Cross_Controls.css") + panel = controls.split(".b06-structure-panel {", 1)[1].split("}", 1)[0] + assert "top: var(--spacing-8);" in panel and "--b06-areas-height" not in controls + table = os.path.join(ROOT, "B07_DesignDetail", "B07_DesignDetail_Engine_Cad_Table.py") + assert os.path.isfile(table) # 모달 표 그대로 # ── 173-4 [상세] 그림 ──────────────────────────────────────── diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 9240f1b4a..796f13715 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -424,23 +424,9 @@ export const ui_locales_b2 = { B06_Design_Ditch_Legend: ["측구위치", "Ditch side"], B06_Design_Ditch_Left: ["좌", "Left"], B06_Design_Ditch_Right: ["우", "Right"], - B06_Design_Cut_Area: ["절토", "Cut"], /* 절토 내역 — 지표면~암반 경계선이 토사, 그 아래가 암반(합=절토). */ B06_Design_Cut_Soil_Area: ["절토(토사)", "Cut (soil)"], B06_Design_Cut_Rock_Area: ["절토(암반)", "Cut (rock)"], - /* 횡단 면적표 열 이름은 유토곡선 도면 표기를 그대로 쓴다(EA 토사 / RR 리핑암 / BR 발파암). */ - B06_Design_Area_EA: ["EA", "EA"], - B06_Design_Area_RR: ["RR", "RR"], - B06_Design_Area_BR: ["BR", "BR"], - B06_Design_Area_Total: ["계", "Total"], - /* 단위는 값 칸마다 붙이지 않고 표 좌상단(행제목 × 열제목 교차) 칸에 한 번만 적는다. */ - B06_Design_Area_Unit: ["㎡", "㎡"], - B06_Design_Area_Highlight: ["누르면 해당 면적을 강조합니다", "Click to highlight this area"], - B06_Design_Fill_Area: ["성토", "Fill"], - // 사토장(유용토운반작업장) — **성토와 갈라 보인다**. 합치면 「쌓기」가 갑자기 커 보여 - // 사람이 오해한다(2026-09-09 네 창 확정 ㉠). - B06_Design_SpoilFill_Area: ["사토장", "Spoil site"], - B06_Design_Unset: ["미지정", "Not set"], B06_Design_DitchType_Legend: ["측구형식", "Ditch type"], B06_Design_DitchType_Standard: ["일반", "Standard"], B06_Design_DitchType_LType: ["L형", "L-type"], From 289aa715bf830a89f8f96e453b0d7faf06717597 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 14:58:06 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(B06=20=ED=9A=A1=EB=8B=A8):=20Y?= =?UTF-8?q?=EC=B6=95=20=ED=91=9C=EA=B3=A0=20=EC=83=81=EC=9E=90=20=EB=91=98?= =?UTF-8?q?=20=E2=80=94=20=EC=9B=90=EC=A7=80=EB=B0=98=EA=B3=A0(=EB=85=B9)?= =?UTF-8?q?=20=C2=B7=20=EA=B3=84=ED=9A=8D=20=EC=A4=91=EC=8B=AC=EA=B3=A0(?= =?UTF-8?q?=EB=B3=B4=EB=9D=BC)=20=C2=B7=20=EA=B2=B9=EC=B9=98=EB=A9=B4=20?= =?UTF-8?q?=EC=9B=90=EC=A7=80=EB=B0=98=EB=A7=8C=20=EB=B9=84=ED=82=B4=20(37?= =?UTF-8?q?9-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createCrossAxes designZ 옵션 · 원지반 상자 먼저 · 계획 상자 나중(위) · 색 = 원지반선 · 설계선 - y 차 16 px 미만이면 원지반 상자만 위아래 관계 따라 비킴 · 칸 밖이면 반대 · 값 그대로 - 배선 = 카드(십자선 계획고) · 상세 모달(cardDesignElevation) · 시험 test_318 겹침 경우 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- B06_Section/B06_Section_UI_Cross_Axes.ts | 47 +++++++++++++++---- B06_Section/B06_Section_UI_Cross_View.ts | 1 + B06_Section/B06_Section_UI_Section_View.ts | 1 + B06_Section/B06_Section_UI_Style_Cross.css | 10 ++-- .../B06_Section_UI_Style_Cross_Areas.css | 9 ++++ resources/tester/helper_318_cross_card.cjs | 33 +++++++++++++ resources/tester/test_318_cross_card.py | 42 +++++++++++++++-- 7 files changed, 125 insertions(+), 18 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Axes.ts b/B06_Section/B06_Section_UI_Cross_Axes.ts index 2bb553ade..f4d6993b9 100644 --- a/B06_Section/B06_Section_UI_Cross_Axes.ts +++ b/B06_Section/B06_Section_UI_Cross_Axes.ts @@ -28,6 +28,8 @@ export interface CrossAxesOptions { exaggeration: number; /** 중심고(실제 표고) — Y축 라벨 자리에 둥근 상자로(318-2 · 소수 첫째 자리). 없으면 안 그림. */ centerZ?: number | null; + /** 계획 중심고(십자선 계획고) — 보라 상자를 원지반 상자 뒤(위)에(379-3). 없으면 안 그림. */ + designZ?: number | null; } /** X 눈금 라벨("-12" 등)이 겹치지 않는 최소 간격(px). */ @@ -43,6 +45,8 @@ const Y_LABEL_GAP_PX = 7; /** 중심고 상자 — 글자 좌우 여백 · 높이(px). 글자 크기는 눈금과 같음(10px). */ const CENTER_PAD_X = 3; const CENTER_BOX_H = 14; +/** 두 상자 y 차가 이보다 작으면 원지반 상자만 비킴(px · 379-3). */ +const TAG_GAP_PX = 16; /** * 축 레이어를 그리는 함수를 만든다. 줌·팬이 바뀔 때마다 그 상태로 다시 부르면 된다. @@ -118,12 +122,12 @@ export function createCrossAxes( const screenYOf = (raw: number) => ty + scale * y(options.elevationMid + (raw - options.elevationMid) * options.exaggeration); // 중심고 상자 자리 — 보이는 범위 안일 때만 · 겹친 눈금 글도 그림(상자를 맨 뒤에 붙여 위에 · - // 바탕이 안 비쳐 가림 · 319-1) - const centerZ = options.centerZ; - const centerY = - typeof centerZ === "number" && centerZ >= rawBottom && centerZ <= rawTop - ? screenYOf(centerZ) - : null; + // 바탕이 안 비쳐 가림 · 319-1) · 원지반(녹) 먼저 · 계획(보라) 나중(379-3) + const { centerZ, designZ } = options; + const inView = (z: number | null | undefined): number | null => + typeof z === "number" && z >= rawBottom && z <= rawTop ? screenYOf(z) : null; + const groundY = inView(centerZ); + const designY = inView(designZ); for (let raw = Math.ceil(rawBottom / yStep) * yStep; raw <= rawTop + 1e-9; raw += yStep) { const screenY = screenYOf(raw); layer.append( @@ -144,26 +148,49 @@ export function createCrossAxes( }), ); } - if (centerY !== null) appendCenterTag(layer, (centerZ as number).toFixed(1), plotLeft, centerY); + if (groundY !== null) { + const at = designY === null ? groundY : dodgeY(groundY, designY, plotTop, plotBottom); + appendCenterTag(layer, (centerZ as number).toFixed(1), plotLeft, at, "ground"); + } + if (designY !== null) + appendCenterTag(layer, (designZ as number).toFixed(1), plotLeft, designY, "design"); }; } +/** 원지반 상자 y — 계획 상자와 `TAG_GAP_PX` 안이면 아래(원지반이 낮으면) · 위로 비킴 · 칸 밖이면 반대(379-3). */ +function dodgeY(groundY: number, designY: number, top: number, bottom: number): number { + if (Math.abs(groundY - designY) >= TAG_GAP_PX) return groundY; + const below = groundY >= designY; // 화면 아래 = 낮은 표고 + const half = CENTER_BOX_H / 2; + const away = designY + (below ? TAG_GAP_PX : -TAG_GAP_PX); + return away - half < top || away + half > bottom + ? designY + (below ? -TAG_GAP_PX : TAG_GAP_PX) + : away; +} + /** 중심고 둥근 상자 — 글 오른끝 = 다른 눈금 자리 · 상자는 Y 축선(`axisX`) 4px 앞에서 멈춤 · * 글 폭은 잴 수 있으면 재고(붙은 뒤) 아니면 어림. */ -function appendCenterTag(layer: SVGGElement, text: string, axisX: number, centerY: number): void { +function appendCenterTag( + layer: SVGGElement, + text: string, + axisX: number, + centerY: number, + kind: "ground" | "design", +): void { + const tone = `b06-chart__center-tag--${kind}`; // 색 = 원지반선 · 설계선(379-3) const textRight = axisX - Y_LABEL_GAP_PX; const box = svgElement("rect", { y: centerY - CENTER_BOX_H / 2, height: CENTER_BOX_H, rx: 3, ry: 3, - class: "b06-chart__center-box", + class: `b06-chart__center-box ${tone}`, }); const label = svgText(text, { x: textRight, y: centerY + 3.5, "text-anchor": "end", - class: "b06-chart__tick b06-chart__center-text", + class: `b06-chart__tick b06-chart__center-text ${tone}`, }); layer.append(box, label); const measured = (label as SVGTextElement).getComputedTextLength?.() ?? 0; diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index de5501110..966789135 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -380,6 +380,7 @@ export function createCrossSectionCard( elevationMid, exaggeration, centerZ: section.center_z ?? null, + designZ: hasDesign ? (designElevation as number) : null, }); renderAxes(cardZoomStates.get(section.station_id) ?? IDENTITY_VIEW); diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index a7934b7ba..b0b8745f3 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -627,6 +627,7 @@ export function createSectionView( elevationMid: plot.elevationMid, exaggeration: Math.max(currentExaggeration, 0.1), centerZ: section.center_z ?? null, + designZ: cardDesignElevation(detail, section) ?? null, }; // 칸 맞춤이 넓힌 가로 범위를 처음 배율로 되좁힘 = 카드 반폭(266-1) const homeScale = base && fitted ? base.pixelsPerMeter / fitted.pixelsPerMeter : 1; diff --git a/B06_Section/B06_Section_UI_Style_Cross.css b/B06_Section/B06_Section_UI_Style_Cross.css index 7f6983c94..6ac2324fa 100644 --- a/B06_Section/B06_Section_UI_Style_Cross.css +++ b/B06_Section/B06_Section_UI_Style_Cross.css @@ -395,16 +395,16 @@ html.is-bm-route .b06-section__notice-bar { font-size: 10px; } -/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 · - 바탕 = 불투명 섞음(겹친 눈금 글을 위에서 가림 · 319-1) */ +/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 축선 앞에서 멈춤 · + 바탕 = 불투명 섞음(겹친 눈금 글을 위에서 가림 · 319-1) · 색 = 원지반선 · 설계선(379-3 · `--b06-tag` = _Cross_Areas.css) */ .b06-chart__center-box { - fill: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface-raised)); - stroke: var(--color-accent); + fill: color-mix(in srgb, var(--b06-tag) 14%, var(--color-surface-raised)); + stroke: var(--b06-tag); stroke-width: 1; } .b06-chart__center-text { - fill: var(--color-accent); + fill: var(--b06-tag); font-weight: var(--font-weight-medium); } diff --git a/B06_Section/B06_Section_UI_Style_Cross_Areas.css b/B06_Section/B06_Section_UI_Style_Cross_Areas.css index 256898f46..59ab22186 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Areas.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Areas.css @@ -12,6 +12,15 @@ --b06-areas-font: clamp(0.6rem, 0.55rem + 0.7cqw, 0.9rem); } +/* Y축 중심고 상자 색(379-3) — 원지반 = 원지반선 · 계획 = 설계선. */ +.b06-chart__center-tag--ground { + --b06-tag: var(--color-chart-1); +} + +.b06-chart__center-tag--design { + --b06-tag: var(--color-royal-amethyst); +} + /* 면적 강조 밴드 — 평소엔 클릭 대상으로만 남고, 켜질 때만 채워진다. */ .b06-chart__area > polygon { fill: transparent; diff --git a/resources/tester/helper_318_cross_card.cjs b/resources/tester/helper_318_cross_card.cjs index e176501a4..c5db0b8c8 100644 --- a/resources/tester/helper_318_cross_card.cjs +++ b/resources/tester/helper_318_cross_card.cjs @@ -283,6 +283,39 @@ const out = {}; hidden_when_out_of_view: draw(800).querySelector(".b06-chart__center-box") === null, absent_without_center: draw(null).querySelector(".b06-chart__center-box") === null, }; + // 379-3 상자 둘 — 원지반(녹) 먼저 · 계획(보라) 나중 · 16 px 안이면 원지반 상자만 비킴 + const yOf = (z) => CROSS_PAD.top + (base.displayMax - z) * base.pixelsPerMeter; + const tagsOf = (centerZ, designZ) => { + const layer = new FakeNode("g"); + createCrossAxes(layer, { ...base, centerZ, designZ })(IDENTITY_VIEW); + const pick = (kind) => { + const all = layer.querySelectorAll(`.b06-chart__center-tag--${kind}`); + const box = all.find((n) => n.classList.contains("b06-chart__center-box")); + const text = all.find((n) => n.classList.contains("b06-chart__center-text")); + return ( + box && { + mid: Number(box.attrs.y) + Number(box.attrs.height) / 2, + text: text?.textContent ?? null, + } + ); + }; + return { + ground: pick("ground"), + design: pick("design"), + order: layer.children.slice(-4).map((node) => node.className), + groundY: yOf(centerZ), + designY: yOf(designZ), + }; + }; + const bottomZ = + base.displayMax - (base.heightPx - CROSS_PAD.bottom - CROSS_PAD.top) / base.pixelsPerMeter; + out.tags = { + apart: tagsOf(876.57, 878.5), + groundLow: tagsOf(876.57, 876.9), + groundHigh: tagsOf(876.9, 876.57), + edge: tagsOf(bottomZ + 0.4, bottomZ + 0.5), // 아래로 비키면 칸 밖 → 위로 + designOnly: tagsOf(null, 876.9), + }; } // ── 2. 아래 띠(318-2 · 318-3) — 중심고 글 없음 · 암 경계 맨 왼쪽 · 토사도 띠 남음 ───── diff --git a/resources/tester/test_318_cross_card.py b/resources/tester/test_318_cross_card.py index 146d0ae88..0ef30c293 100644 --- a/resources/tester/test_318_cross_card.py +++ b/resources/tester/test_318_cross_card.py @@ -80,14 +80,40 @@ def test_center_tag_on_y_axis(result: dict) -> None: assert center["text_x"] == center["tick_x"] # 글 오른끝 = 다른 눈금 자리 assert center["tick_near_center"] >= 1 # 겹친 눈금 글도 그대로(319-1) assert center["on_top"] == [ # 상자 · 글을 맨 뒤에 붙여 위에서 가림(319-1) - "b06-chart__center-box", - "b06-chart__tick b06-chart__center-text", + "b06-chart__center-box b06-chart__center-tag--ground", + "b06-chart__tick b06-chart__center-text b06-chart__center-tag--ground", ] assert center["tick_count"] > 3 assert center["hidden_when_out_of_view"] assert center["absent_without_center"] +def test_two_tags_ground_dodges(result: dict) -> None: + """379-3 — 원지반 상자 먼저 · 계획 상자 나중(위) · 16 px 안이면 원지반만 비킴 · 값 그대로.""" + tags = result["tags"] + apart = tags["apart"] + assert abs(apart["ground"]["mid"] - apart["groundY"]) < 0.01 # 멀면 제자리 + assert abs(apart["design"]["mid"] - apart["designY"]) < 0.01 + assert [c.split()[-1] for c in apart["order"]] == [ + "b06-chart__center-tag--ground", + "b06-chart__center-tag--ground", + "b06-chart__center-tag--design", + "b06-chart__center-tag--design", + ] + low = tags["groundLow"] # 원지반이 낮음 → 아래로 + assert abs(low["design"]["mid"] - low["designY"]) < 0.01 + assert abs(low["ground"]["mid"] - (low["designY"] + 16)) < 0.01 + assert low["ground"]["text"] == "876.6" and low["design"]["text"] == "876.9" + high = tags["groundHigh"] # 원지반이 높음 → 위로 + assert abs(high["ground"]["mid"] - (high["designY"] - 16)) < 0.01 + assert high["ground"]["text"] == "876.9" and high["design"]["text"] == "876.6" + edge = tags["edge"] # 아래가 칸 밖 → 반대(위) + assert edge["groundY"] > edge["designY"] + assert abs(edge["ground"]["mid"] - (edge["designY"] - 16)) < 0.01 + only = tags["designOnly"] + assert only.get("ground") is None and abs(only["design"]["mid"] - only["designY"]) < 0.01 + + def test_footer_without_center_text(result: dict) -> None: footer = result["footer"] assert "중심고" not in footer["rock_text"] @@ -109,6 +135,13 @@ def test_center_wired_into_card_and_modal() -> None: assert "centerZ: section.center_z ?? null" in _src( "B06_Section", "B06_Section_UI_Section_View.ts" ) + # 379-3 계획 상자 = 십자선 계획고(카드 designElevation · 모달 cardDesignElevation) + assert "designZ: hasDesign ? (designElevation as number) : null" in _src( + "B06_Section", "B06_Section_UI_Cross_View.ts" + ) + assert "designZ: cardDesignElevation(detail, section) ?? null" in _src( + "B06_Section", "B06_Section_UI_Section_View.ts" + ) # ── 318-2 · 318-3 양식 ────────────────────────────────────────────────────── @@ -127,7 +160,10 @@ def test_css_layout() -> None: assert "position: absolute" not in rock and "translateX" not in rock # 왼쪽 맞춤 assert 'content: "\\200b"' in _rule(css, ".b06-cross-card > footer:empty::before") box = _rule(css, ".b06-chart__center-box") - assert "var(--color-accent)" in box # 테마 색 + assert "var(--b06-tag)" in box # 색 = 갈래 변수(379-3) + areas = _src("B06_Section", "B06_Section_UI_Style_Cross_Areas.css") + assert "var(--color-chart-1)" in _rule(areas, ".b06-chart__center-tag--ground") # 원지반선 + assert "var(--color-royal-amethyst)" in _rule(areas, ".b06-chart__center-tag--design") # 설계선 assert "font-size" not in _rule(css, ".b06-chart__center-text") # 글자 크기 = 눈금 그대로