260723_4
This commit is contained in:
@@ -27,6 +27,8 @@ export interface IrregularStationsSection {
|
||||
getStations: () => IrregularStation[];
|
||||
/** chainage로 목록 항목을 골라 폼에 로드한다(그래프·3D에서 선택 시). null이면 선택 해제. */
|
||||
selectByChainage: (chainageM: number | null) => void;
|
||||
/** 외부(백엔드 복귀)에서 목록을 통째로 채운다(측점번호·잔여거리는 chainage로 역산). */
|
||||
setStations: (seed: Array<{ chainage_m: number; structure: string }>) => void;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
@@ -231,6 +233,24 @@ export function createIrregularStationsSection(
|
||||
const target = stations.find((entry) => Math.abs(entry.chainage_m - chainageM) < 1e-6);
|
||||
loadForm(target ?? null);
|
||||
},
|
||||
setStations(seed) {
|
||||
const interval = intervalMax();
|
||||
stations.length = 0;
|
||||
seed.forEach((entry) => {
|
||||
const stationNo = Math.floor((entry.chainage_m + 1e-6) / interval);
|
||||
const remainder = Number((entry.chainage_m - stationNo * interval).toFixed(3));
|
||||
stations.push({
|
||||
id: String(nextId++),
|
||||
station: stationNo,
|
||||
remainder,
|
||||
chainage_m: entry.chainage_m,
|
||||
structure: entry.structure,
|
||||
});
|
||||
});
|
||||
loadForm(null);
|
||||
renderList();
|
||||
callbacks.onChange([...stations]);
|
||||
},
|
||||
clear() {
|
||||
stations.length = 0;
|
||||
loadForm(null);
|
||||
|
||||
@@ -316,6 +316,17 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
try {
|
||||
renderSections(detail, routeId);
|
||||
// 복귀/최초 진입 시(클라이언트 목록이 비어 있을 때만) 확정된 비정규 측점을 사이드바에 복원한다.
|
||||
// 재탐색 시엔 새 경로 detail에 비정규 측점이 없어(빈 목록으로 덮이지 않게) 이 가드로 건너뛴다.
|
||||
if (!irregularStations.length) {
|
||||
const restored = detail.longitudinal.stations
|
||||
.filter((station) => station.kind === "irregular")
|
||||
.map((station) => ({
|
||||
chainage_m: station.chainage_m,
|
||||
structure: station.structure ?? "",
|
||||
}));
|
||||
if (restored.length) panel.irregularStations.setStations(restored);
|
||||
}
|
||||
} catch (error) {
|
||||
// 렌더 실패를 "데이터 없음"으로 감추면 원인을 알 수 없게 된다. 조회는 성공했으므로
|
||||
// 빈 안내로 되돌리지 않고 실패 사실을 그대로 드러낸다.
|
||||
|
||||
@@ -155,6 +155,8 @@ export interface EditOverlayOptions {
|
||||
width: number;
|
||||
x: (chainageM: number) => number;
|
||||
step: number;
|
||||
/** 규칙 격자 밖 비정규 측점(구조물). 규칙 측점과 똑같이 ▲/▼(+원복) 버튼을 단다. */
|
||||
irregularStations?: Array<{ chainage_m: number }>;
|
||||
onStation: (chainageM: number, delta: number) => void;
|
||||
onSegment: (segment: AlignmentSegment, delta: number) => void;
|
||||
onResetStation: (chainageM: number) => void;
|
||||
@@ -191,16 +193,38 @@ function overlayButton(
|
||||
return button;
|
||||
}
|
||||
|
||||
/** 계획선 샘플에서 chainage 위치의 계획고를 선형보간한다(버튼 라벨용). */
|
||||
function planElevationAtSample(alignment: ProfileAlignment, chainageM: number): number {
|
||||
const samples = alignment.samples;
|
||||
if (!samples.length) return 0;
|
||||
if (chainageM <= samples[0].chainage_m) return samples[0].elevation_m;
|
||||
const last = samples[samples.length - 1];
|
||||
if (chainageM >= last.chainage_m) return last.elevation_m;
|
||||
for (let index = 1; index < samples.length; index += 1) {
|
||||
const current = samples[index];
|
||||
if (current.chainage_m < chainageM) continue;
|
||||
const previous = samples[index - 1];
|
||||
const span = current.chainage_m - previous.chainage_m;
|
||||
const ratio = span > 1e-9 ? (chainageM - previous.chainage_m) / span : 0;
|
||||
return previous.elevation_m + (current.elevation_m - previous.elevation_m) * ratio;
|
||||
}
|
||||
return last.elevation_m;
|
||||
}
|
||||
|
||||
/** 그래프 영역 위에 겹치는 편집 버튼 층을 만든다 (선 자체는 가리지 않는다). */
|
||||
export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
const { alignment, width, x, step, onStation, onSegment, onResetStation } = options;
|
||||
const irregular = options.irregularStations ?? [];
|
||||
const layer = document.createElement("div");
|
||||
layer.className = "b05-profile-edit";
|
||||
layer.style.width = `${width}px`;
|
||||
|
||||
const repeater = createHoldRepeater();
|
||||
const edited = new Set(Object.keys(alignment.edits.station_offsets));
|
||||
const stationXs = alignment.stations.map((station) => x(station.chainage_m));
|
||||
const stationXs = [
|
||||
...alignment.stations.map((station) => x(station.chainage_m)),
|
||||
...irregular.map((station) => x(station.chainage_m)),
|
||||
];
|
||||
|
||||
/**
|
||||
* 구간 버튼을 측점 버튼과 같은 행에 두되, 겹치는 자리면 옆으로 비킨다.
|
||||
@@ -219,13 +243,15 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
}
|
||||
return best < BUTTON_CLEARANCE_PX ? nearest + BUTTON_CLEARANCE_PX : center;
|
||||
}
|
||||
alignment.stations.forEach((station) => {
|
||||
const left = x(station.chainage_m);
|
||||
const isEdited = edited.has(chainageKey(station.chainage_m));
|
||||
const label = `${station.chainage_m.toFixed(1)}m 계획고 ${station.plan_elevation_m.toFixed(2)}m`;
|
||||
// 측점 하나에 ▲/▼(+편집됐으면 원복 ↺) 버튼을 단다. 규칙·비정규 측점 공용 — 비정규 측점도
|
||||
// `onStation`이 임의 chainage를 변화점으로 승격시키므로 규칙 측점과 완전히 같은 파이프라인이다.
|
||||
function addStationButtons(chainageM: number, planElevationM: number): void {
|
||||
const left = x(chainageM);
|
||||
const isEdited = edited.has(chainageKey(chainageM));
|
||||
const label = `${chainageM.toFixed(1)}m 계획고 ${planElevationM.toFixed(2)}m`;
|
||||
|
||||
const up = overlayButton(repeater, "is-station is-up", "▲", `${label} — ${step}m 올림`, () =>
|
||||
onStation(station.chainage_m, step),
|
||||
onStation(chainageM, step),
|
||||
);
|
||||
up.style.left = `${left - BUTTON_HALF_PX}px`;
|
||||
const down = overlayButton(
|
||||
@@ -233,23 +259,30 @@ export function createEditOverlay(options: EditOverlayOptions): HTMLElement {
|
||||
"is-station is-down",
|
||||
"▼",
|
||||
`${label} — ${step}m 내림`,
|
||||
() => onStation(station.chainage_m, -step),
|
||||
() => onStation(chainageM, -step),
|
||||
);
|
||||
down.style.left = `${left - BUTTON_HALF_PX}px`;
|
||||
layer.append(up, down);
|
||||
|
||||
if (!isEdited) return;
|
||||
const offset = alignment.edits.station_offsets[chainageKey(station.chainage_m)];
|
||||
const offset = alignment.edits.station_offsets[chainageKey(chainageM)];
|
||||
const reset = overlayButton(
|
||||
repeater,
|
||||
"is-reset",
|
||||
"↺",
|
||||
`${label} — 자동 선형으로 원복 (현재 ${offset >= 0 ? "+" : ""}${offset.toFixed(2)}m)`,
|
||||
() => onResetStation(station.chainage_m),
|
||||
() => onResetStation(chainageM),
|
||||
);
|
||||
reset.style.left = `${left - BUTTON_HALF_PX}px`;
|
||||
layer.append(reset);
|
||||
});
|
||||
}
|
||||
|
||||
alignment.stations.forEach((station) =>
|
||||
addStationButtons(station.chainage_m, station.plan_elevation_m),
|
||||
);
|
||||
irregular.forEach((station) =>
|
||||
addStationButtons(station.chainage_m, planElevationAtSample(alignment, station.chainage_m)),
|
||||
);
|
||||
|
||||
alignment.segments.forEach((segment) => {
|
||||
const left = x(segment.from_m);
|
||||
|
||||
@@ -176,6 +176,33 @@ function irregularGraphStations(list: IrregularStation[], maxChainage: number):
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 가로 스크롤에도 좌측에 고정되는 Y축(표고 눈금) 오버레이.
|
||||
*
|
||||
* SVG와 **같은 눈금**(렌더러가 콜백으로 넘김)을 쓰고, 불투명 배경으로 스크롤되는 그래프가
|
||||
* 새어 보이지 않게 가린다. 레이아웃에 영향을 주지 않도록 0크기 sticky 앵커 위에 축을 절대배치한다.
|
||||
*/
|
||||
function buildStickyYAxis(
|
||||
axis: { padLeft: number; ticks: Array<{ y: number; label: string }> },
|
||||
chartHeight: number,
|
||||
): HTMLElement {
|
||||
const anchor = document.createElement("div");
|
||||
anchor.className = "b05-profile__yaxis";
|
||||
const inner = document.createElement("div");
|
||||
inner.className = "b05-profile__yaxis-inner";
|
||||
inner.style.width = `${axis.padLeft}px`;
|
||||
inner.style.height = `${chartHeight}px`;
|
||||
axis.ticks.forEach(({ y, label }) => {
|
||||
const tick = document.createElement("span");
|
||||
tick.className = "b05-profile__yaxis-tick";
|
||||
tick.style.top = `${y}px`;
|
||||
tick.textContent = label;
|
||||
inner.append(tick);
|
||||
});
|
||||
anchor.append(inner);
|
||||
return anchor;
|
||||
}
|
||||
|
||||
/** 종단면도 렌더러와 **같은** chainage → x(px) 매핑을 만든다 (테이블·버튼 정렬 기준). */
|
||||
function chainageMapper(
|
||||
data: LongitudinalSection,
|
||||
@@ -401,6 +428,9 @@ export function createRouteProfilePanel(
|
||||
selectedStationId,
|
||||
onCurveRadiusChange: (curve, radius) =>
|
||||
applyEdits(setCurveRadius(store.edits(), curve, radius ?? 0)),
|
||||
// 값 열 계획고 직접 입력 → 규칙 측점과 동일한 station_offset 파이프라인.
|
||||
onAdjustStation: (chainage, delta) =>
|
||||
base && applyEdits(adjustStation(base, store.edits(), chainage, delta)),
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -421,6 +451,7 @@ export function createRouteProfilePanel(
|
||||
),
|
||||
}
|
||||
: graphData;
|
||||
let yAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null;
|
||||
chartWrap.append(
|
||||
createLongitudinalProfile(
|
||||
graphLongitudinal,
|
||||
@@ -434,8 +465,13 @@ export function createRouteProfilePanel(
|
||||
width,
|
||||
designProfiles,
|
||||
originOffset,
|
||||
(axis) => {
|
||||
yAxis = axis;
|
||||
},
|
||||
),
|
||||
);
|
||||
// 가로 스크롤에도 고정되는 sticky Y축 오버레이(SVG와 같은 눈금·불투명 배경으로 값 누출 차단).
|
||||
if (yAxis) chartWrap.append(buildStickyYAxis(yAxis, chartHeight));
|
||||
if (alignment) {
|
||||
chartWrap.append(
|
||||
createEditOverlay({
|
||||
@@ -443,6 +479,11 @@ export function createRouteProfilePanel(
|
||||
width,
|
||||
x,
|
||||
step: alignment.policy.edit_step_m,
|
||||
// 비정규 측점도 규칙 측점처럼 ▲/▼ 버튼으로 계획고 조정(임의 chainage 변화점 승격).
|
||||
irregularStations: irregularStations.filter(
|
||||
(entry) =>
|
||||
entry.chainage_m >= 0 && entry.chainage_m <= maxChainageOf(longitudinal) + 1e-6,
|
||||
),
|
||||
onStation: (chainage, delta) =>
|
||||
base && applyEdits(adjustStation(base, store.edits(), chainage, delta)),
|
||||
onSegment: (segment, delta) =>
|
||||
@@ -504,12 +545,18 @@ export function createRouteProfilePanel(
|
||||
detail = nextDetail;
|
||||
stationInterval = nextStationInterval;
|
||||
const stored = readAlignment(nextDetail.longitudinal);
|
||||
// 재탐색으로 경로(routeId)가 바뀔 때, 사용자가 조작한 편집이 있으면 **새 경로에 이월**한다.
|
||||
// 편집은 chainage 키라 새 base에 그대로 재적용된다(범위 밖·미매칭 변화점은 best-effort로 드롭).
|
||||
const routeChanged = nextRouteId !== routeId;
|
||||
const carried = routeChanged && store.edited() ? store.edits() : null;
|
||||
// 서버 저장분을 기준으로 삼되, 남아 있는 세션 초안이 있으면 그쪽을 우선한다.
|
||||
if (nextRouteId !== routeId || !store.dirty()) {
|
||||
if (routeChanged || !store.dirty()) {
|
||||
routeId = nextRouteId ?? routeId;
|
||||
store = createProfileEditStore(routeId, stored?.edits ?? emptyEdits(), () => rebuild());
|
||||
}
|
||||
base = stored ? toAlignmentBase(stored) : null;
|
||||
// 이월분은 base 설정 후 미저장 초안으로 커밋한다(확정 시 전송·재탐색 후 새로고침에도 유지).
|
||||
if (carried) store.replace(carried);
|
||||
alignment = base ? buildAlignment(base, store.edits()) : null;
|
||||
draw();
|
||||
requestAnimationFrame(draw);
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface ProfileTableOptions {
|
||||
/** 현재 선택된 측점 id. 비정규 측점이면 그 측점의 값 열을 테이블에 겹쳐 보여준다. */
|
||||
selectedStationId?: string | null;
|
||||
onCurveRadiusChange: (curve: AlignmentCurve, radiusM: number | null) => void;
|
||||
/** 임의 chainage의 계획고를 delta만큼 조정(비정규 측점 값 열 직접 입력용). */
|
||||
onAdjustStation?: (chainageM: number, deltaM: number) => void;
|
||||
}
|
||||
|
||||
interface StationRowSpec {
|
||||
@@ -344,35 +346,52 @@ function buildIrregularColumn(
|
||||
centerX: number,
|
||||
cellWidth: number,
|
||||
interval: number,
|
||||
onAdjustStation?: (chainageM: number, deltaM: number) => void,
|
||||
): HTMLElement {
|
||||
const plan = interpolateSample(alignment.samples, station.chainage_m, "elevation_m");
|
||||
const ground = interpolateSample(alignment.samples, station.chainage_m, "ground_elevation_m");
|
||||
const cut = plan !== null && ground !== null && ground - plan > 0.005 ? ground - plan : null;
|
||||
const fill = plan !== null && ground !== null && plan - ground > 0.005 ? plan - ground : null;
|
||||
// 테이블 행 순서(구배 3 · 측점값 7 · 곡선 2)와 정확히 같게 채운다. 없는 값은 공백.
|
||||
const rows: Array<[string, string]> = [
|
||||
["", ""], // 구배 L
|
||||
["", ""], // 구배 H
|
||||
["", "grade"], // 구배 S
|
||||
[cut !== null ? cut.toFixed(2) : "", "cut"], // 절토고
|
||||
[fill !== null ? fill.toFixed(2) : "", "fill"], // 성토고
|
||||
[plan !== null ? plan.toFixed(2) : "", "plan"], // 계획고
|
||||
[ground !== null ? ground.toFixed(2) : "", ""], // 지반고
|
||||
[station.chainage_m.toFixed(2), ""], // 누가거리
|
||||
["", ""], // 거리
|
||||
[stationLabel(station.chainage_m, interval), ""], // 측점
|
||||
["", ""], // 곡선 L
|
||||
["", ""], // 곡선 R
|
||||
// `plan: true`인 계획고 행만 직접 입력 셀로 만든다(값 열은 오버레이라 실제 12행 격자는 불변).
|
||||
const rows: Array<{ text: string; modifier: string; plan?: boolean }> = [
|
||||
{ text: "", modifier: "" }, // 구배 L
|
||||
{ text: "", modifier: "" }, // 구배 H
|
||||
{ text: "", modifier: "grade" }, // 구배 S
|
||||
{ text: cut !== null ? cut.toFixed(2) : "", modifier: "cut" }, // 절토고
|
||||
{ text: fill !== null ? fill.toFixed(2) : "", modifier: "fill" }, // 성토고
|
||||
{ text: plan !== null ? plan.toFixed(2) : "", modifier: "plan", plan: true }, // 계획고
|
||||
{ text: ground !== null ? ground.toFixed(2) : "", modifier: "" }, // 지반고
|
||||
{ text: station.chainage_m.toFixed(2), modifier: "" }, // 누가거리
|
||||
{ text: "", modifier: "" }, // 거리
|
||||
{ text: stationLabel(station.chainage_m, interval), modifier: "" }, // 측점
|
||||
{ text: "", modifier: "" }, // 곡선 L
|
||||
{ text: "", modifier: "" }, // 곡선 R
|
||||
];
|
||||
const column = element("div", "b05-profile-table__irregular-col");
|
||||
column.style.left = `${centerX}px`;
|
||||
column.style.width = `${cellWidth}px`;
|
||||
rows.forEach(([text, modifier]) => {
|
||||
rows.forEach((row) => {
|
||||
const cell = element(
|
||||
"div",
|
||||
`b05-profile-table__irregular-col-cell${modifier ? ` is-${modifier}` : ""}`,
|
||||
text,
|
||||
`b05-profile-table__irregular-col-cell${row.modifier ? ` is-${row.modifier}` : ""}`,
|
||||
);
|
||||
if (row.plan && onAdjustStation && plan !== null) {
|
||||
// 계획고 직접 입력 → (입력값 − 현재 계획고) delta로 규칙 측점과 동일한 station_offset 반영.
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.step = "any";
|
||||
input.className = "b05-profile-table__irregular-input b05-profile-table__no-spin";
|
||||
input.value = plan.toFixed(2);
|
||||
input.title = `계획고 직접 입력 (현재 ${plan.toFixed(2)}m)`;
|
||||
input.addEventListener("change", () => {
|
||||
const target = Number.parseFloat(input.value);
|
||||
if (Number.isFinite(target)) onAdjustStation(station.chainage_m, target - plan);
|
||||
});
|
||||
cell.append(input);
|
||||
} else {
|
||||
cell.textContent = row.text;
|
||||
}
|
||||
column.append(cell);
|
||||
});
|
||||
column.title = `비정규 측점 ${irregularLabel(station)} · ${station.chainage_m.toFixed(2)}m`;
|
||||
@@ -422,6 +441,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
|
||||
x(selectedIrregular.chainage_m),
|
||||
cellWidth,
|
||||
stationInterval,
|
||||
options.onAdjustStation,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,35 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 가로 스크롤에도 고정되는 Y축(표고 눈금). 0크기 sticky 앵커에 축을 절대배치해 레이아웃 불변. */
|
||||
.b05-profile__yaxis {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.b05-profile__yaxis-inner {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
/* SVG 배경(b06-chart__bg)과 같은 색으로 스크롤되는 그래프를 가려 값 누출을 막는다. */
|
||||
background: var(--color-surface-raised);
|
||||
border-right: 1px solid var(--color-text-secondary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.b05-profile__yaxis-tick {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.b05-route-profile.is-collapsed .b05-route-profile__body,
|
||||
.b05-route-profile.is-collapsed .b05-route-profile__balance {
|
||||
display: none;
|
||||
@@ -604,7 +633,9 @@
|
||||
세로 점선·구조물 태그 없이, 선택 시에만 값 열을 테이블 위에 겹쳐 보여준다. */
|
||||
.b05-profile-table__irregular-col {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
/* 값 셀(0)·곡선(2) 위, sticky 행 이름표(5) **아래**로 둔다 — 스크롤로 값 열이 이름표까지 와도
|
||||
행 제목이 가려지지 않는다. */
|
||||
z-index: 4;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
@@ -617,6 +648,7 @@
|
||||
var(--color-surface)
|
||||
);
|
||||
transform: translateX(-50%);
|
||||
/* 열 자체는 클릭을 통과시키고, 입력 셀만 pointer-events를 되살린다(작업6). */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -648,6 +680,22 @@
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* 값 열의 계획고 직접 입력 셀 — 열은 pointer-events:none이라 입력만 되살린다. */
|
||||
.b05-profile-table__irregular-input {
|
||||
box-sizing: border-box;
|
||||
width: 92%;
|
||||
height: 88%;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-royal-amethyst, rgb(109 40 217));
|
||||
border-radius: 2px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-royal-amethyst, rgb(109 40 217));
|
||||
font: inherit;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* ─── 계획고 편집 버튼 (크기 유지·상시 표시·밝은 글자) ───────────────────── */
|
||||
.b05-profile-edit {
|
||||
position: absolute;
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface SectionStation {
|
||||
label: string;
|
||||
/** irregular = 사용자가 구조물용으로 추가한 비정규 측점(프론트 주입, 백엔드 미영속). */
|
||||
kind: "bp" | "ep" | "regular" | "irregular";
|
||||
/** 비정규 측점의 구조물 설명(백엔드가 확정 시 부여). 복귀 시 사이드바 목록 복원에 쓴다. */
|
||||
structure?: string;
|
||||
center_z: number | null;
|
||||
azimuth_deg: number | null;
|
||||
center_x: number;
|
||||
|
||||
@@ -91,6 +91,11 @@ export function createLongitudinalProfile(
|
||||
* B05 도면 테이블과 정렬할 때 0측점을 이름표 열 바깥으로 밀어내는 데 쓴다(기본 0).
|
||||
*/
|
||||
originOffsetPx = 0,
|
||||
/**
|
||||
* Y축(표고 눈금) 정보를 넘겨받는 콜백(선택). B05가 가로 스크롤에도 고정되는 sticky Y축
|
||||
* 오버레이를 그릴 때 SVG와 **같은 Y-스케일**을 공유하려고 쓴다. B06은 넘기지 않는다.
|
||||
*/
|
||||
onYAxis?: (axis: { padLeft: number; ticks: Array<{ y: number; label: string }> }) => void,
|
||||
): HTMLElement {
|
||||
const samples = data.samples.filter(validElevation);
|
||||
if (samples.length < 2) return emptyView(L("B06_Profile_View_NoLongitudinal"));
|
||||
@@ -131,10 +136,12 @@ export function createLongitudinalProfile(
|
||||
LONG_PAD.top + ((elevationMid + elevationSpan / 2 - elevation) / elevationSpan) * plotHeight;
|
||||
const stationInterval = configuredStationInterval ?? inferStationInterval(data.stations);
|
||||
|
||||
const yAxisTicks: Array<{ y: number; label: string }> = [];
|
||||
for (const ratio of [0, 0.25, 0.5, 0.75, 1]) {
|
||||
const gridY = LONG_PAD.top + ratio * plotHeight;
|
||||
const displayed = elevationMid + elevationSpan / 2 - ratio * elevationSpan;
|
||||
const rawValue = elevationMid + (displayed - elevationMid) / exaggeration;
|
||||
yAxisTicks.push({ y: gridY, label: `${rawValue.toFixed(1)}m` });
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
x1: LONG_PAD.left,
|
||||
@@ -151,6 +158,8 @@ export function createLongitudinalProfile(
|
||||
}),
|
||||
);
|
||||
}
|
||||
// sticky Y축 오버레이가 SVG와 동일한 눈금을 쓰도록 전달(B05 전용, B06은 콜백 없음).
|
||||
onYAxis?.({ padLeft: LONG_PAD.left, ticks: yAxisTicks });
|
||||
|
||||
// 절·성토 음영과 균형 구역 경계는 측점선·프로파일선보다 아래에 깔린다.
|
||||
const toY = (elevation: number) => y(elevationMid + (elevation - elevationMid) * exaggeration);
|
||||
|
||||
Reference in New Issue
Block a user