feat(B06 횡단): Y축 표고 상자 둘 — 원지반고(녹) · 계획 중심고(보라) · 겹치면 원지반만 비킴 (379-3)
- createCrossAxes designZ 옵션 · 원지반 상자 먼저 · 계획 상자 나중(위) · 색 = 원지반선 · 설계선 - y 차 16 px 미만이면 원지반 상자만 위아래 관계 따라 비킴 · 칸 밖이면 반대 · 값 그대로 - 배선 = 카드(십자선 계획고) · 상세 모달(cardDesignElevation) · 시험 test_318 겹침 경우 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) — 중심고 글 없음 · 암 경계 맨 왼쪽 · 토사도 띠 남음 ─────
|
||||
|
||||
@@ -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") # 글자 크기 = 눈금 그대로
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user