feat(b06): 횡단 카드 — 단추 줄 빈자리 누름 = 카드 고름 · 중심고 = Y축 라벨 둥근 상자(눈금과 같은 글자 · 소수 첫째 자리 · 축선 앞에서 멈춤 · 카드 · 상세 모달) · 아래 띠 중심고 글 뺌 · 암 경계 왼쪽 · 카드 = 내용 높이(아래 여유 없앰 · 빈 띠 높이 그대로) (318-1~3 · 요구 215 · 216)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
This commit is contained in:
@@ -26,6 +26,8 @@ export interface CrossAxesOptions {
|
|||||||
/** 표고 과장 기준점과 배율 — 화면 표고 → 실제 표고 환산에 쓴다. */
|
/** 표고 과장 기준점과 배율 — 화면 표고 → 실제 표고 환산에 쓴다. */
|
||||||
elevationMid: number;
|
elevationMid: number;
|
||||||
exaggeration: number;
|
exaggeration: number;
|
||||||
|
/** 중심고(실제 표고) — Y축 라벨 자리에 둥근 상자로(318-2 · 소수 첫째 자리). 없으면 안 그림. */
|
||||||
|
centerZ?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** X 눈금 라벨("-12" 등)이 겹치지 않는 최소 간격(px). */
|
/** X 눈금 라벨("-12" 등)이 겹치지 않는 최소 간격(px). */
|
||||||
@@ -36,6 +38,11 @@ const X_TARGET_TICKS = 7;
|
|||||||
/** 표고(Y) 눈금 최소 간격(m) — 1m 아래 단위는 표기하지 않는다. */
|
/** 표고(Y) 눈금 최소 간격(m) — 1m 아래 단위는 표기하지 않는다. */
|
||||||
const MIN_ELEVATION_STEP_M = 1;
|
const MIN_ELEVATION_STEP_M = 1;
|
||||||
const Y_TARGET_TICKS = 10;
|
const Y_TARGET_TICKS = 10;
|
||||||
|
/** Y 눈금 글 오른끝 ↔ Y 축선(px). */
|
||||||
|
const Y_LABEL_GAP_PX = 7;
|
||||||
|
/** 중심고 상자 — 글자 좌우 여백 · 높이(px). 글자 크기는 눈금과 같음(10px). */
|
||||||
|
const CENTER_PAD_X = 3;
|
||||||
|
const CENTER_BOX_H = 14;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 축 레이어를 그리는 함수를 만든다. 줌·팬이 바뀔 때마다 그 상태로 다시 부르면 된다.
|
* 축 레이어를 그리는 함수를 만든다. 줌·팬이 바뀔 때마다 그 상태로 다시 부르면 된다.
|
||||||
@@ -108,9 +115,16 @@ export function createCrossAxes(
|
|||||||
MIN_ELEVATION_STEP_M,
|
MIN_ELEVATION_STEP_M,
|
||||||
);
|
);
|
||||||
const yDecimals = Math.max(0, Math.ceil(-Math.log10(yStep) - 1e-9));
|
const yDecimals = Math.max(0, Math.ceil(-Math.log10(yStep) - 1e-9));
|
||||||
|
const screenYOf = (raw: number) =>
|
||||||
|
ty + scale * y(options.elevationMid + (raw - options.elevationMid) * options.exaggeration);
|
||||||
|
// 중심고 상자 자리 — 보이는 범위 안일 때만 · 그 높이 눈금 글자는 상자에 양보
|
||||||
|
const centerZ = options.centerZ;
|
||||||
|
const centerY =
|
||||||
|
typeof centerZ === "number" && centerZ >= rawBottom && centerZ <= rawTop
|
||||||
|
? screenYOf(centerZ)
|
||||||
|
: null;
|
||||||
for (let raw = Math.ceil(rawBottom / yStep) * yStep; raw <= rawTop + 1e-9; raw += yStep) {
|
for (let raw = Math.ceil(rawBottom / yStep) * yStep; raw <= rawTop + 1e-9; raw += yStep) {
|
||||||
const display = options.elevationMid + (raw - options.elevationMid) * options.exaggeration;
|
const screenY = screenYOf(raw);
|
||||||
const screenY = ty + scale * y(display);
|
|
||||||
layer.append(
|
layer.append(
|
||||||
svgElement("line", {
|
svgElement("line", {
|
||||||
x1: plotLeft,
|
x1: plotLeft,
|
||||||
@@ -119,13 +133,41 @@ export function createCrossAxes(
|
|||||||
y2: screenY,
|
y2: screenY,
|
||||||
class: "b06-chart__grid",
|
class: "b06-chart__grid",
|
||||||
}),
|
}),
|
||||||
|
);
|
||||||
|
if (centerY !== null && Math.abs(screenY - centerY) < CENTER_BOX_H) continue;
|
||||||
|
layer.append(
|
||||||
svgText(raw.toFixed(yDecimals), {
|
svgText(raw.toFixed(yDecimals), {
|
||||||
x: plotLeft - 7,
|
x: plotLeft - Y_LABEL_GAP_PX,
|
||||||
y: screenY + 3,
|
y: screenY + 3,
|
||||||
"text-anchor": "end",
|
"text-anchor": "end",
|
||||||
class: "b06-chart__tick",
|
class: "b06-chart__tick",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (centerY !== null) appendCenterTag(layer, (centerZ as number).toFixed(1), plotLeft, centerY);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 중심고 둥근 상자 — 글 오른끝 = 다른 눈금 자리 · 상자는 Y 축선(`axisX`) 4px 앞에서 멈춤 ·
|
||||||
|
* 글 폭은 잴 수 있으면 재고(붙은 뒤) 아니면 어림. */
|
||||||
|
function appendCenterTag(layer: SVGGElement, text: string, axisX: number, centerY: number): void {
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
const label = svgText(text, {
|
||||||
|
x: textRight,
|
||||||
|
y: centerY + 3.5,
|
||||||
|
"text-anchor": "end",
|
||||||
|
class: "b06-chart__tick b06-chart__center-text",
|
||||||
|
});
|
||||||
|
layer.append(box, label);
|
||||||
|
const measured = (label as SVGTextElement).getComputedTextLength?.() ?? 0;
|
||||||
|
const width = (measured > 0 ? measured : text.length * 5.8) + CENTER_PAD_X * 2;
|
||||||
|
box.setAttribute("x", String(textRight + CENTER_PAD_X - width));
|
||||||
|
box.setAttribute("width", String(width));
|
||||||
|
}
|
||||||
|
|||||||
@@ -172,11 +172,9 @@ export function appendCardFooter(
|
|||||||
rockBoundary?: RockBoundaryControl,
|
rockBoundary?: RockBoundaryControl,
|
||||||
cutSlope?: CutSlopeControl,
|
cutSlope?: CutSlopeControl,
|
||||||
): void {
|
): void {
|
||||||
|
// 중심고는 그래프 Y축 라벨 상자로 옮김(318-2 · `_Cross_Axes`) — 하단 띠는 암 제어만 · 비어도 띠 높이 그대로
|
||||||
const footer = document.createElement("footer");
|
const footer = document.createElement("footer");
|
||||||
const center = document.createElement("span");
|
// 암 경계선 제어는 하단 정보 행 왼쪽(318-2) — 암 지반만.
|
||||||
center.textContent = `${L("B06_Profile_View_CenterElevation")} ${section.center_z?.toFixed(2) ?? "-"}m`;
|
|
||||||
footer.append(center);
|
|
||||||
// 암 경계선 제어는 하단 정보 행 가운데(2026-08-02) — 암 지반만.
|
|
||||||
if (rockBoundary && section.design?.geometry_preset === "rock") {
|
if (rockBoundary && section.design?.geometry_preset === "rock") {
|
||||||
const rockControl = buildRockBoundaryControl(section, rockBoundary);
|
const rockControl = buildRockBoundaryControl(section, rockBoundary);
|
||||||
rockControl.classList.add("b06-cross-card__rockb");
|
rockControl.classList.add("b06-cross-card__rockb");
|
||||||
|
|||||||
@@ -346,8 +346,12 @@ export function buildDesignControls(
|
|||||||
};
|
};
|
||||||
const bar = document.createElement("div");
|
const bar = document.createElement("div");
|
||||||
bar.className = "b06-design";
|
bar.className = "b06-design";
|
||||||
// 컨트롤 상호작용이 카드 선택 클릭으로 전파되지 않게 한다.
|
// 조작(단추 · 입력 · ⋯ 펼침)만 카드 선택 클릭으로 안 넘김 — 빈자리 누름은 카드를 고름(318-1).
|
||||||
bar.addEventListener("click", (event) => event.stopPropagation());
|
bar.addEventListener("click", (event) => {
|
||||||
|
if ((event.target as Element).closest("button, input, select, label, details")) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 경사 방향 선택이 필요한 경우 = 양절(both_cut)·양성(both_fill). 편절은 절토측 자동.
|
// 경사 방향 선택이 필요한 경우 = 양절(both_cut)·양성(both_fill). 편절은 절토측 자동.
|
||||||
// 양성은 측구가 없어도 노면 기울기 방향(생략된 측구 방향)을 여기서 지정한다(N-2-3).
|
// 양성은 측구가 없어도 노면 기울기 방향(생략된 측구 방향)을 여기서 지정한다(N-2-3).
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ export function createCrossSectionCard(
|
|||||||
displayMax,
|
displayMax,
|
||||||
elevationMid,
|
elevationMid,
|
||||||
exaggeration,
|
exaggeration,
|
||||||
|
centerZ: section.center_z ?? null,
|
||||||
});
|
});
|
||||||
renderAxes(cardZoomStates.get(section.station_id) ?? IDENTITY_VIEW);
|
renderAxes(cardZoomStates.get(section.station_id) ?? IDENTITY_VIEW);
|
||||||
|
|
||||||
|
|||||||
@@ -599,6 +599,7 @@ export function createSectionView(
|
|||||||
displayMax: plot.displayMax,
|
displayMax: plot.displayMax,
|
||||||
elevationMid: plot.elevationMid,
|
elevationMid: plot.elevationMid,
|
||||||
exaggeration: Math.max(currentExaggeration, 0.1),
|
exaggeration: Math.max(currentExaggeration, 0.1),
|
||||||
|
centerZ: section.center_z ?? null,
|
||||||
};
|
};
|
||||||
// 칸 맞춤이 넓힌 가로 범위를 처음 배율로 되좁힘 = 카드 반폭(266-1)
|
// 칸 맞춤이 넓힌 가로 범위를 처음 배율로 되좁힘 = 카드 반폭(266-1)
|
||||||
const homeScale = base && fitted ? base.pixelsPerMeter / fitted.pixelsPerMeter : 1;
|
const homeScale = base && fitted ? base.pixelsPerMeter / fitted.pixelsPerMeter : 1;
|
||||||
|
|||||||
@@ -121,6 +121,8 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(480px, 100%), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(min(480px, 100%), 1fr));
|
||||||
gap: var(--spacing-16);
|
gap: var(--spacing-16);
|
||||||
|
/* 카드 = 내용 높이만큼 — 같은 줄 큰 카드에 맞춰 늘어나 아래 띠 밑이 비던 것(318-3 · 띠 높이 그대로) */
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 알림 줄(138-3) — 카드 칸 맨 위 고정 · 알림 없어도 줄 유지 · 오른쪽 끝 열 수 칸.
|
/* 알림 줄(138-3) — 카드 칸 맨 위 고정 · 알림 없어도 줄 유지 · 오른쪽 끝 열 수 칸.
|
||||||
@@ -344,6 +346,11 @@ html.is-bm-route .b06-section__notice-bar {
|
|||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 빈 띠(토사 측점 · 중심고는 Y축으로 옮김 318-2)도 글 한 줄 높이 그대로 */
|
||||||
|
.b06-cross-card > footer:empty::before {
|
||||||
|
content: "\200b";
|
||||||
|
}
|
||||||
|
|
||||||
.b06-section__empty {
|
.b06-section__empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
@@ -388,6 +395,18 @@ html.is-bm-route .b06-section__notice-bar {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 */
|
||||||
|
.b06-chart__center-box {
|
||||||
|
fill: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface-raised));
|
||||||
|
stroke: var(--color-accent);
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.b06-chart__center-text {
|
||||||
|
fill: var(--color-accent);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
/* 구조물(비정규) 측점 이름 — 종단면도 측점선 상단 표기. */
|
/* 구조물(비정규) 측점 이름 — 종단면도 측점선 상단 표기. */
|
||||||
.b06-chart__structure-label {
|
.b06-chart__structure-label {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
@@ -607,24 +626,14 @@ html.is-bm-route .b06-section__notice-bar {
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 암 경계선 제어(E-7) — 하단 정보 행 **가운데 맞춤**(2026-08-06 사용자 지시).
|
/* 암 경계선 제어(E-7) — 하단 정보 행 **왼쪽 맞춤**(318-2 · 중심고는 그래프 Y축 라벨 상자로 옮김). */
|
||||||
방위각 삭제로 space-between 3분할 배치가 깨져 좌측에 붙었다 — 절대 배치로
|
|
||||||
행 중앙에 고정한다(중심고·반폭 버튼과 겹치지 않는 폭). */
|
|
||||||
.b06-cross-card > footer {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b06-cross-card__rockb {
|
.b06-cross-card__rockb {
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-radius: var(--radius-inputs);
|
border-radius: var(--radius-inputs);
|
||||||
padding: 1px 4px;
|
padding: 1px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 측점별 암 절토 경사(2026-09-07) — 암 경계선이 행 가운데를 절대 배치로 쓰므로
|
/* 측점별 암 절토 경사(2026-09-07) — 행의 **오른쪽 끝**(암 경계 = 왼쪽). */
|
||||||
이 칸은 행의 **오른쪽 끝**에 둔다. 중심고(왼쪽)와 겹치지 않는다. */
|
|
||||||
.b06-cross-card__cutslope {
|
.b06-cross-card__cutslope {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user