This commit is contained in:
2026-07-19 11:08:16 +09:00
parent 52778784de
commit db817b940a
7 changed files with 274 additions and 61 deletions
+35 -1
View File
@@ -99,11 +99,20 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou
const bounds = getBounds();
if (!bounds) return;
allPoints().forEach((point) => {
const pointIndex =
point.type === "bp" || point.type === "ep"
? 0
: points[point.type].findIndex((candidate) => candidate.id === point.id);
const interactionData = {
routePointId: point.id,
routePointKind: point.type,
routePointIndex: pointIndex,
};
const material = new THREE.MeshBasicMaterial({ color: COLORS[point.type] });
const marker = new THREE.Mesh(new THREE.SphereGeometry(1.6, 18, 12), material);
marker.position.copy(modelToScene(point, bounds));
marker.position.y += 1.6;
marker.userData.routePointId = point.id;
Object.assign(marker.userData, interactionData);
if (point.id === selectedId) marker.scale.setScalar(1.35);
markerGroup.add(marker);
if ((point.type === "ap" || point.type === "fp") && point.radius_m) {
@@ -118,6 +127,7 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou
);
zone.position.copy(modelToScene(point, bounds));
zone.position.y += 0.2;
Object.assign(zone.userData, interactionData);
markerGroup.add(zone);
}
});
@@ -155,6 +165,19 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou
notify();
}
function movePoint(id: string, model: { x: number; y: number; z: number }): void {
const point = allPoints().find((candidate) => candidate.id === id);
if (!point) return;
const update = (candidate: PlacedRoutePoint) =>
candidate.id === id ? { ...candidate, ...model } : candidate;
if (point.type === "bp" || point.type === "ep") {
points = { ...points, [point.type]: update(point) };
} else {
points = { ...points, [point.type]: points[point.type].map(update) };
}
notify();
}
function deleteSelected(): void {
const current = selected();
if (!current) return;
@@ -264,6 +287,7 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou
moveSelected(model: { x: number; y: number; z: number }) {
updateSelected(model);
},
movePoint,
updateSelected,
deleteSelected,
selectObject(object: THREE.Object3D | undefined) {
@@ -277,6 +301,16 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou
renderMarkers();
selectionListener?.(selected());
},
pointIdForObject(object: THREE.Object3D | undefined) {
return typeof object?.userData.routePointId === "string"
? (object.userData.routePointId as string)
: null;
},
selectPoint(id: string) {
selectedId = allPoints().some((point) => point.id === id) ? id : null;
renderMarkers();
selectionListener?.(selected());
},
renderMarkers,
renderRoute,
renderStationLines,
@@ -2,7 +2,10 @@ import type {
LongitudinalSection,
SectionDetailResponse,
} from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_Api_Fetch";
import { createLongitudinalProfile } from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View";
import {
createLongitudinalProfile,
longitudinalMinimumWidth,
} from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View";
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
import "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css";
@@ -33,9 +36,20 @@ export function createRouteProfilePanel(onSelectStation: (stationId: string) =>
let detail: SectionDetailResponse | null = null;
let selectedStationId: string | null = null;
let stationInterval: number | undefined;
let resizeTimer = 0;
let lastWidth = 0;
let lastHeight = 0;
function draw(): void {
if (!detail) return;
if (!detail || body.clientWidth <= 0 || body.clientHeight <= 0) return;
const availableWidth = Math.max(1, body.clientWidth - 30);
const height = body.clientHeight;
const width = Math.max(
availableWidth,
longitudinalMinimumWidth(detail.longitudinal, stationInterval),
);
lastWidth = body.clientWidth;
lastHeight = height;
body.replaceChildren(
createLongitudinalProfile(
normalizedLongitudinal(detail.longitudinal),
@@ -44,14 +58,29 @@ export function createRouteProfilePanel(onSelectStation: (stationId: string) =>
undefined,
onSelectStation,
stationInterval,
width,
height,
),
);
}
const resizeObserver = new ResizeObserver(() => {
if (
body.clientWidth <= 0 ||
body.clientHeight <= 0 ||
(Math.abs(body.clientWidth - lastWidth) < 1 && Math.abs(body.clientHeight - lastHeight) < 1)
)
return;
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(draw, 150);
});
resizeObserver.observe(body);
function setCollapsed(collapsed: boolean): void {
root.classList.toggle("is-collapsed", collapsed);
panelHandle.setOpen(!collapsed);
sessionStorage.setItem(COLLAPSED_KEY, String(collapsed));
if (!collapsed) requestAnimationFrame(draw);
}
toggle.addEventListener("click", () => setCollapsed(!root.classList.contains("is-collapsed")));
@@ -63,6 +92,7 @@ export function createRouteProfilePanel(onSelectStation: (stationId: string) =>
detail = nextDetail;
stationInterval = nextStationInterval;
draw();
requestAnimationFrame(draw);
},
setSelectedStation(stationId: string | null) {
selectedStationId = stationId;
@@ -73,5 +103,9 @@ export function createRouteProfilePanel(onSelectStation: (stationId: string) =>
selectedStationId = null;
body.replaceChildren(empty);
},
dispose() {
window.clearTimeout(resizeTimer);
resizeObserver.disconnect();
},
};
}
+5 -5
View File
@@ -50,8 +50,11 @@
}
.b05-route-profile__body {
box-sizing: border-box;
height: 100%;
overflow: auto;
overflow-x: auto;
overflow-y: hidden;
padding-inline: 15px;
}
.b05-route-profile.is-collapsed .b05-route-profile__body {
@@ -65,11 +68,8 @@
font-size: var(--text-body-sm);
}
.b05-route-profile .b06-section__chart-wrap,
.b05-route-profile .b06-section__chart {
width: 100%;
.b05-route-profile .b06-section__chart-wrap {
height: 100%;
min-width: 0;
}
.b05-route__viewport canvas {
+89 -5
View File
@@ -80,6 +80,9 @@ export function createRouteViewer(): RouteViewer {
let current: { projectId: string; modelId: number; smooth: boolean; interval: number } | null =
null;
let movingSelected = false;
let dragCandidate: { id: string; pointerId: number; x: number; y: number } | null = null;
let draggingMarker = false;
let lastDragPoint: { x: number; y: number; z: number } | null = null;
const markers = createRouteMarkers(scene, () => bounds);
function clearContours(): void {
@@ -173,7 +176,7 @@ export function createRouteViewer(): RouteViewer {
const point = terrainPoint(event);
if (point && ["bp", "ep", "cp", "ap", "fp"].includes(kind)) markers.place(kind, point);
});
canvas.addEventListener("pointerdown", (event) => {
function markerHit(event: PointerEvent): THREE.Object3D | undefined {
const rect = canvas.getBoundingClientRect();
const pointer = new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1,
@@ -181,9 +184,43 @@ export function createRouteViewer(): RouteViewer {
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(pointer, camera);
const markerHit = raycaster.intersectObject(markers.group, true)[0];
if (markerHit) {
markers.selectObject(markerHit.object);
return raycaster.intersectObject(markers.group, true)[0]?.object;
}
function finishMarkerInteraction(selectCandidate: boolean): void {
if (dragCandidate && (draggingMarker || selectCandidate)) {
markers.selectPoint(dragCandidate.id);
}
if (dragCandidate && canvas.hasPointerCapture(dragCandidate.pointerId)) {
canvas.releasePointerCapture(dragCandidate.pointerId);
}
controls.enabled = true;
dragCandidate = null;
draggingMarker = false;
lastDragPoint = null;
}
function handlePointerDown(event: PointerEvent): void {
if (event.button !== 0) return;
const hit = markerHit(event);
const pointId = markers.pointIdForObject(hit);
if (pointId) {
dragCandidate = {
id: pointId,
pointerId: event.pointerId,
x: event.clientX,
y: event.clientY,
};
draggingMarker = false;
lastDragPoint = null;
canvas.setPointerCapture(event.pointerId);
event.preventDefault();
event.stopPropagation();
return;
}
if (hit) {
markers.selectObject(hit);
event.stopPropagation();
return;
}
if (movingSelected) {
@@ -193,7 +230,49 @@ export function createRouteViewer(): RouteViewer {
} else {
markers.selectObject(undefined);
}
});
}
function handlePointerMove(event: PointerEvent): void {
if (!dragCandidate || event.pointerId !== dragCandidate.pointerId) return;
event.preventDefault();
event.stopPropagation();
if (
!draggingMarker &&
Math.hypot(event.clientX - dragCandidate.x, event.clientY - dragCandidate.y) > 3
) {
draggingMarker = true;
controls.enabled = false;
}
if (!draggingMarker) return;
const point = terrainPoint(event);
if (!point) return;
lastDragPoint = point;
markers.movePoint(dragCandidate.id, point);
}
function handlePointerUp(event: PointerEvent): void {
if (!dragCandidate || event.pointerId !== dragCandidate.pointerId) return;
event.preventDefault();
event.stopPropagation();
if (draggingMarker) {
const point = terrainPoint(event) ?? lastDragPoint;
if (point) markers.movePoint(dragCandidate.id, point);
}
finishMarkerInteraction(!draggingMarker);
}
function handlePointerExit(event: PointerEvent): void {
if (!dragCandidate || event.pointerId !== dragCandidate.pointerId) return;
event.preventDefault();
event.stopPropagation();
finishMarkerInteraction(false);
}
canvas.addEventListener("pointerdown", handlePointerDown, true);
canvas.addEventListener("pointermove", handlePointerMove, true);
canvas.addEventListener("pointerup", handlePointerUp, true);
canvas.addEventListener("pointerleave", handlePointerExit, true);
canvas.addEventListener("pointercancel", handlePointerExit, true);
let frame = 0;
function animate(): void {
@@ -256,6 +335,11 @@ export function createRouteViewer(): RouteViewer {
dispose() {
cancelAnimationFrame(frame);
resizeObserver.disconnect();
canvas.removeEventListener("pointerdown", handlePointerDown, true);
canvas.removeEventListener("pointermove", handlePointerMove, true);
canvas.removeEventListener("pointerup", handlePointerUp, true);
canvas.removeEventListener("pointerleave", handlePointerExit, true);
canvas.removeEventListener("pointercancel", handlePointerExit, true);
markers.dispose();
clearContours();
disposeObject(terrain);
@@ -231,6 +231,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
if (projectId) goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
},
});
layout.root.classList.add("b06-profile-layout");
root.replaceChildren(layout.root);
if (!projectId) {
@@ -10,7 +10,7 @@ const SVG_NS = "http://www.w3.org/2000/svg";
const LONG_WIDTH = 1200;
const LONG_HEIGHT = 220;
const CROSS_WIDTH = 560;
const CROSS_HEIGHT = 260;
const CROSS_HEIGHT = 250;
const LONG_PAD = { left: 62, right: 24, top: 30, bottom: 52 };
const CROSS_PAD = { left: 58, right: 20, top: 20, bottom: 52 };
@@ -97,6 +97,19 @@ function stationLabel(chainage: number, interval: number): string {
return `${stationNumber}+${remainder.toFixed(1)}`;
}
export function longitudinalMinimumWidth(
data: LongitudinalSection,
configuredStationInterval?: number,
): number {
const stationInterval = configuredStationInterval ?? inferStationInterval(data.stations);
const longestLabelLength = Math.max(
1,
...data.stations.map((station) => stationLabel(station.chainage_m, stationInterval).length),
);
const labelWidth = Math.max(48, longestLabelLength * 6 + 16);
return LONG_PAD.left + LONG_PAD.right + Math.max(1, data.stations.length) * labelWidth;
}
export function createLongitudinalProfile(
data: LongitudinalSection,
selectedStationId: string | null,
@@ -104,6 +117,8 @@ export function createLongitudinalProfile(
yScaleOptions: YScaleOptions | undefined,
onSelectStation: (stationId: string) => void,
configuredStationInterval?: number,
widthPx = LONG_WIDTH,
heightPx = LONG_HEIGHT,
): HTMLElement {
const samples = data.samples.filter(validElevation);
if (samples.length < 2) return emptyView(L("B06_Profile_View_NoLongitudinal"));
@@ -112,13 +127,13 @@ export function createLongitudinalProfile(
wrapper.className = "b06-section__chart-wrap";
const svg = svgElement("svg", {
class: "b06-section__chart",
viewBox: `0 0 ${LONG_WIDTH} ${LONG_HEIGHT}`,
width: widthPx,
height: heightPx,
viewBox: `0 0 ${widthPx} ${heightPx}`,
role: "img",
"aria-label": L("B06_Profile_View_Longitudinal"),
});
svg.append(
svgElement("rect", { width: LONG_WIDTH, height: LONG_HEIGHT, class: "b06-chart__bg" }),
);
svg.append(svgElement("rect", { width: widthPx, height: heightPx, class: "b06-chart__bg" }));
const maxChainage = Math.max(data.length_m, samples[samples.length - 1]?.chainage_m ?? 1, 1);
const elevations = samples.map((sample) => sample.elevation_m);
@@ -126,8 +141,8 @@ export function createLongitudinalProfile(
const rawMax = yScaleOptions?.globalMaxElevation ?? Math.max(...elevations);
const elevationMid = (rawMin + rawMax) / 2;
const exaggeration = Math.max(verticalExaggeration, 0.1);
const plotWidth = LONG_WIDTH - LONG_PAD.left - LONG_PAD.right;
const plotHeight = LONG_HEIGHT - LONG_PAD.top - LONG_PAD.bottom;
const plotWidth = widthPx - LONG_PAD.left - LONG_PAD.right;
const plotHeight = heightPx - LONG_PAD.top - LONG_PAD.bottom;
const elevationSpan = yScaleOptions
? plotHeight / yScaleOptions.pixelsPerMeter
: Math.max(rawMax - rawMin, 1);
@@ -144,7 +159,7 @@ export function createLongitudinalProfile(
svgElement("line", {
x1: LONG_PAD.left,
y1: gridY,
x2: LONG_WIDTH - LONG_PAD.right,
x2: widthPx - LONG_PAD.right,
y2: gridY,
class: "b06-chart__grid",
}),
@@ -175,19 +190,19 @@ export function createLongitudinalProfile(
x1: stationX,
y1: LONG_PAD.top,
x2: stationX,
y2: LONG_HEIGHT - LONG_PAD.bottom + 8,
y2: heightPx - LONG_PAD.bottom + 8,
class: "b06-chart__station-hit",
}),
svgElement("line", {
x1: stationX,
y1: LONG_PAD.top,
x2: stationX,
y2: LONG_HEIGHT - LONG_PAD.bottom + 8,
y2: heightPx - LONG_PAD.bottom + 8,
class: `b06-chart__station-line b06-chart__station-line--${selected ? "selected" : station.kind}`,
}),
svgText(stationLabel(station.chainage_m, stationInterval), {
x: stationX,
y: LONG_HEIGHT - 23,
y: heightPx - 23,
"text-anchor": "middle",
class: "b06-chart__station-label",
}),
@@ -205,29 +220,29 @@ export function createLongitudinalProfile(
svgElement("polyline", { points, class: "b06-chart__profile" }),
svgElement("line", {
x1: LONG_PAD.left,
y1: LONG_HEIGHT - LONG_PAD.bottom,
x2: LONG_WIDTH - LONG_PAD.right,
y2: LONG_HEIGHT - LONG_PAD.bottom,
y1: heightPx - LONG_PAD.bottom,
x2: widthPx - LONG_PAD.right,
y2: heightPx - LONG_PAD.bottom,
class: "b06-chart__axis",
}),
svgElement("line", {
x1: LONG_PAD.left,
y1: LONG_PAD.top,
x2: LONG_PAD.left,
y2: LONG_HEIGHT - LONG_PAD.bottom,
y2: heightPx - LONG_PAD.bottom,
class: "b06-chart__axis",
}),
svgText(L("B06_Profile_View_LongitudinalXAxis"), {
x: LONG_WIDTH / 2,
y: LONG_HEIGHT - 4,
x: widthPx / 2,
y: heightPx - 4,
"text-anchor": "middle",
class: "b06-chart__axis-label",
}),
svgText(L("B06_Profile_View_ElevationAxis"), {
x: 15,
y: LONG_HEIGHT / 2,
y: heightPx / 2,
"text-anchor": "middle",
transform: `rotate(-90 15 ${LONG_HEIGHT / 2})`,
transform: `rotate(-90 15 ${heightPx / 2})`,
class: "b06-chart__axis-label",
}),
);
@@ -243,6 +258,8 @@ export function createCrossSectionCard(
onSelect: (stationId: string) => void,
stationInterval: number,
crossHalfWidth?: number,
widthPx = CROSS_WIDTH,
heightPx = CROSS_HEIGHT,
): HTMLElement {
const card = document.createElement("article");
card.id = `cross-${section.station_id}`;
@@ -287,8 +304,8 @@ export function createCrossSectionCard(
const elevationMid = (rawMin + rawMax) / 2;
const padding = rawMax > rawMin ? (rawMax - rawMin) * 0.08 : 0.5;
const exaggeration = Math.max(verticalExaggeration, 0.1);
const plotWidth = CROSS_WIDTH - CROSS_PAD.left - CROSS_PAD.right;
const plotHeight = CROSS_HEIGHT - CROSS_PAD.top - CROSS_PAD.bottom;
const plotWidth = widthPx - CROSS_PAD.left - CROSS_PAD.right;
const plotHeight = heightPx - CROSS_PAD.top - CROSS_PAD.bottom;
const displaySpan = yScaleOptions
? plotHeight / yScaleOptions.pixelsPerMeter
: Math.max((rawMax - rawMin + padding * 2) * exaggeration, 1);
@@ -301,13 +318,13 @@ export function createCrossSectionCard(
((displayMax - elevation) / Math.max(displayMax - displayMin, 1)) * plotHeight;
const svg = svgElement("svg", {
class: "b06-section__chart",
viewBox: `0 0 ${CROSS_WIDTH} ${CROSS_HEIGHT}`,
width: widthPx,
height: heightPx,
viewBox: `0 0 ${widthPx} ${heightPx}`,
role: "img",
"aria-label": `${section.label} ${L("B06_Profile_View_Cross")}`,
});
svg.append(
svgElement("rect", { width: CROSS_WIDTH, height: CROSS_HEIGHT, class: "b06-chart__bg" }),
);
svg.append(svgElement("rect", { width: widthPx, height: heightPx, class: "b06-chart__bg" }));
const xTicks = Array.from(
{ length: 7 },
@@ -319,12 +336,12 @@ export function createCrossSectionCard(
x1: x(tick),
y1: CROSS_PAD.top,
x2: x(tick),
y2: CROSS_HEIGHT - CROSS_PAD.bottom,
y2: heightPx - CROSS_PAD.bottom,
class: "b06-chart__grid",
}),
svgText(Math.abs(tick) < 1e-6 ? "0" : tick.toFixed(0), {
x: x(tick),
y: CROSS_HEIGHT - CROSS_PAD.bottom + 16,
y: heightPx - CROSS_PAD.bottom + 16,
"text-anchor": "middle",
class: "b06-chart__tick",
}),
@@ -338,7 +355,7 @@ export function createCrossSectionCard(
svgElement("line", {
x1: CROSS_PAD.left,
y1: y(displayTick),
x2: CROSS_WIDTH - CROSS_PAD.right,
x2: widthPx - CROSS_PAD.right,
y2: y(displayTick),
class: "b06-chart__grid",
}),
@@ -375,20 +392,20 @@ export function createCrossSectionCard(
const centerX = x(0);
const centerY = centerSample
? y(elevationMid + (centerSample.elevation_m - elevationMid) * exaggeration)
: CROSS_HEIGHT / 2;
: heightPx / 2;
svg.append(
svgElement("line", {
x1: CROSS_PAD.left,
y1: CROSS_HEIGHT - CROSS_PAD.bottom,
x2: CROSS_WIDTH - CROSS_PAD.right,
y2: CROSS_HEIGHT - CROSS_PAD.bottom,
y1: heightPx - CROSS_PAD.bottom,
x2: widthPx - CROSS_PAD.right,
y2: heightPx - CROSS_PAD.bottom,
class: "b06-chart__axis",
}),
svgElement("line", {
x1: CROSS_PAD.left,
y1: CROSS_PAD.top,
x2: CROSS_PAD.left,
y2: CROSS_HEIGHT - CROSS_PAD.bottom,
y2: heightPx - CROSS_PAD.bottom,
class: "b06-chart__axis",
}),
svgElement("line", {
@@ -406,16 +423,16 @@ export function createCrossSectionCard(
class: "b06-chart__center-marker",
}),
svgText(L("B06_Profile_View_CrossXAxis"), {
x: CROSS_WIDTH / 2,
y: CROSS_HEIGHT - 8,
x: widthPx / 2,
y: heightPx - 8,
"text-anchor": "middle",
class: "b06-chart__axis-label",
}),
svgText(L("B06_Profile_View_ElevationAxis"), {
x: 13,
y: CROSS_HEIGHT / 2,
y: heightPx / 2,
"text-anchor": "middle",
transform: `rotate(-90 13 ${CROSS_HEIGHT / 2})`,
transform: `rotate(-90 13 ${heightPx / 2})`,
class: "b06-chart__axis-label",
}),
);
@@ -441,6 +458,7 @@ export interface SectionViewController {
stationInterval?: number,
) => void;
clear: () => void;
dispose: () => void;
}
export function createSectionView(): SectionViewController {
@@ -451,10 +469,20 @@ export function createSectionView(): SectionViewController {
let currentExaggeration = 1;
let currentCrossHalfWidth: number | undefined;
let currentStationInterval: number | undefined;
let renderWidth = 0;
let resizeTimer = 0;
const contentWidth = (): number => {
const style = getComputedStyle(root);
return Math.max(
0,
root.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight),
);
};
const draw = (): void => {
if (!currentDetail || renderWidth <= 0) return;
root.replaceChildren();
if (!currentDetail) return;
const detail = currentDetail;
const yScale = calculateYScale(detail);
const stationInterval =
@@ -479,6 +507,8 @@ export function createSectionView(): SectionViewController {
yScale,
(stationId) => selectStation(stationId, true),
stationInterval,
Math.max(renderWidth, longitudinalMinimumWidth(detail.longitudinal, stationInterval)),
LONG_HEIGHT,
),
);
@@ -491,6 +521,8 @@ export function createSectionView(): SectionViewController {
crossHeading.append(crossTitle, crossCount);
const grid = document.createElement("div");
grid.className = "b06-section__grid";
const columnCount = renderWidth >= 976 ? 2 : 1;
const cardWidth = (renderWidth - (columnCount - 1) * 16) / columnCount;
if (detail.cross_sections.length) {
detail.cross_sections.forEach((section) =>
grid.append(
@@ -502,6 +534,8 @@ export function createSectionView(): SectionViewController {
(stationId) => selectStation(stationId, false),
stationInterval,
currentCrossHalfWidth,
cardWidth,
CROSS_HEIGHT,
),
),
);
@@ -511,6 +545,17 @@ export function createSectionView(): SectionViewController {
root.append(longitudinalPanel, crossHeading, grid);
};
const resizeObserver = new ResizeObserver(() => {
const nextWidth = contentWidth();
if (nextWidth <= 0 || Math.abs(nextWidth - renderWidth) < 1) return;
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(() => {
renderWidth = nextWidth;
draw();
}, 150);
});
resizeObserver.observe(root);
return {
root,
render(detail, verticalExaggeration, crossHalfWidth, stationInterval) {
@@ -521,12 +566,18 @@ export function createSectionView(): SectionViewController {
currentStationInterval =
stationInterval !== undefined && stationInterval > 0 ? stationInterval : undefined;
selectedStationId ??= detail.longitudinal.stations[0]?.station_id ?? null;
renderWidth = contentWidth();
draw();
if (renderWidth <= 0) requestAnimationFrame(() => resizeObserver.observe(root));
},
clear() {
currentDetail = null;
selectedStationId = null;
root.replaceChildren();
},
dispose() {
window.clearTimeout(resizeTimer);
resizeObserver.disconnect();
},
};
}
@@ -8,6 +8,23 @@
* ========================================================================== */
/* --- 좌측 입력 폼 --- */
.b06-profile-layout {
height: calc(100vh - var(--spacing-64));
height: calc(100dvh - var(--spacing-64));
min-height: 0;
overflow: hidden;
}
.b06-profile-layout .ui-workflow-layout__body,
.b06-profile-layout .ui-workflow-layout__main {
height: 100%;
min-height: 0;
}
.b06-profile-layout .ui-workflow-layout__main {
overflow: auto;
}
.b06-profile__form {
display: flex;
flex-direction: column;
@@ -162,9 +179,7 @@
.b06-section__chart {
display: block;
width: 100%;
min-width: 520px;
height: auto;
max-width: none;
}
.b06-section__heading {
@@ -178,7 +193,7 @@
.b06-section__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(auto-fill, minmax(min(480px, 100%), 1fr));
gap: var(--spacing-16);
}
@@ -308,9 +323,3 @@
fill: var(--color-danger);
font-weight: var(--font-weight-bold);
}
@media (max-width: 900px) {
.b06-section__grid {
grid-template-columns: 1fr;
}
}