refactor(common): 유토곡선 엔진·렌더러를 common_util로 공용화
B05 계획 유토곡선과 B06 정식 유토곡선이 같은 엔진을 쓰도록 모듈을 옮겼다.
- B06_wf3_ProfileCross_UI_MassHaul{,_Balance,_Balance_View,_Balloon,_Curve,
_Settle,_View}.ts + Style_MassHaul.css → common_util/common_util_mass_haul*.
- common_util_mass_haul_types.ts 신설: GroundType/EarthworkConversion/
HaulEquipmentLimit/BalloonOffsets 정의처를 한 곳으로 모으고 B06 Api_Fetch가
재수출. 엔진 입력은 페이지 API 타입 대신 구조적 부분집합으로 받는다.
- common_util_svg.ts 신설: svgElement/svgText/L/stationLabel/
inferStationInterval 이관, B06 _UI_Section_Common은 재수출로 경로 유지.
- createMassHaulChart에 MassHaulAxis 인자 추가 — 종단 렌더러 상수 의존을
걷어내고 호출한 쪽이 X축(누가거리 최댓값·좌우 여백)을 주입한다.
동작 무변경. npm run typecheck 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,12 @@
|
|||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
|
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
|
||||||
|
import type {
|
||||||
|
BalloonOffsets,
|
||||||
|
EarthworkConversion,
|
||||||
|
GroundType,
|
||||||
|
HaulEquipmentLimit,
|
||||||
|
} from "@util/common_util_mass_haul_types";
|
||||||
|
|
||||||
export interface SectionOptionDefaults {
|
export interface SectionOptionDefaults {
|
||||||
station_interval_m: number;
|
station_interval_m: number;
|
||||||
@@ -51,26 +57,15 @@ export type StandardCrossKey = "soil" | "rock" | "paved";
|
|||||||
|
|
||||||
export type StandardCrossSection = Record<StandardCrossKey, StandardCrossGroup>;
|
export type StandardCrossSection = Record<StandardCrossKey, StandardCrossGroup>;
|
||||||
|
|
||||||
/**
|
// 지반유형·토량환산계수·운반장비 한계거리는 B05 계획 유토곡선과 공유하므로 정의처를
|
||||||
* 지반유형 하나의 토량환산계수(둘 다 자연상태 기준 배율).
|
// `@util/common_util_mass_haul_types` 한 곳에 두고 여기서는 재수출만 한다(사본 금지).
|
||||||
* loose = 흐트러진 상태 / 자연상태(팽창률), compacted = 다져진 상태 / 자연상태(다짐률).
|
export type {
|
||||||
* 정의처는 config_system.EARTHWORK_CONVERSION_FACTORS 한 곳뿐이다.
|
GroundType,
|
||||||
*/
|
EarthworkConversionFactor,
|
||||||
export interface EarthworkConversionFactor {
|
EarthworkConversion,
|
||||||
loose: number;
|
HaulEquipmentLimit,
|
||||||
compacted: number;
|
BalloonOffsets,
|
||||||
}
|
} from "@util/common_util_mass_haul_types";
|
||||||
|
|
||||||
export type EarthworkConversion = Record<GroundType, EarthworkConversionFactor>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 평균운반거리 상한 하나(m). `max_distance_m: null`이면 상한 없음(나머지를 전부 받는다).
|
|
||||||
* 정의처는 config_system.EARTHWORK_HAUL_EQUIPMENT_LIMITS_M 한 곳뿐이라 프론트는 받아만 쓴다.
|
|
||||||
*/
|
|
||||||
export interface HaulEquipmentLimit {
|
|
||||||
key: string;
|
|
||||||
max_distance_m: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SectionContextResponse {
|
export interface SectionContextResponse {
|
||||||
project_id: string;
|
project_id: string;
|
||||||
@@ -188,9 +183,6 @@ export interface LongitudinalSection {
|
|||||||
profile_alignment?: unknown;
|
profile_alignment?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 유토곡선 balloon을 끌어 옮긴 위치(띠 번호 → [dx, dy], px). */
|
|
||||||
export type BalloonOffsets = Record<string, [number, number]>;
|
|
||||||
|
|
||||||
export interface CrossSection extends SectionStation {
|
export interface CrossSection extends SectionStation {
|
||||||
samples: SectionSample[];
|
samples: SectionSample[];
|
||||||
/** DB에 저장된 잠정 설계 지정(있을 때만). 상세 조회 시 얹혀 온다. */
|
/** DB에 저장된 잠정 설계 지정(있을 때만). 상세 조회 시 얹혀 온다. */
|
||||||
@@ -213,7 +205,6 @@ export interface SectionConfirmResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 측점 표준횡단 설계 지정값 (버튼 상태). */
|
/** 측점 표준횡단 설계 지정값 (버튼 상태). */
|
||||||
export type GroundType = "soil" | "ripping_rock" | "blasting_rock";
|
|
||||||
export type SectionMode = "left_cut" | "right_cut" | "both_cut" | "both_fill";
|
export type SectionMode = "left_cut" | "right_cut" | "both_cut" | "both_fill";
|
||||||
export type DitchSide = "left" | "right";
|
export type DitchSide = "left" | "right";
|
||||||
export type DitchType = "standard" | "l_type";
|
export type DitchType = "standard" | "l_type";
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ import {
|
|||||||
createSectionView,
|
createSectionView,
|
||||||
type RockBoundaryControl,
|
type RockBoundaryControl,
|
||||||
} from "./B06_wf3_ProfileCross_UI_Section_View";
|
} from "./B06_wf3_ProfileCross_UI_Section_View";
|
||||||
import { computeMassHaul, massHaulPayload } from "./B06_wf3_ProfileCross_UI_MassHaul";
|
import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul";
|
||||||
import { computeHaulPlan } from "./B06_wf3_ProfileCross_UI_MassHaul_Balance";
|
import { computeHaulPlan } from "@util/common_util_mass_haul_balance";
|
||||||
import { balloonOffsetsPayload } from "./B06_wf3_ProfileCross_UI_MassHaul_Balance_View";
|
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||||
import { designElevationAt } from "./B06_wf3_ProfileCross_UI_Section_Common";
|
import { designElevationAt } from "./B06_wf3_ProfileCross_UI_Section_Common";
|
||||||
import {
|
import {
|
||||||
createStandardPanel,
|
createStandardPanel,
|
||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
import "./B06_wf3_ProfileCross_UI_Style.css";
|
import "./B06_wf3_ProfileCross_UI_Style.css";
|
||||||
import "./B06_wf3_ProfileCross_UI_Style_Cross.css";
|
import "./B06_wf3_ProfileCross_UI_Style_Cross.css";
|
||||||
import "./B06_wf3_ProfileCross_UI_Style_Cross_Areas.css";
|
import "./B06_wf3_ProfileCross_UI_Style_Cross_Areas.css";
|
||||||
import "./B06_wf3_ProfileCross_UI_Style_MassHaul.css";
|
import "@util/common_util_mass_haul.css";
|
||||||
|
|
||||||
function L(key: keyof typeof ui_locales): string {
|
function L(key: keyof typeof ui_locales): string {
|
||||||
return ui_locales[key][currentLanguageIndex];
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
* 이 모듈을 참조한다. 색상 하드코딩 금지 원칙은 그대로이며 여기서는 지오메트리 계산만 다룬다.
|
* 이 모듈을 참조한다. 색상 하드코딩 금지 원칙은 그대로이며 여기서는 지오메트리 계산만 다룬다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
|
||||||
import type { CrossDesignChange } from "./B06_wf3_ProfileCross_UI_Cross_Design";
|
import type { CrossDesignChange } from "./B06_wf3_ProfileCross_UI_Cross_Design";
|
||||||
import type {
|
import type {
|
||||||
DesignProfile,
|
DesignProfile,
|
||||||
@@ -19,7 +18,17 @@ import type {
|
|||||||
export type { CrossDesignChange };
|
export type { CrossDesignChange };
|
||||||
export type DesignChangeHandler = (chainageM: number, change: CrossDesignChange) => void;
|
export type DesignChangeHandler = (chainageM: number, change: CrossDesignChange) => void;
|
||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
// SVG 생성·측점 표기 유틸은 B05 계획 유토곡선과 공유하려고 `@util/common_util_svg`로 옮겼다.
|
||||||
|
// 여기서 재수출해 기존 B06 임포트 경로를 그대로 둔다(정의는 저쪽 한 곳뿐).
|
||||||
|
export {
|
||||||
|
SVG_NS,
|
||||||
|
L,
|
||||||
|
svgElement,
|
||||||
|
svgText,
|
||||||
|
inferStationInterval,
|
||||||
|
stationLabel,
|
||||||
|
} from "@util/common_util_svg";
|
||||||
|
|
||||||
export const LONG_WIDTH = 1200;
|
export const LONG_WIDTH = 1200;
|
||||||
export const LONG_HEIGHT = 220;
|
export const LONG_HEIGHT = 220;
|
||||||
export const CROSS_WIDTH = 560;
|
export const CROSS_WIDTH = 560;
|
||||||
@@ -44,28 +53,6 @@ export interface YScaleOptions {
|
|||||||
globalMaxElevation: number;
|
globalMaxElevation: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function L(key: keyof typeof ui_locales): string {
|
|
||||||
return ui_locales[key][currentLanguageIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function svgElement<K extends keyof SVGElementTagNameMap>(
|
|
||||||
tag: K,
|
|
||||||
attributes: Record<string, string | number> = {},
|
|
||||||
): SVGElementTagNameMap[K] {
|
|
||||||
const element = document.createElementNS(SVG_NS, tag);
|
|
||||||
Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, String(value)));
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function svgText(
|
|
||||||
value: string,
|
|
||||||
attributes: Record<string, string | number>,
|
|
||||||
): SVGTextElement {
|
|
||||||
const text = svgElement("text", attributes);
|
|
||||||
text.textContent = value;
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validElevation(
|
export function validElevation(
|
||||||
sample: SectionSample,
|
sample: SectionSample,
|
||||||
): sample is SectionSample & { elevation_m: number } {
|
): sample is SectionSample & { elevation_m: number } {
|
||||||
@@ -146,30 +133,3 @@ export function emptyView(message: string): HTMLElement {
|
|||||||
empty.textContent = message;
|
empty.textContent = message;
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inferStationInterval(stations: Array<{ chainage_m: number }>): number {
|
|
||||||
const counts = new Map<number, number>();
|
|
||||||
for (let index = 1; index < stations.length; index += 1) {
|
|
||||||
const difference = stations[index].chainage_m - stations[index - 1].chainage_m;
|
|
||||||
if (difference <= 0) continue;
|
|
||||||
const rounded = Math.round(difference * 10) / 10;
|
|
||||||
counts.set(rounded, (counts.get(rounded) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
[...counts.entries()].sort(
|
|
||||||
([intervalA, countA], [intervalB, countB]) => countB - countA || intervalB - intervalA,
|
|
||||||
)[0]?.[0] ?? 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stationLabel(chainage: number, interval: number): string {
|
|
||||||
const safeInterval = interval > 0 ? interval : 1;
|
|
||||||
let stationNumber = Math.floor((chainage + 1e-6) / safeInterval);
|
|
||||||
let remainder = chainage - stationNumber * safeInterval;
|
|
||||||
if (Math.abs(remainder) < 0.05) remainder = 0;
|
|
||||||
if (remainder >= safeInterval - 0.05) {
|
|
||||||
stationNumber += 1;
|
|
||||||
remainder = 0;
|
|
||||||
}
|
|
||||||
return `${stationNumber}+${remainder.toFixed(1)}`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,19 +37,19 @@ import {
|
|||||||
MASS_HAUL_BALANCE_KEY,
|
MASS_HAUL_BALANCE_KEY,
|
||||||
MASS_HAUL_DEFAULT_VISIBLE,
|
MASS_HAUL_DEFAULT_VISIBLE,
|
||||||
type MassHaulSeries,
|
type MassHaulSeries,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul";
|
} from "@util/common_util_mass_haul";
|
||||||
import { computeHaulPlan } from "./B06_wf3_ProfileCross_UI_MassHaul_Balance";
|
import { computeHaulPlan } from "@util/common_util_mass_haul_balance";
|
||||||
import {
|
import {
|
||||||
configureBalloonOffsets,
|
configureBalloonOffsets,
|
||||||
resetBalloonOffsets,
|
resetBalloonOffsets,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul_Balance_View";
|
} from "@util/common_util_mass_haul_balance_view";
|
||||||
import {
|
import {
|
||||||
createMassHaulChart,
|
createMassHaulChart,
|
||||||
createMassHaulLegend,
|
createMassHaulLegend,
|
||||||
createMassHaulSummary,
|
createMassHaulSummary,
|
||||||
MASS_HAUL_HEIGHT,
|
MASS_HAUL_HEIGHT,
|
||||||
MASS_HAUL_MIN_HEIGHT,
|
MASS_HAUL_MIN_HEIGHT,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul_View";
|
} from "@util/common_util_mass_haul_view";
|
||||||
import {
|
import {
|
||||||
calculateYScale,
|
calculateYScale,
|
||||||
CROSS_GRID_GAP,
|
CROSS_GRID_GAP,
|
||||||
@@ -62,6 +62,8 @@ import {
|
|||||||
inferStationInterval,
|
inferStationInterval,
|
||||||
L,
|
L,
|
||||||
LONG_HEIGHT,
|
LONG_HEIGHT,
|
||||||
|
longitudinalMaxChainage,
|
||||||
|
LONG_PAD,
|
||||||
type YScaleOptions,
|
type YScaleOptions,
|
||||||
} from "./B06_wf3_ProfileCross_UI_Section_Common";
|
} from "./B06_wf3_ProfileCross_UI_Section_Common";
|
||||||
|
|
||||||
@@ -488,6 +490,11 @@ export function createSectionView(
|
|||||||
series,
|
series,
|
||||||
visibleSeries,
|
visibleSeries,
|
||||||
detail.longitudinal,
|
detail.longitudinal,
|
||||||
|
{
|
||||||
|
maxChainageM: longitudinalMaxChainage(detail.longitudinal),
|
||||||
|
padLeft: LONG_PAD.left,
|
||||||
|
padRight: LONG_PAD.right,
|
||||||
|
},
|
||||||
selectedStationId,
|
selectedStationId,
|
||||||
cachedStationInterval,
|
cachedStationInterval,
|
||||||
chartWidth,
|
chartWidth,
|
||||||
|
|||||||
+19
-19
@@ -1,10 +1,10 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B06_wf3_ProfileCross_UI_MassHaul.ts
|
* common_util_mass_haul.ts
|
||||||
* 유토곡선(Mass Haul Diagram) 계산 엔진 — 순수 함수, DOM 의존 없음.
|
* 유토곡선(Mass Haul Diagram) 계산 엔진 — 순수 함수, DOM 의존 없음.
|
||||||
*
|
*
|
||||||
* 구간 토량을 평균단면법으로 만들고 토량환산계수로 기준을 통일해 누가토량을 누적한다.
|
* 구간 토량을 평균단면법으로 만들고 토량환산계수로 기준을 통일해 누가토량을 누적한다.
|
||||||
* 결과는 B08 수량산출·B09 견적이 그대로 재사용할 수 있도록 계산과 렌더링을 분리해
|
* 결과는 B08 수량산출·B09 견적이 그대로 재사용할 수 있도록 계산과 렌더링을 분리해
|
||||||
* 둔다(렌더러는 `_UI_MassHaul_View`).
|
* 둔다(렌더러는 `common_util_mass_haul_view`).
|
||||||
*
|
*
|
||||||
* ── 계산 기준: 다짐상태 ──────────────────────────────────────────────
|
* ── 계산 기준: 다짐상태 ──────────────────────────────────────────────
|
||||||
* "운반거리의 산정 시에 모든 수량은 다짐상태로 환산하여 계산하고, 내역서에 적용하는
|
* "운반거리의 산정 시에 모든 수량은 다짐상태로 환산하여 계산하고, 내역서에 적용하는
|
||||||
@@ -34,14 +34,14 @@
|
|||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CrossDesign,
|
|
||||||
CrossSection,
|
|
||||||
EarthworkConversion,
|
EarthworkConversion,
|
||||||
GroundType,
|
GroundType,
|
||||||
LongitudinalSection,
|
MassHaulDesign,
|
||||||
} from "./B06_wf3_ProfileCross_Api_Fetch";
|
MassHaulLongitudinal,
|
||||||
import type { HaulPlan } from "./B06_wf3_ProfileCross_UI_MassHaul_Balance";
|
MassHaulSection,
|
||||||
import { haulPlanPayload } from "./B06_wf3_ProfileCross_UI_MassHaul_Balance";
|
} from "./common_util_mass_haul_types";
|
||||||
|
import type { HaulPlan } from "./common_util_mass_haul_balance";
|
||||||
|
import { haulPlanPayload } from "./common_util_mass_haul_balance";
|
||||||
|
|
||||||
/** 지반유형별 토량(㎥). 도면 표기 EA(토사)/RR(리핑암)/BR(발파암)에 대응한다. */
|
/** 지반유형별 토량(㎥). 도면 표기 EA(토사)/RR(리핑암)/BR(발파암)에 대응한다. */
|
||||||
export interface GroundVolumes {
|
export interface GroundVolumes {
|
||||||
@@ -63,7 +63,7 @@ export interface MassHaulPoint {
|
|||||||
cut_rock_m3: number;
|
cut_rock_m3: number;
|
||||||
/**
|
/**
|
||||||
* 암반분을 암종까지 가른 값(자연상태, `cut_rr_m3 + cut_br_m3 === cut_rock_m3`).
|
* 암반분을 암종까지 가른 값(자연상태, `cut_rr_m3 + cut_br_m3 === cut_rock_m3`).
|
||||||
* 운반 블록의 EA/RR/BR 안분(`_UI_MassHaul_Balance`)이 이 구간 단위를 되받아 쓴다 —
|
* 운반 블록의 EA/RR/BR 안분(`common_util_mass_haul_balance`)이 이 구간 단위를 되받아 쓴다 —
|
||||||
* 한 구간의 양 끝 측점이 서로 다른 암종일 수 있어 합계만으로는 되돌릴 수 없다.
|
* 한 구간의 양 끝 측점이 서로 다른 암종일 수 있어 합계만으로는 되돌릴 수 없다.
|
||||||
*/
|
*/
|
||||||
cut_rr_m3: number;
|
cut_rr_m3: number;
|
||||||
@@ -83,7 +83,7 @@ export interface MassHaulPoint {
|
|||||||
* 유토곡선은 측점 사이에서 직선이 아니라 **포물선**이다 — 단면적이 선형으로 변하면
|
* 유토곡선은 측점 사이에서 직선이 아니라 **포물선**이다 — 단면적이 선형으로 변하면
|
||||||
* 그 적분인 누가토량은 2차식이 된다. 평균운반거리(반종거 현)를 직선으로 재면 참고 도면 대비
|
* 그 적분인 누가토량은 2차식이 된다. 평균운반거리(반종거 현)를 직선으로 재면 참고 도면 대비
|
||||||
* 약 40% 짧게 나와 장비 경계 판정이 어긋난다(도면의 20m 구간 반종거가 20/√2 = 14.14m).
|
* 약 40% 짧게 나와 장비 경계 판정이 어긋난다(도면의 20m 구간 반종거가 20/√2 = 14.14m).
|
||||||
* 이 값이 있어야 `_UI_MassHaul_Balance`가 구간마다 2차식을 세울 수 있다.
|
* 이 값이 있어야 `common_util_mass_haul_balance`가 구간마다 2차식을 세울 수 있다.
|
||||||
*/
|
*/
|
||||||
net_area_m2: number;
|
net_area_m2: number;
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ interface AreaSample {
|
|||||||
* 엔진이 낸 절토 분리값을 읽는다. 구 데이터(분리 필드 없음)는 절토 전량을 측점
|
* 엔진이 낸 절토 분리값을 읽는다. 구 데이터(분리 필드 없음)는 절토 전량을 측점
|
||||||
* 지반유형으로 돌려 **기존 단일 지반유형 환산과 같은 결과**를 유지한다.
|
* 지반유형으로 돌려 **기존 단일 지반유형 환산과 같은 결과**를 유지한다.
|
||||||
*/
|
*/
|
||||||
function splitCut(design: CrossDesign | undefined): {
|
function splitCut(design: MassHaulDesign | undefined): {
|
||||||
soil: number;
|
soil: number;
|
||||||
rock: number;
|
rock: number;
|
||||||
rock_kind: GroundType | null;
|
rock_kind: GroundType | null;
|
||||||
@@ -308,7 +308,7 @@ function integrate(samples: AreaSample[], conversion: EarthworkConversion): Mass
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 설계가 지정된 측점만 누가거리 순으로 모은다. 두 기준이 공통으로 쓰는 기준점 목록. */
|
/** 설계가 지정된 측점만 누가거리 순으로 모은다. 두 기준이 공통으로 쓰는 기준점 목록. */
|
||||||
function designedSections(crossSections: CrossSection[]): CrossSection[] {
|
function designedSections(crossSections: MassHaulSection[]): MassHaulSection[] {
|
||||||
return crossSections
|
return crossSections
|
||||||
.filter((section) => section.design && Number.isFinite(section.chainage_m))
|
.filter((section) => section.design && Number.isFinite(section.chainage_m))
|
||||||
.sort((a, b) => a.chainage_m - b.chainage_m);
|
.sort((a, b) => a.chainage_m - b.chainage_m);
|
||||||
@@ -319,14 +319,14 @@ function designedSections(crossSections: CrossSection[]): CrossSection[] {
|
|||||||
* 스스로 흘러내린다. 판정 경사(config)를 못 받았으면 **전부 불가**로 본다 — 프론트에 사본을
|
* 스스로 흘러내린다. 판정 경사(config)를 못 받았으면 **전부 불가**로 본다 — 프론트에 사본을
|
||||||
* 두지 않으며, 모르면 운반비를 세는 쪽이 안전하다.
|
* 두지 않으며, 모르면 운반비를 세는 쪽이 안전하다.
|
||||||
*/
|
*/
|
||||||
function naturalSpoilAllowed(design: CrossDesign | undefined, minSlope?: number): boolean {
|
function naturalSpoilAllowed(design: MassHaulDesign | undefined, minSlope?: number): boolean {
|
||||||
if (!Number.isFinite(minSlope)) return false;
|
if (!Number.isFinite(minSlope)) return false;
|
||||||
const slope = design?.fill_ground_slope;
|
const slope = design?.fill_ground_slope;
|
||||||
return Number.isFinite(slope) && (slope as number) >= (minSlope as number);
|
return Number.isFinite(slope) && (slope as number) >= (minSlope as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 횡단 기준 — 엔진이 낸 실측 단면적을 그대로 쓴다. */
|
/** 횡단 기준 — 엔진이 낸 실측 단면적을 그대로 쓴다. */
|
||||||
function crossAreaSamples(sections: CrossSection[], minSlope?: number): AreaSample[] {
|
function crossAreaSamples(sections: MassHaulSection[], minSlope?: number): AreaSample[] {
|
||||||
return sections.map((section) => {
|
return sections.map((section) => {
|
||||||
const cut = splitCut(section.design);
|
const cut = splitCut(section.design);
|
||||||
return {
|
return {
|
||||||
@@ -353,8 +353,8 @@ function crossAreaSamples(sections: CrossSection[], minSlope?: number): AreaSamp
|
|||||||
* 물려 두 기준이 같은 환산을 타게 한다(비율을 못 구하면 전량 토사로 둔다).
|
* 물려 두 기준이 같은 환산을 타게 한다(비율을 못 구하면 전량 토사로 둔다).
|
||||||
*/
|
*/
|
||||||
function longitudinalAreaSamples(
|
function longitudinalAreaSamples(
|
||||||
longitudinal: LongitudinalSection,
|
longitudinal: MassHaulLongitudinal,
|
||||||
sections: CrossSection[],
|
sections: MassHaulSection[],
|
||||||
): AreaSample[] {
|
): AreaSample[] {
|
||||||
const profile = longitudinal.design_profiles?.[0];
|
const profile = longitudinal.design_profiles?.[0];
|
||||||
if (!profile || !sections.length) return [];
|
if (!profile || !sections.length) return [];
|
||||||
@@ -397,7 +397,7 @@ function longitudinalAreaSamples(
|
|||||||
* 확정 저장과 B08 인계에 쓰는 **정식 곡선**이라 시그니처를 그대로 유지한다.
|
* 확정 저장과 B08 인계에 쓰는 **정식 곡선**이라 시그니처를 그대로 유지한다.
|
||||||
*/
|
*/
|
||||||
export function computeMassHaul(
|
export function computeMassHaul(
|
||||||
crossSections: CrossSection[],
|
crossSections: MassHaulSection[],
|
||||||
conversion: EarthworkConversion,
|
conversion: EarthworkConversion,
|
||||||
naturalSpoilMinSlope?: number,
|
naturalSpoilMinSlope?: number,
|
||||||
): MassHaulResult | null {
|
): MassHaulResult | null {
|
||||||
@@ -412,8 +412,8 @@ export function computeMassHaul(
|
|||||||
* 계산이 불가능한 기준(설계 측점 부족, 종단 계획선 없음)은 조용히 빠진다.
|
* 계산이 불가능한 기준(설계 측점 부족, 종단 계획선 없음)은 조용히 빠진다.
|
||||||
*/
|
*/
|
||||||
export function computeMassHaulSeries(
|
export function computeMassHaulSeries(
|
||||||
longitudinal: LongitudinalSection,
|
longitudinal: MassHaulLongitudinal,
|
||||||
crossSections: CrossSection[],
|
crossSections: MassHaulSection[],
|
||||||
conversion: EarthworkConversion,
|
conversion: EarthworkConversion,
|
||||||
naturalSpoilMinSlope?: number,
|
naturalSpoilMinSlope?: number,
|
||||||
): MassHaulSeries[] {
|
): MassHaulSeries[] {
|
||||||
+6
-6
@@ -1,8 +1,8 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B06_wf3_ProfileCross_UI_MassHaul_Balance.ts
|
* common_util_mass_haul_balance.ts
|
||||||
* 토량 분배(평형선 · 운반 블록 · 장비 띠) 계산 엔진 — 순수 함수, DOM 의존 없음.
|
* 토량 분배(평형선 · 운반 블록 · 장비 띠) 계산 엔진 — 순수 함수, DOM 의존 없음.
|
||||||
*
|
*
|
||||||
* 누가토량 곡선(`_UI_MassHaul`)은 "얼마가 남는가"만 말한다. 이 파일은 그 곡선에서
|
* 누가토량 곡선(`common_util_mass_haul`)은 "얼마가 남는가"만 말한다. 이 파일은 그 곡선에서
|
||||||
* **어디 흙을 어디로 얼마나 옮기는가**를 뽑아 낸다: 평형선 → 운반 블록 → 장비 띠 →
|
* **어디 흙을 어디로 얼마나 옮기는가**를 뽑아 낸다: 평형선 → 운반 블록 → 장비 띠 →
|
||||||
* 띠별 운반토량 `Q` · 평균운반거리 `L` · 지반유형 안분.
|
* 띠별 운반토량 `Q` · 평균운반거리 `L` · 지반유형 안분.
|
||||||
*
|
*
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
* `MassHaulResult.cut_natural_m3` / 측점별 `cut_soil_m3`·`cut_rr_m3`·`cut_br_m3`가 따로 든다.
|
* `MassHaulResult.cut_natural_m3` / 측점별 `cut_soil_m3`·`cut_rr_m3`·`cut_br_m3`가 따로 든다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type { MassHaulPoint, MassHaulResult } from "./B06_wf3_ProfileCross_UI_MassHaul";
|
import type { MassHaulPoint, MassHaulResult } from "./common_util_mass_haul";
|
||||||
import {
|
import {
|
||||||
crossFrom,
|
crossFrom,
|
||||||
EPSILON,
|
EPSILON,
|
||||||
@@ -55,16 +55,16 @@ import {
|
|||||||
extremaIndices,
|
extremaIndices,
|
||||||
pruneExtrema,
|
pruneExtrema,
|
||||||
segmentOf,
|
segmentOf,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul_Curve";
|
} from "./common_util_mass_haul_curve";
|
||||||
import {
|
import {
|
||||||
apportion,
|
apportion,
|
||||||
cutMix,
|
cutMix,
|
||||||
naturalSpoilRatio,
|
naturalSpoilRatio,
|
||||||
settleResiduals,
|
settleResiduals,
|
||||||
sortedLimits,
|
sortedLimits,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul_Settle";
|
} from "./common_util_mass_haul_settle";
|
||||||
|
|
||||||
export { haulPlanPayload } from "./B06_wf3_ProfileCross_UI_MassHaul_Settle";
|
export { haulPlanPayload } from "./common_util_mass_haul_settle";
|
||||||
|
|
||||||
/** 운반거리 상한 하나(m). `max_distance_m: null`이면 상한 없음(나머지를 전부 받는다). */
|
/** 운반거리 상한 하나(m). `max_distance_m: null`이면 상한 없음(나머지를 전부 받는다). */
|
||||||
export interface HaulEquipmentLimit {
|
export interface HaulEquipmentLimit {
|
||||||
+9
-9
@@ -1,5 +1,5 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B06_wf3_ProfileCross_UI_MassHaul_Balance_View.ts
|
* common_util_mass_haul_balance_view.ts
|
||||||
* 토량 분배(평형선 · 장비 띠) SVG 렌더러 — 유토곡선 위에 겹치는 레이어.
|
* 토량 분배(평형선 · 장비 띠) SVG 렌더러 — 유토곡선 위에 겹치는 레이어.
|
||||||
*
|
*
|
||||||
* 그리는 것:
|
* 그리는 것:
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
* 사토·토취는 도형 없이 **언더바만**(2026-08-02 사용자 지시).
|
* 사토·토취는 도형 없이 **언더바만**(2026-08-02 사용자 지시).
|
||||||
*
|
*
|
||||||
* 좌표 변환(x/y)과 그릴 상자는 유토곡선 렌더러가 넘겨준다 — 축을 두 번 정의하지 않는다.
|
* 좌표 변환(x/y)과 그릴 상자는 유토곡선 렌더러가 넘겨준다 — 축을 두 번 정의하지 않는다.
|
||||||
* 계산은 전부 `_UI_MassHaul_Balance`가 끝내 두므로 여기서는 배치만 판단한다.
|
* 계산은 전부 `common_util_mass_haul_balance`가 끝내 두므로 여기서는 배치만 판단한다.
|
||||||
* balloon의 도형·자리 찾기·드래그·위치 보관은 `_UI_MassHaul_Balloon`이 맡는다.
|
* balloon의 도형·자리 찾기·드래그·위치 보관은 `common_util_mass_haul_balloon`이 맡는다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -22,8 +22,8 @@ import type {
|
|||||||
HaulPlan,
|
HaulPlan,
|
||||||
HaulResidual,
|
HaulResidual,
|
||||||
HaulTransfer,
|
HaulTransfer,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul_Balance";
|
} from "./common_util_mass_haul_balance";
|
||||||
import type { BalanceLayerBox, PlacedBox } from "./B06_wf3_ProfileCross_UI_MassHaul_Balloon";
|
import type { BalanceLayerBox, PlacedBox } from "./common_util_mass_haul_balloon";
|
||||||
import {
|
import {
|
||||||
attachBalloonDrag,
|
attachBalloonDrag,
|
||||||
balloonOffsets,
|
balloonOffsets,
|
||||||
@@ -42,15 +42,15 @@ import {
|
|||||||
PAD_Y,
|
PAD_Y,
|
||||||
textWidth,
|
textWidth,
|
||||||
TWO_LINE_ROOM_PX,
|
TWO_LINE_ROOM_PX,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul_Balloon";
|
} from "./common_util_mass_haul_balloon";
|
||||||
import { L, stationLabel, svgElement, svgText } from "./B06_wf3_ProfileCross_UI_Section_Common";
|
import { L, stationLabel, svgElement, svgText } from "./common_util_svg";
|
||||||
|
|
||||||
export type { BalanceLayerBox } from "./B06_wf3_ProfileCross_UI_MassHaul_Balloon";
|
export type { BalanceLayerBox } from "./common_util_mass_haul_balloon";
|
||||||
export {
|
export {
|
||||||
balloonOffsetsPayload,
|
balloonOffsetsPayload,
|
||||||
configureBalloonOffsets,
|
configureBalloonOffsets,
|
||||||
resetBalloonOffsets,
|
resetBalloonOffsets,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul_Balloon";
|
} from "./common_util_mass_haul_balloon";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 옮긴 balloon이 그래프 밖으로 나가지 않게 이동량을 자른다. 화면을 줄이면 상자가 좁아져
|
* 옮긴 balloon이 그래프 밖으로 나가지 않게 이동량을 자른다. 화면을 줄이면 상자가 좁아져
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B06_wf3_ProfileCross_UI_MassHaul_Balloon.ts
|
* common_util_mass_haul_balloon.ts
|
||||||
* 유토곡선 balloon의 공통 부품 — 좌표 상자 · 도형 · 자리 찾기 · 드래그 · 위치 보관.
|
* 유토곡선 balloon의 공통 부품 — 좌표 상자 · 도형 · 자리 찾기 · 드래그 · 위치 보관.
|
||||||
*
|
*
|
||||||
* 토량 분배 렌더러(`_UI_MassHaul_Balance_View`)가 700줄을 넘겨, 「무엇을 그리나」(그쪽)와
|
* 토량 분배 렌더러(`_UI_MassHaul_Balance_View`)가 700줄을 넘겨, 「무엇을 그리나」(그쪽)와
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
* **함께** 쓰므로 배치·드래그 규칙이 한 곳에서만 정의된다.
|
* **함께** 쓰므로 배치·드래그 규칙이 한 곳에서만 정의된다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { L } from "./B06_wf3_ProfileCross_UI_Section_Common";
|
import { L } from "./common_util_svg";
|
||||||
import type { LocaleKey } from "@ui/ui_template_locale";
|
import type { LocaleKey } from "@ui/ui_template_locale";
|
||||||
import { svgElement } from "./B06_wf3_ProfileCross_UI_Section_Common";
|
import { svgElement } from "./common_util_svg";
|
||||||
|
|
||||||
/** 유토곡선 렌더러가 넘겨주는 좌표계와 그릴 수 있는 상자(px). */
|
/** 유토곡선 렌더러가 넘겨주는 좌표계와 그릴 수 있는 상자(px). */
|
||||||
export interface BalanceLayerBox {
|
export interface BalanceLayerBox {
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B06_wf3_ProfileCross_UI_MassHaul_Curve.ts
|
* common_util_mass_haul_curve.ts
|
||||||
* 누가토량 곡선의 기하 — 포물선 보간 · 수평선 교점 · 극값 추출.
|
* 누가토량 곡선의 기하 — 포물선 보간 · 수평선 교점 · 극값 추출.
|
||||||
*
|
*
|
||||||
* ── 측점 사이를 무엇으로 잇는가: **직선**(2026-08-02 사용자 확정) ──────────
|
* ── 측점 사이를 무엇으로 잇는가: **직선**(2026-08-02 사용자 확정) ──────────
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
*
|
*
|
||||||
* 되돌릴 일이 생기면 `PARABOLIC_MASS_CURVE` 하나만 켜면 된다 — 그리기·재기가 함께 바뀐다.
|
* 되돌릴 일이 생기면 `PARABOLIC_MASS_CURVE` 하나만 켜면 된다 — 그리기·재기가 함께 바뀐다.
|
||||||
*
|
*
|
||||||
* 토량 분배 계산(`_UI_MassHaul_Balance`)이 700줄을 넘겨 곡선 기하만 여기로 떼어냈다.
|
* 토량 분배 계산(`common_util_mass_haul_balance`)이 700줄을 넘겨 곡선 기하만 여기로 떼어냈다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type { MassHaulPoint } from "./B06_wf3_ProfileCross_UI_MassHaul";
|
import type { MassHaulPoint } from "./common_util_mass_haul";
|
||||||
|
|
||||||
export const EPSILON = 1e-9;
|
export const EPSILON = 1e-9;
|
||||||
|
|
||||||
+6
-6
@@ -1,24 +1,24 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B06_wf3_ProfileCross_UI_MassHaul_Settle.ts
|
* common_util_mass_haul_settle.ts
|
||||||
* 토량 분배의 **정산** 쪽 — 지반유형 안분 · 장비 선정 · 잉여/부족 상쇄 · 직렬화.
|
* 토량 분배의 **정산** 쪽 — 지반유형 안분 · 장비 선정 · 잉여/부족 상쇄 · 직렬화.
|
||||||
*
|
*
|
||||||
* `_UI_MassHaul_Balance`가 700줄을 넘겨, 「곡선에서 블록·띠를 어떻게 뽑나」(그쪽)와
|
* `common_util_mass_haul_balance`가 700줄을 넘겨, 「곡선에서 블록·띠를 어떻게 뽑나」(그쪽)와
|
||||||
* 「뽑은 물량을 무엇으로 나누고 어디로 보내나」(여기)로 갈랐다.
|
* 「뽑은 물량을 무엇으로 나누고 어디로 보내나」(여기)로 갈랐다.
|
||||||
*
|
*
|
||||||
* 의존 방향은 **한 쪽뿐**이다: Balance → Settle(함수), Settle → Balance(타입만).
|
* 의존 방향은 **한 쪽뿐**이다: Balance → Settle(함수), Settle → Balance(타입만).
|
||||||
* 타입 import는 실행 시 사라지므로 순환이 생기지 않는다.
|
* 타입 import는 실행 시 사라지므로 순환이 생기지 않는다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type { EarthworkConversion, GroundType } from "./B06_wf3_ProfileCross_Api_Fetch";
|
import type { EarthworkConversion, GroundType } from "./common_util_mass_haul_types";
|
||||||
import type { MassHaulPoint } from "./B06_wf3_ProfileCross_UI_MassHaul";
|
import type { MassHaulPoint } from "./common_util_mass_haul";
|
||||||
import type {
|
import type {
|
||||||
HaulBand,
|
HaulBand,
|
||||||
HaulEquipmentLimit,
|
HaulEquipmentLimit,
|
||||||
HaulPlan,
|
HaulPlan,
|
||||||
HaulResidual,
|
HaulResidual,
|
||||||
HaulTransfer,
|
HaulTransfer,
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul_Balance";
|
} from "./common_util_mass_haul_balance";
|
||||||
import { EPSILON } from "./B06_wf3_ProfileCross_UI_MassHaul_Curve";
|
import { EPSILON } from "./common_util_mass_haul_curve";
|
||||||
|
|
||||||
/** 환산계수가 비어 있거나 값이 이상하면 1.0으로 떨어뜨려 계산이 멈추지 않게 한다. */
|
/** 환산계수가 비어 있거나 값이 이상하면 1.0으로 떨어뜨려 계산이 멈추지 않게 한다. */
|
||||||
function factorFor(conversion: EarthworkConversion, ground: GroundType): number {
|
function factorFor(conversion: EarthworkConversion, ground: GroundType): number {
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* common_util_mass_haul_types.ts
|
||||||
|
* 유토곡선 엔진이 읽는 최소 입력 타입 — B05(계획 기준)와 B06(정식 기준)이 함께 쓴다.
|
||||||
|
*
|
||||||
|
* 엔진은 횡단 설계 결과에서 **자기가 실제로 읽는 필드만** 알면 된다. 페이지별 API 응답
|
||||||
|
* 타입(`B06_..._Api_Fetch.CrossSection` 등)을 그대로 요구하면 B05가 B06 모듈을 끌어와야
|
||||||
|
* 하므로, 구조적 부분집합만 여기 두고 각 페이지 타입이 그대로 대입되게 한다.
|
||||||
|
*
|
||||||
|
* 지반유형·토량환산계수는 정의처가 `config_system.py` 한 곳이므로 여기서도 한 번만 적고,
|
||||||
|
* 페이지 API 타입 모듈이 이 정의를 재수출한다(사본 금지).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
export type GroundType = "soil" | "ripping_rock" | "blasting_rock";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지반유형 하나의 토량환산계수(둘 다 자연상태 기준 배율).
|
||||||
|
* loose = 흐트러진 상태 / 자연상태(팽창률), compacted = 다져진 상태 / 자연상태(다짐률).
|
||||||
|
* 정의처는 config_system.EARTHWORK_CONVERSION_FACTORS 한 곳뿐이다.
|
||||||
|
*/
|
||||||
|
export interface EarthworkConversionFactor {
|
||||||
|
loose: number;
|
||||||
|
compacted: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EarthworkConversion = Record<GroundType, EarthworkConversionFactor>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 평균운반거리 상한 하나(m). `max_distance_m: null`이면 상한 없음(나머지를 전부 받는다).
|
||||||
|
* 정의처는 config_system.EARTHWORK_HAUL_EQUIPMENT_LIMITS_M 한 곳뿐이라 프론트는 받아만 쓴다.
|
||||||
|
*/
|
||||||
|
export interface HaulEquipmentLimit {
|
||||||
|
key: string;
|
||||||
|
max_distance_m: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 유토곡선 balloon을 끌어 옮긴 위치(띠 번호 → [dx, dy], px). */
|
||||||
|
export type BalloonOffsets = Record<string, [number, number]>;
|
||||||
|
|
||||||
|
/** 엔진이 읽는 횡단 설계 필드만 추린 구조. 페이지 `CrossDesign`이 그대로 대입된다. */
|
||||||
|
export interface MassHaulDesign {
|
||||||
|
ground_type: GroundType;
|
||||||
|
roadbed_width_m: number;
|
||||||
|
cut_area_m2: number;
|
||||||
|
cut_soil_area_m2?: number;
|
||||||
|
cut_rock_area_m2?: number;
|
||||||
|
cut_rock_kind?: GroundType | null;
|
||||||
|
fill_area_m2: number;
|
||||||
|
/** 성토측 자연 지반 경사(rise/run). 자연방토 판정 입력. 성토측이 없으면 null. */
|
||||||
|
fill_ground_slope?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 엔진이 읽는 측점 필드만 추린 구조. 페이지 `CrossSection`이 그대로 대입된다. */
|
||||||
|
export interface MassHaulSection {
|
||||||
|
station_id: string;
|
||||||
|
chainage_m: number;
|
||||||
|
design?: MassHaulDesign;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 계획선 샘플 중 개략 토량 산출에 쓰는 필드(`difference_m = 계획고 − 지반고`). */
|
||||||
|
export interface MassHaulProfileSample {
|
||||||
|
chainage_m: number;
|
||||||
|
difference_m: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MassHaulProfile {
|
||||||
|
samples: MassHaulProfileSample[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 엔진이 읽는 종단 필드만 추린 구조. 페이지 `LongitudinalSection`이 그대로 대입된다. */
|
||||||
|
export interface MassHaulLongitudinal {
|
||||||
|
length_m: number;
|
||||||
|
design_profiles?: MassHaulProfile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 유토곡선에 측점선을 세울 때 읽는 필드. `kind`는 선 색 변형 클래스에 그대로 붙는다. */
|
||||||
|
export interface MassHaulStation {
|
||||||
|
station_id: string;
|
||||||
|
chainage_m: number;
|
||||||
|
kind: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MassHaulStationSource {
|
||||||
|
stations: MassHaulStation[];
|
||||||
|
}
|
||||||
+39
-38
@@ -1,5 +1,5 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B06_wf3_ProfileCross_UI_MassHaul_View.ts
|
* common_util_mass_haul_view.ts
|
||||||
* 유토곡선(Mass Haul Diagram) SVG 렌더러.
|
* 유토곡선(Mass Haul Diagram) SVG 렌더러.
|
||||||
*
|
*
|
||||||
* 종단면도(`_UI_Longitudinal`) 바로 아래에 붙어 **같은 X 매핑**을 쓴다. 폭·좌우 여백
|
* 종단면도(`_UI_Longitudinal`) 바로 아래에 붙어 **같은 X 매핑**을 쓴다. 폭·좌우 여백
|
||||||
@@ -15,25 +15,25 @@
|
|||||||
* 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md 3.2절
|
* 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md 3.2절
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import type { LongitudinalSection } from "./B06_wf3_ProfileCross_Api_Fetch";
|
import type { MassHaulBasis, MassHaulPoint, MassHaulSeries } from "./common_util_mass_haul";
|
||||||
import type {
|
import type { MassHaulStationSource } from "./common_util_mass_haul_types";
|
||||||
MassHaulBasis,
|
import type { HaulPlan } from "./common_util_mass_haul_balance";
|
||||||
MassHaulPoint,
|
|
||||||
MassHaulSeries,
|
|
||||||
} from "./B06_wf3_ProfileCross_UI_MassHaul";
|
|
||||||
import type { HaulPlan } from "./B06_wf3_ProfileCross_UI_MassHaul_Balance";
|
|
||||||
import type { LocaleKey } from "@ui/ui_template_locale";
|
import type { LocaleKey } from "@ui/ui_template_locale";
|
||||||
import { MASS_HAUL_BALANCE_KEY } from "./B06_wf3_ProfileCross_UI_MassHaul";
|
import { MASS_HAUL_BALANCE_KEY } from "./common_util_mass_haul";
|
||||||
import { appendBalanceLayer, haulPlanChips } from "./B06_wf3_ProfileCross_UI_MassHaul_Balance_View";
|
import { appendBalanceLayer, haulPlanChips } from "./common_util_mass_haul_balance_view";
|
||||||
import { segmentOf } from "./B06_wf3_ProfileCross_UI_MassHaul_Curve";
|
import { segmentOf } from "./common_util_mass_haul_curve";
|
||||||
import {
|
import { L, stationLabel, svgElement, svgText } from "./common_util_svg";
|
||||||
L,
|
|
||||||
LONG_PAD,
|
/**
|
||||||
longitudinalMaxChainage,
|
* 유토곡선 X축을 **호출한 쪽 종단 그래프와 맞추는** 값. 좌우 여백과 누가거리 최댓값이
|
||||||
stationLabel,
|
* 종단도와 같아야 같은 측점이 같은 가로 위치에 온다. 페이지마다 종단 렌더러가 다르므로
|
||||||
svgElement,
|
* (B06 `_UI_Longitudinal`, B05 종단 그래프) 상수를 이 모듈에 두지 않고 받아 쓴다.
|
||||||
svgText,
|
*/
|
||||||
} from "./B06_wf3_ProfileCross_UI_Section_Common";
|
export interface MassHaulAxis {
|
||||||
|
maxChainageM: number;
|
||||||
|
padLeft: number;
|
||||||
|
padRight: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** 축 눈금이 읽히는 최소 높이. 이보다 낮아지면 그래프가 뭉개진다. */
|
/** 축 눈금이 읽히는 최소 높이. 이보다 낮아지면 그래프가 뭉개진다. */
|
||||||
export const MASS_HAUL_MIN_HEIGHT = 90;
|
export const MASS_HAUL_MIN_HEIGHT = 90;
|
||||||
@@ -64,7 +64,7 @@ function seriesClass(series: MassHaulSeries, base: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 곡선 경로. **`_UI_MassHaul_Curve`가 쓰는 것과 같은 기하**로 그려야 한다 — 평형선·장비
|
* 곡선 경로. **`common_util_mass_haul_curve`가 쓰는 것과 같은 기하**로 그려야 한다 — 평형선·장비
|
||||||
* 경계현·평균운반거리선의 양 끝이 그 기하와의 교점으로 계산되므로, 그리기와 재기가 어긋나면
|
* 경계현·평균운반거리선의 양 끝이 그 기하와의 교점으로 계산되므로, 그리기와 재기가 어긋나면
|
||||||
* 수평선 끝이 곡선에서 떠 보인다(2026-08-02에 겪은 문제).
|
* 수평선 끝이 곡선에서 떠 보인다(2026-08-02에 겪은 문제).
|
||||||
*
|
*
|
||||||
@@ -132,7 +132,7 @@ function volumeRange(series: MassHaulSeries[]): { min: number; max: number } {
|
|||||||
*/
|
*/
|
||||||
export function massHaulPointAt(
|
export function massHaulPointAt(
|
||||||
series: MassHaulSeries,
|
series: MassHaulSeries,
|
||||||
longitudinal: LongitudinalSection,
|
longitudinal: MassHaulStationSource,
|
||||||
stationId: string | null,
|
stationId: string | null,
|
||||||
): MassHaulPoint | null {
|
): MassHaulPoint | null {
|
||||||
if (!stationId) return null;
|
if (!stationId) return null;
|
||||||
@@ -265,7 +265,8 @@ function appendVolumeCallout(
|
|||||||
export function createMassHaulChart(
|
export function createMassHaulChart(
|
||||||
series: MassHaulSeries[],
|
series: MassHaulSeries[],
|
||||||
visibleKeys: ReadonlySet<string>,
|
visibleKeys: ReadonlySet<string>,
|
||||||
longitudinal: LongitudinalSection,
|
longitudinal: MassHaulStationSource,
|
||||||
|
axis: MassHaulAxis,
|
||||||
selectedStationId: string | null,
|
selectedStationId: string | null,
|
||||||
stationInterval: number,
|
stationInterval: number,
|
||||||
widthPx: number,
|
widthPx: number,
|
||||||
@@ -286,11 +287,11 @@ export function createMassHaulChart(
|
|||||||
svg.style.minWidth = `${minimumWidthPx}px`;
|
svg.style.minWidth = `${minimumWidthPx}px`;
|
||||||
svg.append(svgElement("rect", { width: widthPx, height: heightPx, class: "b06-chart__bg" }));
|
svg.append(svgElement("rect", { width: widthPx, height: heightPx, class: "b06-chart__bg" }));
|
||||||
|
|
||||||
// 종단면도와 완전히 같은 X 매핑 — 여백·최댓값 모두 종단 렌더러와 공유한다.
|
// 종단면도와 완전히 같은 X 매핑 — 여백·최댓값 모두 호출한 쪽 종단 렌더러에서 받는다.
|
||||||
const maxChainage = longitudinalMaxChainage(longitudinal);
|
const maxChainage = Math.max(axis.maxChainageM, 1);
|
||||||
const plotWidth = widthPx - LONG_PAD.left - LONG_PAD.right;
|
const plotWidth = widthPx - axis.padLeft - axis.padRight;
|
||||||
const plotHeight = heightPx - MASS_PAD_TOP - MASS_PAD_BOTTOM;
|
const plotHeight = heightPx - MASS_PAD_TOP - MASS_PAD_BOTTOM;
|
||||||
const x = (chainage: number) => LONG_PAD.left + (chainage / maxChainage) * plotWidth;
|
const x = (chainage: number) => axis.padLeft + (chainage / maxChainage) * plotWidth;
|
||||||
|
|
||||||
const { min, max } = volumeRange(series);
|
const { min, max } = volumeRange(series);
|
||||||
const span = Math.max(max - min, 1e-6);
|
const span = Math.max(max - min, 1e-6);
|
||||||
@@ -303,14 +304,14 @@ export function createMassHaulChart(
|
|||||||
const value = max - ratio * span;
|
const value = max - ratio * span;
|
||||||
svg.append(
|
svg.append(
|
||||||
svgElement("line", {
|
svgElement("line", {
|
||||||
x1: LONG_PAD.left,
|
x1: axis.padLeft,
|
||||||
y1: gridY,
|
y1: gridY,
|
||||||
x2: widthPx - LONG_PAD.right,
|
x2: widthPx - axis.padRight,
|
||||||
y2: gridY,
|
y2: gridY,
|
||||||
class: "b06-chart__grid",
|
class: "b06-chart__grid",
|
||||||
}),
|
}),
|
||||||
svgText(formatVolume(value), {
|
svgText(formatVolume(value), {
|
||||||
x: LONG_PAD.left - 9,
|
x: axis.padLeft - 9,
|
||||||
y: gridY + 4,
|
y: gridY + 4,
|
||||||
"text-anchor": "end",
|
"text-anchor": "end",
|
||||||
class: "b06-chart__tick",
|
class: "b06-chart__tick",
|
||||||
@@ -390,14 +391,14 @@ export function createMassHaulChart(
|
|||||||
// Y 범위 계산(`volumeRange`)이 0을 항상 품으므로 이 선은 어떤 노선에서도 화면에 남는다.
|
// Y 범위 계산(`volumeRange`)이 0을 항상 품으므로 이 선은 어떤 노선에서도 화면에 남는다.
|
||||||
svg.append(
|
svg.append(
|
||||||
svgElement("line", {
|
svgElement("line", {
|
||||||
x1: LONG_PAD.left,
|
x1: axis.padLeft,
|
||||||
y1: zeroY,
|
y1: zeroY,
|
||||||
x2: widthPx - LONG_PAD.right,
|
x2: widthPx - axis.padRight,
|
||||||
y2: zeroY,
|
y2: zeroY,
|
||||||
class: "b06-masshaul__zero",
|
class: "b06-masshaul__zero",
|
||||||
}),
|
}),
|
||||||
svgText("0", {
|
svgText("0", {
|
||||||
x: LONG_PAD.left - 9,
|
x: axis.padLeft - 9,
|
||||||
y: zeroY + 4,
|
y: zeroY + 4,
|
||||||
"text-anchor": "end",
|
"text-anchor": "end",
|
||||||
class: "b06-chart__tick b06-masshaul__zero-tick",
|
class: "b06-chart__tick b06-masshaul__zero-tick",
|
||||||
@@ -420,8 +421,8 @@ export function createMassHaulChart(
|
|||||||
appendBalanceLayer(svg, haulPlan, {
|
appendBalanceLayer(svg, haulPlan, {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
left: LONG_PAD.left,
|
left: axis.padLeft,
|
||||||
right: widthPx - LONG_PAD.right,
|
right: widthPx - axis.padRight,
|
||||||
top: MASS_PAD_TOP,
|
top: MASS_PAD_TOP,
|
||||||
bottom: heightPx - MASS_PAD_BOTTOM,
|
bottom: heightPx - MASS_PAD_BOTTOM,
|
||||||
curveYAt,
|
curveYAt,
|
||||||
@@ -444,8 +445,8 @@ export function createMassHaulChart(
|
|||||||
appendVolumeCallout(svg, focus, stationInterval, {
|
appendVolumeCallout(svg, focus, stationInterval, {
|
||||||
anchorX: focusX,
|
anchorX: focusX,
|
||||||
anchorY: y(focus.cumulative_volume_m3),
|
anchorY: y(focus.cumulative_volume_m3),
|
||||||
left: LONG_PAD.left,
|
left: axis.padLeft,
|
||||||
right: widthPx - LONG_PAD.right,
|
right: widthPx - axis.padRight,
|
||||||
top: MASS_PAD_TOP,
|
top: MASS_PAD_TOP,
|
||||||
bottom: heightPx - MASS_PAD_BOTTOM,
|
bottom: heightPx - MASS_PAD_BOTTOM,
|
||||||
curveYAt,
|
curveYAt,
|
||||||
@@ -454,9 +455,9 @@ export function createMassHaulChart(
|
|||||||
|
|
||||||
svg.append(
|
svg.append(
|
||||||
svgElement("line", {
|
svgElement("line", {
|
||||||
x1: LONG_PAD.left,
|
x1: axis.padLeft,
|
||||||
y1: MASS_PAD_TOP,
|
y1: MASS_PAD_TOP,
|
||||||
x2: LONG_PAD.left,
|
x2: axis.padLeft,
|
||||||
y2: heightPx - MASS_PAD_BOTTOM,
|
y2: heightPx - MASS_PAD_BOTTOM,
|
||||||
class: "b06-chart__axis",
|
class: "b06-chart__axis",
|
||||||
}),
|
}),
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* common_util_svg.ts
|
||||||
|
* SVG 도면 렌더러가 공유하는 최소 유틸리티 — 요소 생성·텍스트·측점 표기.
|
||||||
|
*
|
||||||
|
* B06 종·횡단 렌더러가 쓰던 것을 유토곡선 모듈 공용화(2026-08-03)에 맞춰 옮겼다.
|
||||||
|
* B05 계획 유토곡선과 B06 정식 유토곡선이 **같은 표기 규칙**을 쓰게 하는 것이 목적이며,
|
||||||
|
* B06 쪽은 `_UI_Section_Common`이 그대로 재수출해 기존 임포트 경로를 유지한다.
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||||
|
|
||||||
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
export function L(key: keyof typeof ui_locales): string {
|
||||||
|
return ui_locales[key][currentLanguageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function svgElement<K extends keyof SVGElementTagNameMap>(
|
||||||
|
tag: K,
|
||||||
|
attributes: Record<string, string | number> = {},
|
||||||
|
): SVGElementTagNameMap[K] {
|
||||||
|
const element = document.createElementNS(SVG_NS, tag);
|
||||||
|
Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, String(value)));
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function svgText(
|
||||||
|
value: string,
|
||||||
|
attributes: Record<string, string | number>,
|
||||||
|
): SVGTextElement {
|
||||||
|
const text = svgElement("text", attributes);
|
||||||
|
text.textContent = value;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 측점 간격을 실제 측점 배열에서 되짚는다(최빈 간격). 라벨 표기의 분모다. */
|
||||||
|
export function inferStationInterval(stations: Array<{ chainage_m: number }>): number {
|
||||||
|
const counts = new Map<number, number>();
|
||||||
|
for (let index = 1; index < stations.length; index += 1) {
|
||||||
|
const difference = stations[index].chainage_m - stations[index - 1].chainage_m;
|
||||||
|
if (difference <= 0) continue;
|
||||||
|
const rounded = Math.round(difference * 10) / 10;
|
||||||
|
counts.set(rounded, (counts.get(rounded) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
[...counts.entries()].sort(
|
||||||
|
([intervalA, countA], [intervalB, countB]) => countB - countA || intervalB - intervalA,
|
||||||
|
)[0]?.[0] ?? 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 측점 라벨 — "STA" 접두사 없이 `측점번호+잔여거리`(예: `4+0.0`). */
|
||||||
|
export function stationLabel(chainage: number, interval: number): string {
|
||||||
|
const safeInterval = interval > 0 ? interval : 1;
|
||||||
|
let stationNumber = Math.floor((chainage + 1e-6) / safeInterval);
|
||||||
|
let remainder = chainage - stationNumber * safeInterval;
|
||||||
|
if (Math.abs(remainder) < 0.05) remainder = 0;
|
||||||
|
if (remainder >= safeInterval - 0.05) {
|
||||||
|
stationNumber += 1;
|
||||||
|
remainder = 0;
|
||||||
|
}
|
||||||
|
return `${stationNumber}+${remainder.toFixed(1)}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user