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") # 글자 크기 = 눈금 그대로