feat(B05): 배관 정착 계획선 1차 로직 + 유토곡선 UI 정리 5건
계획선 1차 로직 (2026-08-03 사용자 확정) - design_pipe_anchored_profile 신설: 배수유역도가 확정한 배관 배치 측점을 변화점으로 삼아 계획선이 각 배관 자리에서 지면선과 만나도록(계획고=지반고) 시작점→배관1→…→종점을 직선으로 잇고 기본 R을 얹는다. 배관(암거)은 계곡 유하부라 계획선이 그 지점에 붙어야 복토·유입 조건이 성립한다. - 기존 지반 추종 직선 분할 DP 선형은 2차 폴백으로, 구 균형 최적화 계획선은 3차 폴백으로 강등. 관 지점 파일의 노선 지문이 다르면 배관을 쓰지 않는다. - 테스트 프로젝트 solve 재실행으로 저장분 재계산 — 변화점이 배관 5개 자리와 일치하고 각 자리 계획고=지반고임을 검증. 유토곡선 UI (사용자 피드백 5건) - 기준 버튼 라디오화: 횡단/종단 택1로 그 기준 그래프만 전체 영역에 표시 (applyLegendToggle/normalizeVisibleBasis, B05·B06 공통 저장 키 공유). Y 범위도 표시 중인 기준으로 산정. - B05 Y축 이중 표시 제거: MassHaulAxis.axisX 신설 — 축 선·눈금을 종단 그래프 축과 같은 자리에 그리고 데이터 매핑은 유지해 측점 세로 정렬 불변. - 종단↔유토곡선 경계 드래그 리사이저(비율 0.25~0.75, 세션 보존, 기본 1:1). - B05 범례를 B06과 같은 오버레이 우상단 절대배치로 이동. - 도면 테이블: 구조물 배치로 갈라진 구배 블록 값을 기본 숨김(윤곽·툴팁만), 해당 측점 선택 시 하이라이트와 함께 표시. typecheck·vite build·ruff·B03 테스트 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,35 @@ export const MASS_HAUL_DEFAULT_VISIBLE: MassHaulBasis[] = ["cross"];
|
||||
*/
|
||||
export const MASS_HAUL_BALANCE_KEY = "balance";
|
||||
|
||||
/**
|
||||
* 곡선 기준 버튼은 **라디오**다 — 횡단/종단 중 하나를 고르면 그 기준으로 계산된 그래프
|
||||
* 하나만 전체 영역에 보인다(2026-08-03 사용자 지시. 겹쳐 비교하던 이전 방식 폐기).
|
||||
* 켜짐 집합에 기준 키가 없거나 여럿이면 순서상 앞선 기준 하나로 눌러 맞춘다 —
|
||||
* 두 화면(B05/B06)이 같은 저장 키를 공유하므로 읽는 쪽마다 같은 규칙이어야 한다.
|
||||
*/
|
||||
export function normalizeVisibleBasis(visible: Set<string>): Set<string> {
|
||||
const active = SERIES_BASES.filter((basis) => visible.has(basis));
|
||||
if (active.length === 1) return visible;
|
||||
const next = new Set(visible);
|
||||
SERIES_BASES.forEach((basis) => next.delete(basis));
|
||||
next.add(active[0] ?? SERIES_BASES[0]);
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 범례 클릭 하나를 적용한다 — 기준 키는 라디오 선택, 그 외(분배 레이어)는 켜고 끄기. */
|
||||
export function applyLegendToggle(visible: Set<string>, key: string): Set<string> {
|
||||
const next = new Set(visible);
|
||||
if ((SERIES_BASES as string[]).includes(key)) {
|
||||
SERIES_BASES.forEach((basis) => next.delete(basis));
|
||||
next.add(key);
|
||||
} else if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
return normalizeVisibleBasis(next);
|
||||
}
|
||||
|
||||
/** 환산계수가 비어 있거나 값이 이상하면 1.0으로 떨어뜨려 계산이 멈추지 않게 한다. */
|
||||
function factorFor(conversion: EarthworkConversion, ground: GroundType): number {
|
||||
const factor = conversion?.[ground]?.compacted;
|
||||
|
||||
@@ -33,6 +33,14 @@ export interface MassHaulAxis {
|
||||
maxChainageM: number;
|
||||
padLeft: number;
|
||||
padRight: number;
|
||||
/**
|
||||
* Y축 선·눈금을 그릴 x(px). 생략하면 `padLeft`.
|
||||
*
|
||||
* B05는 데이터 시작을 반 칸(originOffset) 들여쓰므로 `padLeft = 62 + originOffset`인데,
|
||||
* 축까지 그 자리에 그리면 위 종단 그래프의 축(62px)과 어긋나 **축이 두 개**로 보인다
|
||||
* (2026-08-03 사용자 보고). 축은 여기(종단과 같은 자리)에, 데이터는 padLeft에 둔다.
|
||||
*/
|
||||
axisX?: number;
|
||||
}
|
||||
|
||||
/** 축 눈금이 읽히는 최소 높이. 이보다 낮아지면 그래프가 뭉개진다. */
|
||||
@@ -289,11 +297,15 @@ export function createMassHaulChart(
|
||||
|
||||
// 종단면도와 완전히 같은 X 매핑 — 여백·최댓값 모두 호출한 쪽 종단 렌더러에서 받는다.
|
||||
const maxChainage = Math.max(axis.maxChainageM, 1);
|
||||
const axisX = axis.axisX ?? axis.padLeft;
|
||||
const plotWidth = widthPx - axis.padLeft - axis.padRight;
|
||||
const plotHeight = heightPx - MASS_PAD_TOP - MASS_PAD_BOTTOM;
|
||||
const x = (chainage: number) => axis.padLeft + (chainage / maxChainage) * plotWidth;
|
||||
|
||||
const { min, max } = volumeRange(series);
|
||||
// 기준 버튼이 라디오가 되면서(택1 표시) Y 범위도 **표시 중인 곡선**으로 잡는다 —
|
||||
// 숨은 기준까지 합쳐 잡으면 선택한 그래프가 눌려 보인다. 아무것도 안 켰으면 전체로 폴백.
|
||||
const rangeSource = series.filter((entry) => visibleKeys.has(entry.key));
|
||||
const { min, max } = volumeRange(rangeSource.length ? rangeSource : series);
|
||||
const span = Math.max(max - min, 1e-6);
|
||||
const y = (volume: number) => MASS_PAD_TOP + ((max - volume) / span) * plotHeight;
|
||||
|
||||
@@ -304,14 +316,14 @@ export function createMassHaulChart(
|
||||
const value = max - ratio * span;
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
x1: axis.padLeft,
|
||||
x1: axisX,
|
||||
y1: gridY,
|
||||
x2: widthPx - axis.padRight,
|
||||
y2: gridY,
|
||||
class: "b06-chart__grid",
|
||||
}),
|
||||
svgText(formatVolume(value), {
|
||||
x: axis.padLeft - 9,
|
||||
x: axisX - 9,
|
||||
y: gridY + 4,
|
||||
"text-anchor": "end",
|
||||
class: "b06-chart__tick",
|
||||
@@ -391,14 +403,14 @@ export function createMassHaulChart(
|
||||
// Y 범위 계산(`volumeRange`)이 0을 항상 품으므로 이 선은 어떤 노선에서도 화면에 남는다.
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
x1: axis.padLeft,
|
||||
x1: axisX,
|
||||
y1: zeroY,
|
||||
x2: widthPx - axis.padRight,
|
||||
y2: zeroY,
|
||||
class: "b06-masshaul__zero",
|
||||
}),
|
||||
svgText("0", {
|
||||
x: axis.padLeft - 9,
|
||||
x: axisX - 9,
|
||||
y: zeroY + 4,
|
||||
"text-anchor": "end",
|
||||
class: "b06-chart__tick b06-masshaul__zero-tick",
|
||||
@@ -486,9 +498,9 @@ export function createMassHaulChart(
|
||||
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
x1: axis.padLeft,
|
||||
x1: axisX,
|
||||
y1: MASS_PAD_TOP,
|
||||
x2: axis.padLeft,
|
||||
x2: axisX,
|
||||
y2: heightPx - MASS_PAD_BOTTOM,
|
||||
class: "b06-chart__axis",
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user