Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_5
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user