Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_4
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
* 아래 = B07 횡단 수량표(값 · 칸 배치 모두 서버 `cross-quantity-table` 한 길 · 캐시 설계로 · 열 때 부름).
|
||||
* 공용 큰 창(`wide`) · 공용 막(밖 누르면 닫힘) · 스크롤 없음(150-2).
|
||||
* 그림 = 공용 줌 · 팬(164-1 · 휠 = 커서 고정 줌 · 가운데 단추 팬 · 더블클릭 원복 · [+ − ⤢]) —
|
||||
* 복제 자식을 g 하나로 감싸 축 · 눈금째 한 장으로 움직임.
|
||||
* 카드처럼 도형 층만 움직이고 축 · 눈금은 제자리에서 보이는 범위로 다시 그림(179-3).
|
||||
* 모달이 열린 채 카드를 다시 그리면 그림 · 표를 새로 맞춤.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -30,14 +30,19 @@ import {
|
||||
type ZoomPanState,
|
||||
} from "../A00_Common/b_svg_zoom_pan";
|
||||
import { buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom";
|
||||
import { createCrossAxes, IDENTITY_VIEW, type CrossAxesOptions } from "./B06_Section_UI_Cross_Axes";
|
||||
|
||||
/** 페이지가 한 번 걸어 줌(`_UI_Page_Design_Sync`) — 카드 줄은 프로젝트 · 노선을 모름. */
|
||||
export interface CrossDetailHost {
|
||||
projectId: string;
|
||||
routeId: () => number | null;
|
||||
detail: () => SectionDetailResponse | null;
|
||||
/** 칸 크기(W × H px)로 새로 그린 카드 svg(173-4 · 뷰 `detailChart`). */
|
||||
chart: (stationId: string, widthPx: number, heightPx: number) => SVGSVGElement | null;
|
||||
/** 칸 크기(W × H px)로 새로 그린 카드 svg + 축 옵션(173-4 · 179-3 · 뷰 `detailChart`). */
|
||||
chart: (
|
||||
stationId: string,
|
||||
widthPx: number,
|
||||
heightPx: number,
|
||||
) => { svg: SVGSVGElement; axes: CrossAxesOptions | null } | null;
|
||||
}
|
||||
|
||||
let host: CrossDetailHost | null = null;
|
||||
@@ -121,17 +126,32 @@ export function buildQuantityTable(sheet: CrossQuantitySheet, title: string): HT
|
||||
|
||||
/* ── 그림 줌 · 팬(164-1) ─────────────────────────────────────────────── */
|
||||
|
||||
/** 복제 자식을 g 하나로 감싸 공용 줌 · 팬을 검(축 · 눈금째 한 장) · 휠 = 커서 고정 줌. */
|
||||
/** 도형 층에만 공용 줌 · 팬(179-3 · 축 · 눈금 · 축선 · clip 제자리) · 휠 = 커서 고정 줌 ·
|
||||
* 바뀔 때마다 축을 보이는 범위로 다시 그림 + 십자 크기 되돌림(카드 `_Cross_View_Frame` 꼴). */
|
||||
export function attachDetailZoom(
|
||||
clone: SVGSVGElement,
|
||||
axes: CrossAxesOptions | null,
|
||||
initial: ZoomPanState | undefined,
|
||||
onChange: (state: ZoomPanState) => void,
|
||||
): ZoomPanHandle {
|
||||
const [, , width, height] = (clone.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
|
||||
const layer = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
layer.append(...clone.childNodes);
|
||||
): ZoomPanHandle | null {
|
||||
const layer = clone.querySelector<SVGGElement>("g.b06-chart__plot-layer");
|
||||
if (!layer) return null;
|
||||
// 칸 = viewBox(칸 크기로 그림 · 173-4) → 커서 고정 · 팬 속도 정확
|
||||
clone.append(layer);
|
||||
const [, , width, height] = (clone.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
|
||||
const axisLayer = clone.querySelector<SVGGElement>("g.b06-chart__axes");
|
||||
const renderAxes = axes && axisLayer ? createCrossAxes(axisLayer, axes) : null;
|
||||
const marker = layer.querySelector<SVGGElement>("g.b06-chart__center");
|
||||
const markerLine = marker?.querySelector("line");
|
||||
const cx = Number(markerLine?.getAttribute("x1"));
|
||||
const cy = Number(markerLine?.getAttribute("y1")) + 18;
|
||||
const follow = (state: ZoomPanState): void => {
|
||||
renderAxes?.(state);
|
||||
marker?.setAttribute(
|
||||
"transform",
|
||||
`translate(${cx} ${cy}) scale(${1 / Math.max(state.scale, 1e-6)}) translate(${-cx} ${-cy})`,
|
||||
);
|
||||
};
|
||||
follow(initial ?? IDENTITY_VIEW);
|
||||
return attachSvgZoomPan({
|
||||
svg: clone,
|
||||
layer,
|
||||
@@ -143,7 +163,10 @@ export function attachDetailZoom(
|
||||
content: { x: -width, y: -height, width: 3 * width, height: 3 * height },
|
||||
wheelZoom: true,
|
||||
initial,
|
||||
onChange,
|
||||
onChange: (state) => {
|
||||
follow(state);
|
||||
onChange(state);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,12 +187,16 @@ let tableTimer = 0;
|
||||
let drawing = false;
|
||||
|
||||
/** 칸 크기로 새로 그림 → 복제(듣개 없음) · clip id 를 카드와 가름(같은 id 면 카드 clip 을 씀). */
|
||||
function drawChart(section: CrossSection, widthPx: number, heightPx: number): SVGSVGElement | null {
|
||||
function drawChart(
|
||||
section: CrossSection,
|
||||
widthPx: number,
|
||||
heightPx: number,
|
||||
): { clone: SVGSVGElement; axes: CrossAxesOptions | null } | null {
|
||||
drawing = true;
|
||||
try {
|
||||
const svg = host?.chart(section.station_id, widthPx, heightPx);
|
||||
if (!svg) return null;
|
||||
const clone = svg.cloneNode(true) as SVGSVGElement;
|
||||
const drawn = host?.chart(section.station_id, widthPx, heightPx);
|
||||
if (!drawn) return null;
|
||||
const clone = drawn.svg.cloneNode(true) as SVGSVGElement;
|
||||
clone.querySelectorAll("clipPath[id]").forEach((clip) => {
|
||||
const id = clip.id;
|
||||
clip.id = `b06-detail-${id}`;
|
||||
@@ -177,7 +204,7 @@ function drawChart(section: CrossSection, widthPx: number, heightPx: number): SV
|
||||
.querySelectorAll(`[clip-path="url(#${id})"]`)
|
||||
.forEach((el) => el.setAttribute("clip-path", `url(#${clip.id})`));
|
||||
});
|
||||
return clone;
|
||||
return { clone, axes: drawn.axes };
|
||||
} finally {
|
||||
drawing = false;
|
||||
}
|
||||
@@ -246,11 +273,12 @@ export function openCrossDetail(section: CrossSection): void {
|
||||
const size = `${width}x${height}`;
|
||||
if (size !== drawnSize) zoom = undefined;
|
||||
drawnSize = size;
|
||||
const clone = drawChart(state.section, width, height);
|
||||
if (!clone) cad.replaceChildren(document.createTextNode("그림 없음"));
|
||||
const drawn = drawChart(state.section, width, height);
|
||||
if (!drawn) cad.replaceChildren(document.createTextNode("그림 없음"));
|
||||
else {
|
||||
const handle = attachDetailZoom(clone, zoom, (next) => (zoom = next));
|
||||
cad.replaceChildren(clone, buildZoomControls(handle));
|
||||
const { clone, axes } = drawn;
|
||||
const handle = attachDetailZoom(clone, axes, zoom, (next) => (zoom = next));
|
||||
cad.replaceChildren(clone, ...(handle ? [buildZoomControls(handle)] : []));
|
||||
}
|
||||
showPicked();
|
||||
},
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import {
|
||||
type CrossPlotBase,
|
||||
crossPlotBase,
|
||||
crossPlotFromBase,
|
||||
effectiveCardHalfWidth,
|
||||
fitPlotBase,
|
||||
naturalHeightAtWidth,
|
||||
@@ -561,9 +562,23 @@ export function createSectionView(
|
||||
// 카드 줌 · 팬은 카드 몫 — 모달 그림은 1배에서 시작(카드 상태는 되돌려 둠)
|
||||
const cardZoom = cardZoomStates.get(stationId);
|
||||
cardZoomStates.delete(stationId);
|
||||
const card = buildCrossCard(section, heightPx, base && fitPlotBase(base, heightPx), widthPx);
|
||||
const fitted = base && fitPlotBase(base, heightPx);
|
||||
const card = buildCrossCard(section, heightPx, fitted, widthPx);
|
||||
if (cardZoom) cardZoomStates.set(stationId, cardZoom);
|
||||
return card.querySelector<SVGSVGElement>("svg.b06-section__chart");
|
||||
const svg = card.querySelector<SVGSVGElement>("svg.b06-section__chart");
|
||||
if (!svg) return null;
|
||||
// 축 다시 그리기 옵션(179-3) — 카드 `createCrossAxes` 와 같은 기하
|
||||
const plot = fitted && crossPlotFromBase(fitted, heightPx);
|
||||
const axes = plot && {
|
||||
widthPx,
|
||||
heightPx,
|
||||
pixelsPerMeter: plot.pixelsPerMeter,
|
||||
maxOffset: plot.maxOffset,
|
||||
displayMax: plot.displayMax,
|
||||
elevationMid: plot.elevationMid,
|
||||
exaggeration: Math.max(currentExaggeration, 0.1),
|
||||
};
|
||||
return { svg, axes };
|
||||
},
|
||||
focusStation(stationId) {
|
||||
if (stationId === null) clearSelection();
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
|
||||
import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import type { CrossAxesOptions } from "./B06_Section_UI_Cross_Axes";
|
||||
|
||||
/**
|
||||
* 상태 행·요약줄·하단 접기 손잡이·테두리가 먹는 세로 공간의 **어림값**.
|
||||
@@ -37,8 +38,13 @@ export interface SectionViewController {
|
||||
refreshCard: (chainageM: number) => void;
|
||||
/** 여러 측점을 한꺼번에 교체한다 — 상단 패널은 **마지막에 한 번만** 다시 그린다. */
|
||||
refreshCards: (chainages: ReadonlyArray<number>) => void;
|
||||
/** [상세] 모달 그림(173-4) — 카드 그리기 함수로 칸 크기(W × H px)에 맞춰 새로 그린 svg(없으면 null). */
|
||||
detailChart: (stationId: string, widthPx: number, heightPx: number) => SVGSVGElement | null;
|
||||
/** [상세] 모달 그림(173-4) — 카드 그리기 함수로 칸 크기(W × H px)에 맞춰 새로 그린 svg(없으면 null)
|
||||
* · 축 다시 그리기 옵션(179-3 · 기하 없으면 null). */
|
||||
detailChart: (
|
||||
stationId: string,
|
||||
widthPx: number,
|
||||
heightPx: number,
|
||||
) => { svg: SVGSVGElement; axes: CrossAxesOptions | null } | null;
|
||||
/** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음
|
||||
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). null = 카드 선택 풂(54-8). */
|
||||
focusStation: (stationId: string | null) => void;
|
||||
|
||||
@@ -54,9 +54,11 @@
|
||||
}
|
||||
|
||||
.b06-detail__qtable {
|
||||
/* 이름 칸 바탕 = 데이터 표 머리색(179-3 · `ui_template_data_table` --dt-head) */
|
||||
--b06-detail-head: color-mix(in srgb, var(--color-accent) 6%, var(--color-surface));
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-caption);
|
||||
font-size: var(--text-body-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -70,7 +72,8 @@
|
||||
|
||||
.b06-detail__qtable th {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--b06-detail-head);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.b06-detail__qtable tr:first-child > th {
|
||||
@@ -100,10 +103,18 @@
|
||||
/* 그림에 대상이 없는 칸(암석 띠 없음 · 면적 0 · 사토장 없음) — 회색 · 누름 없음 */
|
||||
.b06-detail__qtable [data-key][data-empty] {
|
||||
cursor: default;
|
||||
background: none;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 빈 칸 = 값 칸만 바탕 지움 · 이름 칸은 머리색 그대로(글만 회색) */
|
||||
.b06-detail__qtable td[data-key][data-empty] {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.b06-detail__qtable th[data-key][data-empty] {
|
||||
background: var(--b06-detail-head);
|
||||
}
|
||||
|
||||
/* 그림 강조 한 벌 = 붉은 계통(164-2 · 카드 색은 그대로) — 띠 · 판정 선 · 중심 십자 · 사토장 */
|
||||
.b06-detail__cad .b06-chart__area.is-active > polygon {
|
||||
fill: color-mix(in srgb, var(--color-danger) 35%, transparent);
|
||||
|
||||
Reference in New Issue
Block a user