Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_5

This commit is contained in:
2026-10-12 21:54:48 +09:00
5 changed files with 271 additions and 0 deletions
@@ -0,0 +1,33 @@
/* =============================================================================
* B06_Section_UI_Cross_Ford_Linked.ts
* 물넘이포장 연결 표기(294-6) — 월류 폭으로 옆 측점 카드에 걸쳐 그린 포장층 · 월류 높이선을
* 흙막이 연동 형상(79-6 `.is-linked`)과 같은 꼴로 · 호버 글에 주인 측점. 주인 측점 카드는 그대로 ·
* 고름 표식(`data-structure-id`)은 주인 구조물 그대로.
* ========================================================================== */
import { stationLabel } from "../common_util/common_util_station";
import type { CrossSection } from "./B06_Section_Api_Fetch";
const SVG_NS = "http://www.w3.org/2000/svg";
/** 연결 꼴을 입힐 물넘이 그림 — 포장층 · 월류 높이선 */
const LINKED_CLASSES = ["b06-chart__ford-pavement", "b06-chart__ford-deck-plan"];
/** `from` 번째 자식부터(방금 그린 물넘이포장) 옆 카드면 연결 꼴 · 주인 측점 글. 붙였으면 true */
export function markLinkedFordPavement(
svg: SVGElement,
from: number,
section: Pick<CrossSection, "ford_pavement" | "chainage_m">,
stationInterval: number,
): boolean {
const owner = section.ford_pavement?.chainage_m;
if (typeof owner !== "number" || Math.abs(owner - section.chainage_m) < 1e-3) return false;
const name = `물넘이포장 · ${stationLabel(owner, stationInterval)} 연결`;
for (const el of [...svg.children].slice(from)) {
if (!LINKED_CLASSES.some((className) => el.classList.contains(className))) continue;
el.classList.add("is-linked");
const title = document.createElementNS(SVG_NS, "title");
title.textContent = name;
el.append(title);
}
return true;
}
+4
View File
@@ -8,6 +8,7 @@
* ========================================================================== */
import { withoutGuardedFill } from "./B06_Section_UI_Cross_Body_Guard";
import { markLinkedFordPavement } from "./B06_Section_UI_Cross_Ford_Linked";
import type { CrossSection } from "./B06_Section_Api_Fetch";
import {
appendCrossAreaBands,
@@ -446,6 +447,7 @@ export function createCrossSectionCard(
// 포장층 → 설계선 → 암 경계선 순으로 겹쳐, 설계선이 포장 박스 위에 오게 한다.
// 물넘이포장은 노면 자체가 파인 자리라 포장층도 그 바닥을 따른다 — 그렸으면 일반
// 포장 박스는 건너뛴다(두 겹으로 깔리면 색 구분이 사라진다).
const fordFrom = plotLayer.children.length;
const fordPaved = appendFordPavementOverlay(
plotLayer,
section.ford_pavement,
@@ -453,6 +455,8 @@ export function createCrossSectionCard(
x,
toDisplayY,
);
// 월류 폭으로 걸친 옆 카드 = 흙막이 연동 꼴 · 주인 측점 글(294-6)
markLinkedFordPavement(plotLayer, fordFrom, section, stationInterval);
if (!fordPaved)
appendPavementOverlay(plotLayer, section.design, x, toDisplayY, section.pavement);
// 맹암거(288-1) — 노면 아래 암거 높이만큼 돌 무늬 형상
@@ -316,6 +316,17 @@
stroke-dasharray: 4 2;
}
/* 월류 폭으로 옆 측점에 걸친 물넘이포장(294-6) — 연동 형상과 같은 꼴 · 고름 강조(is-active)가 먼저. */
.b06-chart__ford-pavement.is-linked:not(.is-active) {
fill: color-mix(in srgb, #6f8696 30%, transparent);
stroke: #6f8696;
stroke-dasharray: 4 2;
}
.b06-chart__ford-deck-plan.is-linked {
stroke: #6f8696;
}
.b06-chart__culvert-revet.is-active {
fill: color-mix(in srgb, var(--color-danger) 45%, transparent);
stroke: var(--color-danger);
@@ -0,0 +1,142 @@
/* 294-6 물넘이 연결 표기 헬퍼 — 진짜 TS 를 Node 로 돌려 표준출력 JSON 한 벌로.
* 옆 카드(물넘이 측점 ≠ 카드 측점) · 주인 카드에 물넘이포장을 그리고 연결 꼴 · 호버 글 · 고름 표식을 잼.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
const load = (file) => {
if (cache.has(file)) return cache.get(file);
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(file, box.exports);
const near = (m) => {
if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`));
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
return new Proxy({}, { get: () => () => {} });
};
new Function("exports", "module", "require", js)(box.exports, box, near);
return box.exports;
};
// DOM 흉내 — 속성 · 자식 · classList · 글만
class Node {
constructor(tag) {
this.tag = tag;
this.attrs = {};
this.children = [];
this.dataset = {};
this.style = {};
this.textContent = "";
}
setAttribute(k, v) {
this.attrs[k] = String(v);
}
getAttribute(k) {
return this.attrs[k] ?? null;
}
hasAttribute(k) {
return k in this.attrs;
}
append(...kids) {
this.children.push(...kids);
}
get classList() {
const names = () => (this.attrs.class ?? "").split(" ").filter(Boolean);
const set = (list) => (this.attrs.class = list.join(" "));
return {
contains: (c) => names().includes(c),
add: (c) => names().includes(c) || set([...names(), c]),
toggle: (c, on) =>
set(on ? [...names().filter((n) => n !== c), c] : names().filter((n) => n !== c)),
};
}
querySelectorAll(selector) {
const attr = /^\[([\w-]+)\]$/.exec(selector)?.[1];
const all = this.children.flatMap((k) => [k, ...k.querySelectorAll(selector)]);
return all.filter((k) => attr && k.hasAttribute(attr));
}
}
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const pave = load(B06("B06_Section_UI_Cross_Ford_Pavement.ts"));
const linked = load(B06("B06_Section_UI_Cross_Ford_Linked.ts"));
const shapePick = load(B06("B06_Section_UI_Cross_Shape_Pick.ts"));
const design = {
section_mode: "left_cut",
tilted: true,
cross_slope_pct: 5,
design_elevation_m: 100,
pavement_thickness_m: 0.2,
paved: true,
road_edges: {
left: { offset_m: 3, elevation_m: 100.15 },
right: { offset_m: -3, elevation_m: 99.85 },
},
carriageway_edges: {
left: { offset_m: 2.5, elevation_m: 100.125 },
right: { offset_m: -2.5, elevation_m: 99.875 },
},
design_line: [
{ offset_m: 6, elevation_m: 103 },
{ offset_m: 3, elevation_m: 100.15 },
{ offset_m: -3, elevation_m: 99.85 },
{ offset_m: -7, elevation_m: 97.2 },
],
};
// 검증 프로젝트 꼴 — 4+10(물넘이 · 폭 10) → 4+15 카드에도 포장 · 측점 간격 20
const spec = { chainage_m: 90, span_m: 10, depth_m: 0.4, slope_pct: 5 };
const draw = (chainage) => {
const svg = new Node("g");
const before = new Node("polyline"); // 앞서 그린 것(세월교 점선 등) — 연결 꼴 밖
before.setAttribute("class", "b06-chart__ford-deck-plan");
svg.append(before);
const from = svg.children.length;
pave.appendFordPavementOverlay(
svg,
spec,
design,
(o) => o,
(e) => e,
);
const marked = linked.markLinkedFordPavement(
svg,
from,
{ ford_pavement: spec, chainage_m: chainage },
20,
);
const shapes = svg.children
.slice(from)
.filter((k) =>
["b06-chart__ford-pavement", "b06-chart__ford-deck-plan"].some((c) =>
k.classList.contains(c),
),
);
shapePick.highlightShapes(svg, "pipe-90.00");
return {
marked,
classes: shapes.map((k) => k.attrs.class),
titles: shapes.map((k) =>
k.children.filter((c) => c.tag === "title").map((c) => c.textContent),
),
pick: shapes
.filter((k) => k.hasAttribute("data-structure-id"))
.map((k) => k.attrs["data-structure-id"]),
beforeClass: before.attrs.class,
};
};
process.stdout.write(
JSON.stringify({
neighbor: draw(95),
owner: draw(90),
noSpec: linked.markLinkedFordPavement(new Node("g"), 0, { chainage_m: 95 }, 20),
}),
);
@@ -0,0 +1,81 @@
"""294-6 물넘이 월류 폭으로 옆 측점 카드에 그려진 포장 = 연결 표기(요구 190-3).
못박는 것
- 옆 카드(물넘이 측점 ≠ 카드 측점) = 포장층 · 월류 높이선에 `is-linked`(흙막이 연동 꼴) ·
호버 글 「물넘이포장 · 4+10 연결」 · 앞서 그린 것(세월교 점선)은 그대로.
- 주인 카드 = 옛 꼴(연결 표식 · 글 없음).
- 고름 = 주인 구조물 그대로(`data-structure-id` pipe-90.00 · 강조 켜짐) · CSS 연결 꼴이 강조를 안 덮음.
- 카드 그리기(`Cross_View`)가 물넘이포장 바로 뒤에 표기를 부름.
진짜 TS 는 `helper_294_6_ford_linked_mark.cjs` 가 Node 로 돌림. 화면은 브레인 확인.
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
reason="프론트엔드 의존성 미설치",
)
@pytest.fixture(scope="module")
def out() -> dict:
proc = subprocess.run(
["node", str(HERE / "helper_294_6_ford_linked_mark.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_옆_카드_연결_꼴_호버_글(out):
neighbor = out["neighbor"]
assert neighbor["marked"] is True
assert len(neighbor["classes"]) == 2
assert all("is-linked" in c.split() for c in neighbor["classes"])
assert neighbor["titles"] == [["물넘이포장 · 4+10 연결"]] * 2
assert neighbor["beforeClass"] == "b06-chart__ford-deck-plan"
def test_주인_카드_옛_꼴(out):
owner = out["owner"]
assert owner["marked"] is False
assert not any("is-linked" in c.split() for c in owner["classes"])
assert owner["titles"] == [[], []]
assert out["noSpec"] is False
def test_고름은_주인_구조물(out):
for card in (out["neighbor"], out["owner"]):
assert card["pick"] == ["pipe-90.00"]
assert "is-active" in card["classes"][0].split()
def test_CSS_연결_꼴_강조가_먼저():
css = (ROOT / "B06_Section" / "B06_Section_UI_Style_Cross_Areas.css").read_text(
encoding="utf-8"
)
assert ".b06-chart__ford-pavement.is-linked:not(.is-active)" in css
assert ".b06-chart__ford-deck-plan.is-linked" in css
def test_카드가_물넘이포장_뒤에_표기_부름():
view = (ROOT / "B06_Section" / "B06_Section_UI_Cross_View.ts").read_text(encoding="utf-8")
start = view.index("const fordFrom = plotLayer.children.length;")
call = view.index("appendFordPavementOverlay(", start)
mark = view.index(
"markLinkedFordPavement(plotLayer, fordFrom, section, stationInterval);", call
)
assert start < call < mark