fix(B06): 횡단 줌·팬에 X·Y 축 눈금 동기화
도형만 transform으로 확대되고 축은 고정이라 확대·이동 후 눈금값이 실제 위치와 어긋났다(2026-08-23 사용자 보고). 축·격자를 별도 레이어(_UI_Cross_Axes)로 옮기고 줌·팬 상태가 바뀔 때마다 보이는 범위를 역산해 다시 그린다. - 눈금 개수·간격도 그때 보이는 범위로 다시 고른다(1·2·5 계열, X 목표 7·Y 목표 10, 라벨 겹침 하한 X 48px·Y 14px). 확대할수록 촘촘·소수 단위로 자동 전환. - X 눈금이 7등분 고정(비정수 간격)이던 것도 같은 규칙으로 정리. - Cross_View는 축 코드가 빠져 622줄. 자체검증: node tmp/tests/test_cross_axes_zoom.mjs (원배율·4배 확대에서 눈금이 플롯 안에 있고, 같은 offset의 도형 자리와 눈금 자리가 일치), tsc --noEmit 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import { buildStructurePanel } from "./B06_Section_UI_Cross_Structure_Panel";
|
||||
import { attachZoomPan, buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import type { CrossWidthActions, ZoomPanState } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import { toeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
||||
import { createCrossAxes, IDENTITY_VIEW } from "./B06_Section_UI_Cross_Axes";
|
||||
import type { RevetHighlightSetter, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
@@ -44,7 +45,6 @@ import {
|
||||
type DesignChangeHandler,
|
||||
emptyView,
|
||||
L,
|
||||
niceTickStep,
|
||||
stationLabel,
|
||||
svgElement,
|
||||
svgText,
|
||||
@@ -274,16 +274,8 @@ export function createCrossSectionCard(
|
||||
if (!metrics) {
|
||||
card.append(emptyView(L("B06_Profile_View_NoCross")));
|
||||
} else {
|
||||
const {
|
||||
sourceSamples,
|
||||
minOffset,
|
||||
maxOffset,
|
||||
elevationMid,
|
||||
displaySpan,
|
||||
displayMax,
|
||||
pixelsPerMeter,
|
||||
heightPx,
|
||||
} = metrics;
|
||||
const { sourceSamples, maxOffset, elevationMid, displayMax, pixelsPerMeter, heightPx } =
|
||||
metrics;
|
||||
const hasDesign = designElevation !== undefined && Number.isFinite(designElevation);
|
||||
const valid = sourceSamples.filter(validElevation);
|
||||
const exaggeration = Math.max(verticalExaggeration, 0.1);
|
||||
@@ -301,61 +293,21 @@ export function createCrossSectionCard(
|
||||
});
|
||||
svg.append(svgElement("rect", { width: widthPx, height: heightPx, class: "b06-chart__bg" }));
|
||||
|
||||
const xTicks = Array.from(
|
||||
{ length: 7 },
|
||||
(_, index) => minOffset + ((maxOffset - minOffset) * index) / 6,
|
||||
);
|
||||
for (const tick of xTicks) {
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
x1: x(tick),
|
||||
y1: CROSS_PAD.top,
|
||||
x2: x(tick),
|
||||
y2: heightPx - CROSS_PAD.bottom,
|
||||
class: "b06-chart__grid",
|
||||
}),
|
||||
svgText(Math.abs(tick) < 1e-6 ? "0" : tick.toFixed(0), {
|
||||
x: x(tick),
|
||||
y: heightPx - CROSS_PAD.bottom + 16,
|
||||
"text-anchor": "middle",
|
||||
class: "b06-chart__tick",
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Y축 눈금: 표고 범위를 10칸 안팎으로 나누되 눈금값이 1·2·5 계열로 딱 떨어지게 한다
|
||||
// (종단면도와 같은 규칙). 5등분 고정은 눈금값이 소수로 흘러 표고를 못 읽었다
|
||||
// (2026-08-23 사용자 지시). 라벨 겹침 방지로 눈금 간격은 14px 이상 띄운다.
|
||||
// 눈금 범위는 **실제로 보이는 플롯 상·하단**(y() 기준)에서 뽑는다 — elevationMid를
|
||||
// 중심으로 잡으면 displayMax의 면적표 여유(headroom)만큼 아래로 밀려 X축 라벨 줄
|
||||
// 아래에 격자선이 삐져나왔다(2026-08-23 사용자 보고).
|
||||
const rawDisplaySpan = displaySpan / exaggeration;
|
||||
const rawTop = elevationMid + (displayMax - elevationMid) / exaggeration;
|
||||
const rawBottom = rawTop - rawDisplaySpan;
|
||||
const plotHeightPx = heightPx - CROSS_PAD.top - CROSS_PAD.bottom;
|
||||
const tickStep = niceTickStep(rawDisplaySpan, 10, Math.floor(plotHeightPx / 14));
|
||||
const tickDecimals = Math.max(0, Math.ceil(-Math.log10(tickStep) - 1e-9));
|
||||
for (
|
||||
let tick = Math.ceil(rawBottom / tickStep) * tickStep;
|
||||
tick <= rawTop + 1e-9;
|
||||
tick += tickStep
|
||||
) {
|
||||
const displayTick = elevationMid + (tick - elevationMid) * exaggeration;
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
x1: CROSS_PAD.left,
|
||||
y1: y(displayTick),
|
||||
x2: widthPx - CROSS_PAD.right,
|
||||
y2: y(displayTick),
|
||||
class: "b06-chart__grid",
|
||||
}),
|
||||
svgText(tick.toFixed(tickDecimals), {
|
||||
x: CROSS_PAD.left - 7,
|
||||
y: y(displayTick) + 3,
|
||||
"text-anchor": "end",
|
||||
class: "b06-chart__tick",
|
||||
}),
|
||||
);
|
||||
}
|
||||
// 축·격자는 별도 레이어에 그리고, 줌·팬이 바뀔 때마다 **보이는 범위로 다시** 그린다
|
||||
// (2026-08-23 사용자 보고: 확대하면 축 값과 도형이 어긋남). 눈금 개수·간격도 그때
|
||||
// 보이는 범위에 맞춰 다시 고른다.
|
||||
const axisLayer = svgElement("g", { class: "b06-chart__axes" }) as SVGGElement;
|
||||
svg.append(axisLayer);
|
||||
const renderAxes = createCrossAxes(axisLayer, {
|
||||
widthPx,
|
||||
heightPx,
|
||||
pixelsPerMeter,
|
||||
maxOffset,
|
||||
displayMax,
|
||||
elevationMid,
|
||||
exaggeration,
|
||||
});
|
||||
renderAxes(cardZoomStates.get(section.station_id) ?? IDENTITY_VIEW);
|
||||
|
||||
// 확대·축소·팬이 닿는 범위 = 이 레이어 안(2026-08-08 사용자 지시. 축·눈금·축 이름은 제자리).
|
||||
// clip 그룹은 고정하고 안쪽 plotLayer만 transform으로 움직인다 — clip을 transform
|
||||
@@ -537,7 +489,10 @@ export function createCrossSectionCard(
|
||||
widthPx,
|
||||
heightPx,
|
||||
cardZoomStates.get(section.station_id),
|
||||
(state) => cardZoomStates.set(section.station_id, state),
|
||||
(state) => {
|
||||
cardZoomStates.set(section.station_id, state);
|
||||
renderAxes(state);
|
||||
},
|
||||
);
|
||||
// 절·성토 면적값은 그래프 중상단 오버레이로 표시(E-4). 값 칸은 항상 강조 토글이다.
|
||||
const readout = buildAreaReadout(section.design, toggleArea);
|
||||
|
||||
Reference in New Issue
Block a user