feat(B06): 횡단도에 사토장 성토선·말풍선 표시
교본 6장 3절이 운반처리 위치를 평면도·횡단도에 표시하도록 요구함. - 긴 파선+점(9 3 2 3) · 사토장 색 — **터파기 짧은 점선과 선 종류를 가름** - 이름표 「유용토운반작업장 ○㎡」 를 평상 위에 얹음 - 말풍선에 단면·폭·구간 용량·못 담은 몫·잘림 경고와 **근거 두 줄** (「폭은 노면 끝에서 잼」·「교본 6장 3절이 표시를 요구함」) - 지반을 못 만나 잘린 사토장은 붉게 — 사면 미폐합 경고와 같은 결 - 계획선을 만져도 사토장이 안 사라지게 저장분 폭·구간값을 이어 붙임 (폭은 구간 용량에서 서버가 정한 값이라 브라우저가 다시 풀지 않음) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -453,6 +453,25 @@ export interface CrossDesign {
|
||||
/** 암반부에 적용할 지반유형(`ripping_rock`/`blasting_rock`). 토사 측점은 null. */
|
||||
cut_rock_kind?: GroundType | null;
|
||||
fill_area_m2: number;
|
||||
/**
|
||||
* 사토장(유용토운반작업장) 몫 — **`fill_area_m2` 와 합치지 않는다**(2026-09-09 확정 ㉠).
|
||||
* 노면 끝 바깥은 노선 성토가 아니라 사토장 성토라, 받는 쪽이 갈라 볼 수 있어야 한다.
|
||||
* 사토장이 없는 측점은 전부 0·null 이다(구 데이터에는 아예 없어 optional).
|
||||
*/
|
||||
spoil_fill_area_m2?: number;
|
||||
spoil_fill_side?: "left" | "right" | null;
|
||||
spoil_fill_width_m?: number;
|
||||
spoil_fill_max_width_m?: number;
|
||||
spoil_fill_line?: Array<{ offset_m: number; elevation_m: number }>;
|
||||
spoil_fill_unclosed?: boolean;
|
||||
/** 사토장이 대신 차지해 노선 성토에서 뺀 몫(㎡) — 되짚기용. 합계에 또 넣지 말 것. */
|
||||
spoil_fill_replaced_fill_m2?: number;
|
||||
/** 구간(구조물) 단위 값 — 측점마다 같은 값이 실린다. 말풍선·수량이 되짚는 데 쓴다. */
|
||||
spoil_fill_capacity_m3?: number;
|
||||
spoil_fill_placed_m3?: number;
|
||||
spoil_fill_unplaced_m3?: number;
|
||||
spoil_fill_structure_id?: string | null;
|
||||
spoil_fill_extra_distance_m?: number | null;
|
||||
/** 성토측 자연 지반 경사(rise/run). 자연방토 판정 입력. 성토측이 없으면 null. */
|
||||
fill_ground_slope?: number | null;
|
||||
/**
|
||||
|
||||
@@ -89,7 +89,23 @@ export const USER_TOUCHED_KEYS = [
|
||||
] as const;
|
||||
|
||||
/** 다시 계산해도 살려 두는 값 — 위 사용자 값에 **상태를 나르는 둘**을 더한 것. */
|
||||
const PRESERVED_KEYS = ["status", "pavement_suggested", ...USER_TOUCHED_KEYS] as const;
|
||||
// 사토장 구간값 — **브라우저가 못 만드는 값**이라 이어 붙인다. 구간 전체를 봐야 나오는
|
||||
// 값이고(용량 배분) 정본은 [저장] 때 서버가 다시 낸다. 안 이으면 계획선을 만지는 순간
|
||||
// 말풍선에서 「구간 용량 …」이 사라진다.
|
||||
const SPOIL_KEYS = [
|
||||
"spoil_fill_capacity_m3",
|
||||
"spoil_fill_placed_m3",
|
||||
"spoil_fill_unplaced_m3",
|
||||
"spoil_fill_structure_id",
|
||||
"spoil_fill_extra_distance_m",
|
||||
] as const;
|
||||
|
||||
const PRESERVED_KEYS = [
|
||||
"status",
|
||||
"pavement_suggested",
|
||||
...USER_TOUCHED_KEYS,
|
||||
...SPOIL_KEYS,
|
||||
] as const;
|
||||
|
||||
function preserveUserFields(
|
||||
next: NonNullable<CrossSection["design"]>,
|
||||
@@ -281,6 +297,19 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
? section.curve_outer_side
|
||||
: null,
|
||||
curveWideningM: section.curve_widening_m ?? null,
|
||||
// 사토장 — **저장분 폭을 그대로 잇는다**. 폭은 구간 용량에서 서버가 정한 값이라
|
||||
// 브라우저가 다시 풀지 않는다(다시 풀면 그 측점만 폭이 달라져 작업장 모양이 깨진다).
|
||||
// 이어 붙이지 않으면 계획선을 만질 때마다 사토장이 그림에서 사라진다.
|
||||
spoilFill:
|
||||
typeof design.spoil_fill_width_m === "number" &&
|
||||
design.spoil_fill_width_m > 0 &&
|
||||
(design.spoil_fill_side === "left" || design.spoil_fill_side === "right")
|
||||
? {
|
||||
side: design.spoil_fill_side,
|
||||
widthM: design.spoil_fill_width_m,
|
||||
slopeRatioN: null,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_SpoilFill.ts
|
||||
* 횡단도에 **사토장(유용토운반작업장) 성토선**을 얹는다.
|
||||
*
|
||||
* 왜 있나 — 임도기술교본 6장 3절이 **운반처리 위치를 평면도와 횡단도에 표시**하도록
|
||||
* 요구한다(지식DB `01_임도/02_상세설계/유용토운반작업장.md` §2). 수량을 세기 전에
|
||||
* 도면에 그것이 보여야 한다.
|
||||
*
|
||||
* ⚠ **터파기 파선과 선 종류를 가른다** — 터파기는 짧은 점선, 사토장은 긴 파선+점.
|
||||
* 같은 파선으로 두면 도면에서 둘을 구별할 수 없다(2026-09-09 네 창 확정).
|
||||
* ⚠ 값은 설계 결과(`spoil_fill_*`)에서 온다 — 여기서 **다시 세지 않는다**.
|
||||
* ========================================================================== */
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/** 설계 결과에서 사토장 그리기에 쓰는 값만 추려 받는다. */
|
||||
export interface SpoilFillDrawing {
|
||||
spoil_fill_line?: Array<{ offset_m: number; elevation_m: number }> | null;
|
||||
spoil_fill_area_m2?: number | null;
|
||||
spoil_fill_width_m?: number | null;
|
||||
spoil_fill_unclosed?: boolean | null;
|
||||
spoil_fill_capacity_m3?: number | null;
|
||||
spoil_fill_placed_m3?: number | null;
|
||||
spoil_fill_unplaced_m3?: number | null;
|
||||
}
|
||||
|
||||
/** 말풍선 문구 — 무엇이 얼마나 쌓였는지와 **근거**를 함께 적는다. */
|
||||
export function spoilFillTooltip(design: SpoilFillDrawing): string {
|
||||
const area = Number(design.spoil_fill_area_m2 ?? 0);
|
||||
const width = Number(design.spoil_fill_width_m ?? 0);
|
||||
const lines = [
|
||||
`유용토운반작업장(구 사토장) · 단면 ${area.toFixed(2)}㎡ · 폭 ${width.toFixed(2)}m`,
|
||||
];
|
||||
const capacity = design.spoil_fill_capacity_m3;
|
||||
if (typeof capacity === "number" && capacity > 0) {
|
||||
const placed = Number(design.spoil_fill_placed_m3 ?? 0);
|
||||
lines.push(`구간 용량 ${capacity.toFixed(1)}㎥ 중 ${placed.toFixed(1)}㎥ 담김`);
|
||||
const unplaced = Number(design.spoil_fill_unplaced_m3 ?? 0);
|
||||
if (unplaced > 0) {
|
||||
lines.push(`⚠ ${unplaced.toFixed(1)}㎥ 는 못 담음 — 지반 자료가 있는 데까지만 넓힘`);
|
||||
}
|
||||
}
|
||||
if (design.spoil_fill_unclosed) {
|
||||
lines.push("⚠ 비탈이 원지반을 못 만나 잘림 — 지반 자료 범위를 넘어감");
|
||||
}
|
||||
lines.push("폭은 노면 끝(노견이 시작하는 자리)에서 잼 — 그 구간 노견도 이 성토 안에 듦");
|
||||
lines.push("교본 6장 3절이 평면도·횡단도 표시를 요구함");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 사토장 성토선을 그린다. 선이 없으면 아무것도 하지 않는다.
|
||||
* 돌려주는 값은 그린 폴리라인(없으면 `null`) — 부르는 쪽이 강조에 쓸 수 있다.
|
||||
*/
|
||||
export function appendSpoilFillOverlay(
|
||||
layer: SVGElement,
|
||||
design: SpoilFillDrawing | null | undefined,
|
||||
x: (offset: number) => number,
|
||||
toDisplayY: (elevation: number) => number,
|
||||
): SVGElement | null {
|
||||
const line = design?.spoil_fill_line;
|
||||
if (!design || !Array.isArray(line) || line.length < 2) return null;
|
||||
const polyline = document.createElementNS(SVG_NS, "polyline");
|
||||
polyline.setAttribute(
|
||||
"points",
|
||||
line.map((point) => `${x(point.offset_m)},${toDisplayY(point.elevation_m)}`).join(" "),
|
||||
);
|
||||
polyline.setAttribute(
|
||||
"class",
|
||||
design.spoil_fill_unclosed ? "b06-chart__spoil-fill is-unclosed" : "b06-chart__spoil-fill",
|
||||
);
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
title.textContent = spoilFillTooltip(design);
|
||||
polyline.append(title);
|
||||
layer.append(polyline);
|
||||
|
||||
// 이름표 — 평상 한가운데 위에 얹는다. 선만 있으면 그것이 무엇인지 도면에서 모른다.
|
||||
const first = line[0];
|
||||
const last = line[line.length - 1];
|
||||
const label = document.createElementNS(SVG_NS, "text");
|
||||
label.setAttribute("x", String((x(first.offset_m) + x(last.offset_m)) / 2));
|
||||
label.setAttribute("y", String(toDisplayY(Math.max(first.elevation_m, last.elevation_m)) - 4));
|
||||
label.setAttribute("text-anchor", "middle");
|
||||
label.setAttribute("class", "b06-chart__spoil-fill-label");
|
||||
label.textContent = `유용토운반작업장 ${Number(design.spoil_fill_area_m2 ?? 0).toFixed(2)}㎡`;
|
||||
const labelTitle = document.createElementNS(SVG_NS, "title");
|
||||
labelTitle.textContent = spoilFillTooltip(design);
|
||||
label.append(labelTitle);
|
||||
layer.append(label);
|
||||
return polyline;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
appendFordSurfaceDropPlan,
|
||||
} from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import { culvertWallsStandAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { appendSpoilFillOverlay } from "./B06_Section_UI_Cross_SpoilFill";
|
||||
import { appendRevetmentOverlay, computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment";
|
||||
import {
|
||||
appendCrossDesignOverlay,
|
||||
@@ -467,6 +468,9 @@ export function createCrossSectionCard(
|
||||
// 같은 트림 값을 쓰므로 "그림은 이런데 수량은 저렇다"가 생기지 않는다.
|
||||
applyStructureAreas(section, designTrim);
|
||||
appendCrossDesignOverlay(plotLayer, section.design, x, toDisplayY, drawSamples, designTrim);
|
||||
// 사토장(유용토운반작업장) — 교본 6장 3절이 횡단도 표시를 요구한다. 값은 설계 결과에서
|
||||
// 그대로 오고 여기서 다시 세지 않는다. 선 종류는 터파기 파선과 갈라 둔다.
|
||||
appendSpoilFillOverlay(plotLayer, section.design, x, toDisplayY);
|
||||
// 암 경계선 = 지면선 복사 + 오프셋(계획선 기준 아님).
|
||||
if (rockBoundary && section.design.geometry_preset === "rock") {
|
||||
appendRockBoundaryOverlay(
|
||||
|
||||
@@ -388,3 +388,26 @@
|
||||
stroke-dasharray: 4 3;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
/* 사토장(유용토운반작업장) — **터파기 파선과 선 종류를 가른다**(2026-09-09).
|
||||
터파기는 짧은 점선(4 3), 사토장은 **긴 파선 + 점**(9 3 2 3)이라 한눈에 갈린다.
|
||||
색은 등록부 사토장 색(#8c9a2e)을 그대로 쓴다. */
|
||||
.b06-chart__spoil-fill {
|
||||
fill: none;
|
||||
stroke: #8c9a2e;
|
||||
stroke-dasharray: 9 3 2 3;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.b06-chart__spoil-fill-label {
|
||||
fill: #6d7a20;
|
||||
font-size: 10px;
|
||||
paint-order: stroke;
|
||||
stroke: rgba(255, 255, 255, 0.9);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* 지반을 못 만나 잘린 사토장 — 사면 미폐합 경고와 같은 결로 붉게 알린다. */
|
||||
.b06-chart__spoil-fill.is-unclosed {
|
||||
stroke: #c0392b;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user