260723_6
This commit is contained in:
@@ -184,9 +184,19 @@ export function createIrregularStationsSection(
|
||||
const structure = structureField.value.trim();
|
||||
const chainage_m = chainageOf(station, safeRemainder);
|
||||
if (editingId) {
|
||||
const target = stations.find((entry) => entry.id === editingId);
|
||||
if (target)
|
||||
Object.assign(target, { station, remainder: safeRemainder, chainage_m, structure });
|
||||
// 그 자리에서 변경(Object.assign)하면 Page가 보관한 이전 목록의 객체도 함께 바뀌어
|
||||
// "이동 전 chainage"를 잃는다 → 옛 위치의 계획고·곡선 편집을 정리하지 못한다.
|
||||
// 새 객체로 교체해, Page가 이전 위치를 감지하고 그 편집을 지우게 한다.
|
||||
const index = stations.findIndex((entry) => entry.id === editingId);
|
||||
if (index >= 0) {
|
||||
stations[index] = {
|
||||
id: editingId,
|
||||
station,
|
||||
remainder: safeRemainder,
|
||||
chainage_m,
|
||||
structure,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
stations.push({
|
||||
id: String(nextId++),
|
||||
|
||||
@@ -169,10 +169,15 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
const activeProjectId: string = projectId;
|
||||
|
||||
const viewer = createRouteViewer();
|
||||
const profilePanel = createRouteProfilePanel(activeProjectId, (stationId) => {
|
||||
viewer.markers.selectStation(stationId);
|
||||
syncIrregularSelection(stationId);
|
||||
});
|
||||
const profilePanel = createRouteProfilePanel(
|
||||
activeProjectId,
|
||||
(stationId) => {
|
||||
viewer.markers.selectStation(stationId);
|
||||
syncIrregularSelection(stationId);
|
||||
},
|
||||
// [초기선 복원] 시 추가한 비정규 측점도 함께 지운다.
|
||||
() => panel.irregularStations.clear(),
|
||||
);
|
||||
|
||||
/**
|
||||
* 그래프·3D에서 비정규 측점을 고르면 사이드바 입력 폼에 로드해 수정/삭제할 수 있게 한다.
|
||||
|
||||
@@ -252,6 +252,8 @@ function computeProfileLayout(
|
||||
export function createRouteProfilePanel(
|
||||
projectId: string,
|
||||
onSelectStation: (stationId: string) => void,
|
||||
/** [초기선 복원] 클릭 시 함께 실행(비정규 측점 등 다른 조작값도 초기화하려고 Page가 넘긴다). */
|
||||
onResetAll?: () => void,
|
||||
) {
|
||||
const root = document.createElement("section");
|
||||
root.className = "b05-route-profile";
|
||||
@@ -323,13 +325,16 @@ export function createRouteProfilePanel(
|
||||
.join("\n");
|
||||
balanceBar.append(warning);
|
||||
}
|
||||
if (store.edited()) {
|
||||
if (store.edited() || irregularStations.length) {
|
||||
const reset = document.createElement("button");
|
||||
reset.type = "button";
|
||||
reset.className = "b05-route-profile__balance-reset";
|
||||
reset.textContent = "초기선 복원";
|
||||
reset.title = "모든 편집을 지우고 자동 산출된 계획선으로 되돌립니다.";
|
||||
reset.addEventListener("click", () => store.resetAll());
|
||||
reset.title = "모든 편집과 추가한 비정규 측점을 지우고 자동 산출된 계획선으로 되돌립니다.";
|
||||
reset.addEventListener("click", () => {
|
||||
store.resetAll();
|
||||
onResetAll?.();
|
||||
});
|
||||
balanceBar.append(reset);
|
||||
}
|
||||
if (store.dirty()) {
|
||||
|
||||
@@ -197,6 +197,7 @@ function buildSegmentRows(
|
||||
alignment: ProfileAlignment,
|
||||
x: (chainage: number) => number,
|
||||
fontPx: number,
|
||||
rowHeight: number,
|
||||
): HTMLElement[] {
|
||||
return SEGMENT_ROWS.map((spec) => {
|
||||
const row = createRow("b05-profile-table__row--grade", spec.label, spec.unit);
|
||||
@@ -205,9 +206,16 @@ function buildSegmentRows(
|
||||
const span = x(segment.to_m) - left;
|
||||
const text = spec.cell(segment);
|
||||
const node = element("span", "b05-profile-table__segment", "");
|
||||
// 구간이 짧아 글자가 안 들어가면 숫자만 비운다. 블록을 지우면 그 자리가 다시
|
||||
// 틈으로 남아 빈 셀처럼 보이므로, 칸은 유지하고 값은 툴팁으로 읽게 한다.
|
||||
if (segmentTextFits(text, span, fontPx)) node.textContent = text;
|
||||
// 값은 항상 표기한다. 가로로 안 들어가면 90도로 세워 블록의 폭(span)·행 높이에 맞춰
|
||||
// 글자를 줄여 넣는다(작아도 무시 — 값이 아예 안 보이는 것보단 낫다).
|
||||
const value = element("span", "b05-profile-table__segment-value", text);
|
||||
if (!segmentTextFits(text, span, fontPx)) {
|
||||
value.classList.add("is-rotated");
|
||||
const byThickness = Math.max(1, span - 2);
|
||||
const byLength = text.length ? (rowHeight - 2) / (text.length * CHAR_WIDTH_EM) : fontPx;
|
||||
value.style.fontSize = `${Math.max(6, Math.min(fontPx, byThickness, byLength))}px`;
|
||||
}
|
||||
node.append(value);
|
||||
node.style.left = `${left}px`;
|
||||
node.style.width = `${span}px`;
|
||||
node.title =
|
||||
@@ -335,6 +343,9 @@ function interpolateSample(
|
||||
return last[key];
|
||||
}
|
||||
|
||||
/** 값 열 한 칸의 내용: 단일 값이거나, 변화점에서 좌/우로 갈린 두 값. */
|
||||
type CellValue = { text: string } | { left: string; right: string };
|
||||
|
||||
/**
|
||||
* 선택된 비정규 측점의 **값 열 오버레이**. 규칙 측점 열과 같은 12행 순서로 값(계획고·지반고·
|
||||
* 누가거리·측점 등, 계획선 샘플 보간)을 쌓아 테이블 위에 겹친다. 구조물 이름은 사이드바 입력에
|
||||
@@ -352,21 +363,38 @@ function buildIrregularColumn(
|
||||
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;
|
||||
|
||||
// 구배(L/H/S): 이 측점이 구간 안에 있으면 그 구간 값을 그대로(중복이어도) 표기하고,
|
||||
// 변화점이라 좌우 구간이 갈리면 세로 구분선으로 셀을 반 나눠 좌/우 값을 각각 넣는다.
|
||||
const chainage = station.chainage_m;
|
||||
const inside = alignment.segments.find(
|
||||
(segment) => segment.from_m + 1e-6 < chainage && chainage < segment.to_m - 1e-6,
|
||||
);
|
||||
const leftSeg = alignment.segments.find((segment) => Math.abs(segment.to_m - chainage) < 0.01);
|
||||
const rightSeg = alignment.segments.find((segment) => Math.abs(segment.from_m - chainage) < 0.01);
|
||||
const gradeCell = (pick: (segment: AlignmentSegment) => number, digits: number): CellValue => {
|
||||
if (inside) return { text: pick(inside).toFixed(digits) };
|
||||
const left = leftSeg ? pick(leftSeg).toFixed(digits) : "";
|
||||
const right = rightSeg ? pick(rightSeg).toFixed(digits) : "";
|
||||
return left && right ? { left, right } : { text: left || right || "" };
|
||||
};
|
||||
const curve = alignment.curves.find((entry) => Math.abs(entry.chainage_m - chainage) < 0.01);
|
||||
|
||||
// 테이블 행 순서(구배 3 · 측점값 7 · 곡선 2)와 정확히 같게 채운다. 없는 값은 공백.
|
||||
// `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 rows: Array<{ value: CellValue; modifier: string; plan?: boolean }> = [
|
||||
{ value: gradeCell((segment) => segment.length_m, 2), modifier: "" }, // 구배 L
|
||||
{ value: gradeCell((segment) => segment.height_m, 2), modifier: "" }, // 구배 H
|
||||
{ value: gradeCell((segment) => segment.grade_percent, 2), modifier: "grade" }, // 구배 S
|
||||
{ value: { text: cut !== null ? cut.toFixed(2) : "" }, modifier: "cut" }, // 절토고
|
||||
{ value: { text: fill !== null ? fill.toFixed(2) : "" }, modifier: "fill" }, // 성토고
|
||||
{ value: { text: plan !== null ? plan.toFixed(2) : "" }, modifier: "plan", plan: true }, // 계획고
|
||||
{ value: { text: ground !== null ? ground.toFixed(2) : "" }, modifier: "" }, // 지반고
|
||||
{ value: { text: station.chainage_m.toFixed(2) }, modifier: "" }, // 누가거리
|
||||
{ value: { text: "" }, modifier: "" }, // 거리
|
||||
{ value: { text: stationLabel(station.chainage_m, interval) }, modifier: "" }, // 측점
|
||||
{ value: { text: curve ? curve.l_m.toFixed(2) : "" }, modifier: "" }, // 곡선 L
|
||||
{ value: { text: curve ? curve.r_m.toFixed(1) : "" }, modifier: "" }, // 곡선 R
|
||||
];
|
||||
const column = element("div", "b05-profile-table__irregular-col");
|
||||
column.style.left = `${centerX}px`;
|
||||
@@ -389,8 +417,15 @@ function buildIrregularColumn(
|
||||
if (Number.isFinite(target)) onAdjustStation(station.chainage_m, target - plan);
|
||||
});
|
||||
cell.append(input);
|
||||
} else if ("left" in row.value) {
|
||||
// 좌우 값을 세로 구분선으로 반씩 나눠 넣는다(각 반쪽에서 폰트를 줄여 맞춘다).
|
||||
cell.classList.add("is-split");
|
||||
cell.append(
|
||||
element("span", "b05-profile-table__irregular-col-half", row.value.left),
|
||||
element("span", "b05-profile-table__irregular-col-half", row.value.right),
|
||||
);
|
||||
} else {
|
||||
cell.textContent = row.text;
|
||||
cell.textContent = row.value.text;
|
||||
}
|
||||
column.append(cell);
|
||||
});
|
||||
@@ -401,7 +436,8 @@ function buildIrregularColumn(
|
||||
export function createProfileTable(options: ProfileTableOptions): HTMLElement {
|
||||
const { alignment, stationInterval, width, height, cellWidth, labelWidth, rowCount, x } = options;
|
||||
const table = element("div", "b05-profile-table");
|
||||
const fontSize = fitFontSize(height / Math.max(rowCount, 1), cellWidth);
|
||||
const rowHeight = height / Math.max(rowCount, 1);
|
||||
const fontSize = fitFontSize(rowHeight, cellWidth);
|
||||
// 종점이 앞 측점과 겹치면 오른쪽으로 민 셀 중심(측점 값·곡선 셀 공용). 구배 블록은 구간을
|
||||
// 덮는 넓은 범위라 겹치지 않으므로 x를 그대로 쓴다.
|
||||
const centers = stationCellCenters(alignment.stations, x, cellWidth, width);
|
||||
@@ -414,7 +450,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
|
||||
table.style.setProperty("--b05-table-label-width", `${labelWidth}px`);
|
||||
table.style.setProperty("--b05-table-label-font", `${Math.max(8, labelFontSize)}px`);
|
||||
|
||||
table.append(...buildSegmentRows(alignment, x, fontSize));
|
||||
table.append(...buildSegmentRows(alignment, x, fontSize, rowHeight));
|
||||
buildStationRows(alignment, stationInterval).forEach((spec, index) => {
|
||||
const row = createRow(
|
||||
`${spec.modifier ? `is-${spec.modifier}` : ""}${index === 0 ? " is-group-start" : ""}`,
|
||||
|
||||
@@ -607,6 +607,16 @@
|
||||
color: rgb(180 83 9);
|
||||
}
|
||||
|
||||
/* 구배 블록 값. 가로로 안 들어가면 90도 회전해 좁은 블록에도 값을 표기한다. */
|
||||
.b05-profile-table__segment-value {
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b05-profile-table__segment-value.is-rotated {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.b05-profile-table__curve {
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -674,9 +684,31 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
|
||||
color: var(--color-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 변화점에서 좌/우로 갈린 값: 세로 구분선으로 반씩 나누고 각 반쪽은 폰트를 줄여 셀에 맞춘다. */
|
||||
.b05-profile-table__irregular-col-cell.is-split {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.b05-profile-table__irregular-col-half {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 0.72em;
|
||||
}
|
||||
|
||||
.b05-profile-table__irregular-col-half:first-child {
|
||||
border-right: 1px solid color-mix(in srgb, var(--color-border) 85%, transparent);
|
||||
}
|
||||
|
||||
.b05-profile-table__irregular-col-cell:last-child {
|
||||
|
||||
Reference in New Issue
Block a user