refactor(472-3 B06 벽 · 기슭막이 · 배치): 구조물 기하 logic 분리 · 그림 파일은 그리기만

- logic 새 파일 다섯 — Cross_Wall_Geom(벽 1매) · Cross_Wall_Hatch_Geom(무늬 띠 · 물빼기 · 도수로 띠 · 원지반선) · Cross_Excavation_Geom(벽 터파기) · Cross_Design_Surface_Geom(암선 · 포장층) · Cross_Revetment_Geom(독립 기슭막이 배치)
- logic 으로 옮김 — Body_Guard → Cross_Body_Guard · Structure_Layouts · Route_Carry_Patch(내용 그대로 · import 경로만)
- inferStationInterval → common_util_station(글자표 없는 자리 · common_util_svg 는 다시 내보냄) — Node 묶음 ui_template_locale 끌림 끊음
- 부르는 쪽 import 줄 · 등록부 ts 경로 · 시험 · 도우미 경로
- test_472_3 GEOMETRY_PULLS 다섯 줄 걷음 · Wall_Hatch · Excavation · Design_Surface 세 줄은 Culvert · Box · Ford 그림 파일(518-2)이 그리기 함수로 부르는 동안 남김

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 19:22:27 +09:00
co-authored by Claude Opus 5.5
parent 1d5d63875b
commit d3315da993
82 changed files with 1000 additions and 840 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ import { SECTION_STATION_INTERVAL_M } from "../../B05_Profile/logic/B05_Profile_
import {
applyStructureAreaRows,
structureAreaRows,
} from "../../B06_Section/B06_Section_Structure_Layouts";
} from "../../B06_Section/logic/B06_Section_Structure_Layouts";
import { withDraftAdjusts } from "../../B06_Section/B06_Section_Section_Adjusts";
import {
holdSectionDetail,
@@ -10,7 +10,7 @@
* ========================================================================== */
import type { CrossSection } from "../../B06_Section/B06_Section_Api_Fetch";
import { computeStoredLayouts } from "../../B06_Section/B06_Section_Structure_Layouts";
import { computeStoredLayouts } from "../../B06_Section/logic/B06_Section_Structure_Layouts";
export { DEFAULT_BASIN_SHAPE } from "../../B06_Section/logic/B06_Section_UI_Cross_Culvert_Basin";
export { pipeLengthText } from "../../B06_Section/logic/B06_Section_UI_Cross_Culvert_Const"; // 「8 m (7.6)」(225-1)
import { PIPE_MATCH_M } from "./design_selection";
+1 -1
View File
@@ -51,7 +51,7 @@ import {
applyStructureAreaRows,
extraWallRows,
structureAreaRows,
} from "../../../B06_Section/B06_Section_Structure_Layouts";
} from "../../../B06_Section/logic/B06_Section_Structure_Layouts";
import { carryRunners, type CarryInputs } from "./design_calc_node_carry";
import { crossRunners, PAGE_IDS, useDefinition, type CrossInputs } from "./design_calc_node_cross";
import { followRunners, type FollowInputs } from "./design_calc_node_follow";
@@ -18,7 +18,7 @@ import {
import {
routeCarryPatches,
type RouteCarryInput,
} from "../../../B06_Section/B06_Section_Route_Carry_Patch";
} from "../../../B06_Section/logic/B06_Section_Route_Carry_Patch";
type Pipe = FollowingPoint & { id?: string | null };
type Structure = FollowingStructure & { structure_id?: string | null };
@@ -28,7 +28,7 @@ import {
type RefreshInputs,
} from "../../../B06_Section/logic/B06_Section_Cross_Refresh_Calc";
import type { SectionDetailResponse } from "../../../B06_Section/B06_Section_Api_Fetch";
import { structureAreaRows } from "../../../B06_Section/B06_Section_Structure_Layouts";
import { structureAreaRows } from "../../../B06_Section/logic/B06_Section_Structure_Layouts";
import { gridAt } from "./design_calc_node_stages";
export interface CrossInputs {
@@ -47,7 +47,7 @@ import type {
SectionDetailResponse,
} from "../../../B06_Section/B06_Section_Api_Fetch";
import { createCrossStage } from "../../../B06_Section/logic/B06_Section_Cross_Stage";
import { pipeBook } from "../../../B06_Section/B06_Section_Route_Carry_Patch";
import { pipeBook } from "../../../B06_Section/logic/B06_Section_Route_Carry_Patch";
import { plantedBase } from "./design_planted_base";
import { AFTER_STAGES, BROWSER_STAGES, createDesignPipeline } from "./design_pipeline";
import { contextCross, placedFollowInputs } from "./design_pipeline_stage_inputs";
@@ -125,7 +125,7 @@ export const FACILITY_ROWS: readonly CalcEntry[] = [
outputs: ["관 길이 · 구조물 면적 · 선 다단 벽", "db:cross_sections.design"],
site: "browser_now",
stage: null,
ts: `${B06}B06_Section_Structure_Layouts.ts#structureAreaRows`,
ts: `${B06}logic/B06_Section_Structure_Layouts.ts#structureAreaRows`,
runner: "node:structure.layouts",
pair: null,
mirror: ["test_b06_pipe_length", "test_299_ford_span_pipe"],
@@ -50,10 +50,10 @@ import {
storedAdjusts,
type WallPath,
} from "./B05_Profile_UI_Corridor_Structures_Path";
import { computeRevetmentLayout } from "../B06_Section/B06_Section_UI_Cross_Revetment";
import { withoutGuardedFill } from "../B06_Section/B06_Section_UI_Cross_Body_Guard";
import { computeRevetmentLayout } from "../B06_Section/logic/B06_Section_Cross_Revetment_Geom";
import { withoutGuardedFill } from "../B06_Section/logic/B06_Section_Cross_Body_Guard";
import { chuteBands, fordPavementChute } from "./B05_Profile_UI_Corridor_Structures_Chute";
import { isChuteSegment } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch";
import { isChuteSegment } from "../B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom";
import { builds, culvertBody, ownsBody } from "./logic/B05_Profile_UI_Corridor_Structures_Pick";
/** 스윕 프레임 하나 — 노선 위 한 지점의 중심 XY·좌향 단위벡터와 종단 표고 오프셋. */
@@ -14,7 +14,7 @@ import {
chuteBandOutline,
chuteRun,
groundLine,
} from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch";
} from "../B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom";
import { outletFillLine } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement";
/** 성토 조각 하나 — B06 오버레이가 띠를 그리는 선(절토 조각은 뺀다). */
@@ -13,7 +13,7 @@
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
import type { CulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
import { computeCulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
import { bodyAbsolute } from "../B06_Section/B06_Section_UI_Cross_Body_Guard";
import { bodyAbsolute } from "../B06_Section/logic/B06_Section_Cross_Body_Guard";
import type { WallAdjust } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
import { ZERO_ADJUST } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
import {
@@ -23,7 +23,7 @@ import {
extraWalls,
inletStructure,
revetOffset,
} from "../B06_Section/B06_Section_Structure_Layouts";
} from "../B06_Section/logic/B06_Section_Structure_Layouts";
import { DEFAULT_BOX_SIDE_ADJUST } from "../B06_Section/B06_Section_UI_Cross_Box_Geom";
import type { BoxControl } from "../B06_Section/B06_Section_UI_Cross_Box_Panel";
import { culvertLinkFor, type CulvertLink } from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
+1 -1
View File
@@ -59,7 +59,7 @@ export type {
/** 물넘이포장 제원 — 정의처는 렌더 모듈이다(사본 금지, 타입 전용 import라 순환 없음). */
import type { FordPavementSpec } from "./B06_Section_UI_Cross_Ford_Pavement";
import type { RevetmentSpec } from "./B06_Section_UI_Cross_Revetment";
import type { RevetmentSpec } from "./logic/B06_Section_Cross_Revetment_Geom";
import type { PavementEntry } from "@util/common_util_structure_pavement";
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
export type { FordPavementSpec, RevetmentSpec };
+1 -1
View File
@@ -45,7 +45,7 @@ import type { ProfileAlignment } from "../B05_Profile/logic/B05_Profile_UI_Profi
import { readAlignment, toDesignProfile } from "../B05_Profile/logic/B05_Profile_UI_Profile_Data";
import { useStationKeys } from "./B06_Section_Station_Keys";
import { hasStaleDesigns, staleKey } from "./B06_Section_UI_Section_Common";
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
import { applyStructureAreaRows, structureAreaRows } from "./logic/B06_Section_Structure_Layouts";
import { calcOnServerNow } from "../A00_Common/design/design_calc_sites";
import {
areasRequest,
+1 -1
View File
@@ -24,7 +24,7 @@ import {
layPipeOptions,
type Cells,
type OptionPipe,
} from "./B06_Section_Route_Carry_Patch";
} from "./logic/B06_Section_Route_Carry_Patch";
// 얹기 몸통 = `B06_Section_Route_Carry_Patch` 한 벌(서버 노선 바뀐 [저장] 넘김도 · 472-6) — 여기는 세션 겉.
export { layPipeOptions, type OptionPipe };
+2 -2
View File
@@ -26,9 +26,9 @@ import {
withDraftShapes,
withDraftStations,
} from "./B06_Section_Section_Drafts";
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
import { applyStructureAreaRows, structureAreaRows } from "./logic/B06_Section_Structure_Layouts";
import { withDraftAdjusts, withPipeOptions } from "./B06_Section_Section_Adjusts";
import { crossPatchesFromCache } from "./B06_Section_Route_Carry_Patch";
import { crossPatchesFromCache } from "./logic/B06_Section_Route_Carry_Patch";
// [저장] patch 바탕 = 한 벌(서버 노선 바뀐 [저장] 넘김도 · 472-6 6-B)
export { crossPatchesFromCache };
+1 -1
View File
@@ -16,7 +16,7 @@
* 부재 두께는 세월교 승계(측벽 0.2·상판 0.3·저판 0.3)이며 백엔드가 실어 보낸다.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import { groundLine } from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
import type { BoxSet, CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import type {
BasinLayout,
+4 -6
View File
@@ -24,12 +24,10 @@ import type {
} from "./B06_Section_UI_Cross_Culvert_Geom";
import { appendPipeInfoText, outlineBox } from "./B06_Section_UI_Cross_Culvert_Types";
import { pipeLengthText } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation";
import {
appendChuteBand,
appendWallHatch,
isChuteSegment,
} from "./B06_Section_UI_Cross_Wall_Hatch";
import { appendWallTrench } from "./B06_Section_UI_Cross_Excavation";
import { foundationChoice } from "./logic/B06_Section_Cross_Excavation_Geom";
import { appendChuteBand, appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
import { isChuteSegment } from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
// 기하 계산 진입점과 공개 상수·타입은 여기서 재수출한다 — B05(최소 토피)와 횡단 뷰가
// 이 모듈 경로로 이미 참조하고 있어 분리 후에도 import 경로를 바꾸지 않는다.
@@ -32,7 +32,7 @@ import type {
} from "./B06_Section_UI_Cross_Culvert_Types";
import { structureCutPoints } from "./logic/B06_Section_UI_Cross_Structure_Cut";
import type { StructureCutSpec } from "./logic/B06_Section_UI_Cross_Structure_Cut";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
import { buildRevetWallGeometry } from "./logic/B06_Section_Cross_Wall_Geom";
export interface OutletExtrasInput {
/** 성토부선 시작점 = 관 유출 하단 꼭짓점(배관 벽의 성토선 시작 자리). */
@@ -6,7 +6,7 @@
* (designTrim). 유입 = 상단측. 집수정은 `_Basin.ts`, 성토부·다단은 `_Extra.ts`.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import { groundLine } from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
import {
revetWallSpec,
@@ -22,7 +22,7 @@ import {
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { outlineLeanOf } from "./logic/B06_Section_UI_Cross_Lean";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
import { buildRevetWallGeometry } from "./logic/B06_Section_Cross_Wall_Geom";
import {
bodyGuardAxes,
bodyGuardBase,
@@ -30,7 +30,7 @@ import {
bodyGuardHeight,
fordPipeFit,
mergeGuardTrim,
} from "./B06_Section_UI_Cross_Body_Guard";
} from "./logic/B06_Section_Cross_Body_Guard";
import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
import type {
BasinLayout,
@@ -4,7 +4,7 @@
* 분리(700줄 제한). 카드가 다시 그려질 때마다 한 번 불린다.
* ========================================================================== */
import { bodyAbsolute } from "./B06_Section_UI_Cross_Body_Guard";
import { bodyAbsolute } from "./logic/B06_Section_Cross_Body_Guard";
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import { computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert";
import type {
@@ -1,5 +1,9 @@
import type { PavementEntry } from "@util/common_util_structure_pavement";
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import {
pavementLayers,
rockBoundarySegments,
} from "./logic/B06_Section_Cross_Design_Surface_Geom";
const SVG_NS = "http://www.w3.org/2000/svg";
@@ -10,28 +14,17 @@ export function appendRockBoundaryOverlay(
x: (offset: number) => number,
y: (elevation: number) => number,
): void {
const segments: string[][] = [];
let current: string[] = [];
for (const sample of samples) {
const elevation = sample.elevation_m;
if (sample.valid === false || elevation === null || !Number.isFinite(elevation ?? NaN)) {
if (current.length > 1) segments.push(current);
current = [];
continue;
}
current.push(`${x(sample.offset_m ?? 0)},${y((elevation as number) + offsetM)}`);
}
if (current.length > 1) segments.push(current);
for (const points of segments) {
for (const segment of rockBoundarySegments(samples, offsetM)) {
const polyline = document.createElementNS(SVG_NS, "polyline");
polyline.setAttribute("points", points.join(" "));
polyline.setAttribute(
"points",
segment.map((p) => `${x(p.offset_m)},${y(p.elevation_m)}`).join(" "),
);
polyline.setAttribute("class", "b06-chart__rock-boundary");
svg.append(polyline);
}
}
type Edge = { offset_m: number; elevation_m: number };
/** 포장사양 → 무늬(88) — 콘크리트 빗금 · 아스콘 점 · 쇄석 돌. 그 밖 = 채움만. */
const PAVE_MARKS: Record<string, { size: number; marks: string }> = {
콘크리트: { size: 6, marks: '<path d="M0,6 L6,0" class="b06-chart__pave-mark"/>' },
@@ -49,21 +42,6 @@ const PAVE_MARKS: Record<string, { size: number; marks: string }> = {
/** 무늬 id 는 문서 안에서 유일 — 카드 · 다시 그리기마다 새 번호(물넘이 빗금과 같은 방식). */
let paveSeq = 0;
/** 확폭 끈 구간 — 규격 폭 끝(좌 = left − 확폭좌 · 우 = right + 확폭우 · 표고는 두 끝 사이 직선). */
function standardEdges(edges: { left: Edge; right: Edge }, design: CrossDesign) {
const { left, right } = edges;
const span = right.offset_m - left.offset_m || 1;
const at = (offset: number): Edge => ({
offset_m: offset,
elevation_m:
left.elevation_m + ((right.elevation_m - left.elevation_m) * (offset - left.offset_m)) / span,
});
return {
left: at(left.offset_m - (design.widening_left_m ?? 0)),
right: at(right.offset_m + (design.widening_right_m ?? 0)),
};
}
type PaveMark = { type_id: string; start_m?: number | null; end_m?: number | null };
/** 포장층 범위 표식(종류 · 시작 · 끝)이 그 노면공인가 — 겹친 노면공 가름. */
@@ -97,8 +75,8 @@ export function pavementAt<T extends PaveMark>(
}
/**
* 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`section.pavement`)면 그 두께 · 무늬 ·
* 확폭 켬/끔(여러 건은 아래로 차례로 쌓음) · 없으면 카드 포장 켬일 때 표준 두께 채움만.
* 포장층 — 층 꼭짓점 = `pavementLayers`(두께 · 확폭 켬/끔 · 차례로 쌓음) · 여기는 무늬 · 범위 표식 ·
* 그리기. 없으면 카드 포장 켬일 때 표준 두께 채움만.
*/
export function appendPavementOverlay(
svg: SVGElement,
@@ -107,20 +85,8 @@ export function appendPavementOverlay(
y: (elevation: number) => number,
pavement?: readonly PavementEntry[] | null,
): void {
const edges = design.carriageway_edges ?? design.road_edges;
if (!edges) return;
const layers = pavement?.length ? pavement : design.paved ? [null] : [];
let depth = 0;
for (const entry of layers) {
const thickness = entry?.thickness_m ?? design.pavement_thickness_m ?? 0.2;
const { left, right } = entry && !entry.widening_on ? standardEdges(edges, design) : edges;
const points = [
`${x(left.offset_m)},${y(left.elevation_m - depth)}`,
`${x(right.offset_m)},${y(right.elevation_m - depth)}`,
`${x(right.offset_m)},${y(right.elevation_m - depth - thickness)}`,
`${x(left.offset_m)},${y(left.elevation_m - depth - thickness)}`,
].join(" ");
depth += thickness;
for (const { entry, corners } of pavementLayers(design, pavement)) {
const points = corners.map((p) => `${x(p.offset_m)},${y(p.elevation_m)}`).join(" ");
const polygon = document.createElementNS(SVG_NS, "polygon");
polygon.setAttribute("points", points);
polygon.setAttribute("class", "b06-chart__pavement");
+9 -31
View File
@@ -4,6 +4,7 @@
*
* 왜 있나 — 「터파기가 현재 횡단도에서 표현이 안 된다」가 사용자 지적이었다. 수량을
* 세기 전에 **도면에 그것이 보여야** 한다.
* 자리 · 폭 · 깊이 = `logic/B06_Section_Cross_Excavation_Geom`(472-3).
*
* ⚠ 근거의 급이 다르므로 **노티스를 갈라 적는다** — 값은 `common_util_excavation` 한 곳에서
* 온다(파이썬 짝과 거울 시험으로 묶여 있다). 여기서는 **그리기만** 한다.
@@ -12,26 +13,12 @@
* ⚠ **비탈을 주지 않는다 — 수직으로 그린다.** 정본 식에 기울기 항이 없다.
* ========================================================================== */
import {
BASIS_WALL,
WALL_BLINDING_DEPTH_M,
WALL_BLINDING_WIDTH_M,
WALL_FOUNDATION_DEPTH_M,
WALL_FOUNDATION_WIDTH_M,
WALL_TRENCH_CLEARANCE_M,
} from "@util/common_util_excavation";
import { BASIS_WALL } from "@util/common_util_excavation";
import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import { wallTrench } from "./logic/B06_Section_Cross_Excavation_Geom";
const SVG_NS = "http://www.w3.org/2000/svg";
/** 「기초유」인가 — 저장 제원 `foundation` 의 값. 비어 있으면 **그리지 않는다**(근거 없음). */
export function foundationChoice(value: unknown): "기초유" | "기초버림" | null {
const text = typeof value === "string" ? value.trim() : "";
if (text === "기초유") return "기초유";
if (text === "기초버림") return "기초버림";
return null;
}
function trenchPath(layer: SVGElement, corners: Array<[number, number]>, tooltip: string): void {
const line = document.createElementNS(SVG_NS, "polyline");
line.setAttribute("points", corners.map(([px, py]) => `${px},${py}`).join(" "));
@@ -43,7 +30,7 @@ function trenchPath(layer: SVGElement, corners: Array<[number, number]>, tooltip
}
/**
* 벽 터파기 — 벽 바닥선 아래로 **수직**. 폭은 바닥 폭 + 여유 0.2, 깊이는 기초 몫.
* 벽 터파기 — 벽 바닥선 아래로 **수직**(자리 = `wallTrench`).
* `foundation` 이 비어 있으면 **그리지 않는다** — 사용자가 고르기 전에는 근거가 없다.
*/
export function appendWallTrench(
@@ -53,18 +40,9 @@ export function appendWallTrench(
x: (offset: number) => number,
toDisplayY: (elevation: number) => number,
): { widthM: number; depthM: number } | null {
if (foundation === null) return null;
const hasFoundation = foundation === "기초유";
const depth = hasFoundation ? WALL_FOUNDATION_DEPTH_M : WALL_BLINDING_DEPTH_M;
// 폭: 기초 몫은 정본이 정한 값(0.9 / 0.7), 다만 벽 바닥이 그보다 넓으면 그 폭 + 여유.
const baseWidth = Math.abs(wall.bottomFront.offset - wall.bottomBack.offset);
const minWidth = hasFoundation ? WALL_FOUNDATION_WIDTH_M : WALL_BLINDING_WIDTH_M;
const width = Math.max(minWidth, baseWidth + (hasFoundation ? WALL_TRENCH_CLEARANCE_M : 0));
const center = (wall.bottomFront.offset + wall.bottomBack.offset) / 2;
const top = Math.max(wall.bottomFront.elevation, wall.bottomBack.elevation);
const bottom = top - depth;
const left = center - width / 2;
const right = center + width / 2;
const trench = wallTrench(wall, foundation);
if (!trench) return null;
const { left, right, top, bottom, widthM, depthM } = trench;
trenchPath(
layer,
[
@@ -73,8 +51,8 @@ export function appendWallTrench(
[x(right), toDisplayY(bottom)],
[x(right), toDisplayY(top)],
],
`${foundation} 터파기 폭 ${width.toFixed(2)}m · 깊이 ${depth.toFixed(2)}m (수직)\n` +
`${foundation} 터파기 폭 ${widthM.toFixed(2)}m · 깊이 ${depthM.toFixed(2)}m (수직)\n` +
`품셈에 규정이 없어 실무 관행으로 정한 값 — ${BASIS_WALL}`,
);
return { widthM: width, depthM: depth };
return { widthM, depthM };
}
@@ -19,7 +19,7 @@
* 성토측이라도 기슭막이 세트(다단·재질)와는 연계하지 않는다.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import { groundLine } from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
import type { CrossSection, FordSet, SectionSample } from "./B06_Section_Api_Fetch";
import { GUARD_CUT_BERM_M, pipeWallThicknessM } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type {
@@ -15,7 +15,8 @@ import type { CrossDesign } from "./B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import { appendPavementOverlay } from "./B06_Section_UI_Cross_Design_Surface";
import { pipeTagId, tagShape } from "../common_util/common_util_pick_tag";
import { appendChuteBand, chuteRun } from "./B06_Section_UI_Cross_Wall_Hatch";
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
import { chuteRun } from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
export interface FordPavementSpec {
+6 -328
View File
@@ -1,343 +1,21 @@
/* =============================================================================
* B06_Section_UI_Cross_Revetment.ts
* 독립 기슭막이(배관과 무관) — 배관 세트 기슭막이와 **같은 배치·같은 산식·같은 도형**.
* 배관만 없을 뿐, 벽 자리·이동·한계는 배관 유출 벽과 한 몸으로 푼다(2026-08-28 사용자:
* "배관용에서 배관만 빼면 대부분 동일해야").
*
* · 자리(2026-08-29 사용자 개정): 조작은 **순수 좌우(x)·순수 상하(d)** 0.1m다.
* x = 자동 자리에서의 수평 이동, d = 노견에서 잰 수직 하강. 성토선 각도는 유지하고
* **노견 확장 구간**이 길이를 흡수한다. 한계는 노견 안쪽 금지·확장 0(각도 유지)·매몰 금지.
* 배관 기준벽(`placePipeWall`)의 이동은 손대지 않는다.
* · 실제 적용값(한계 절삭 후)은 `appliedAdjust`로 돌려주고, 화면이 조작 저장소에 되받는다
* — 눌러도 안 움직이는데 숫자만 커지는 것 방지(배관과 같은 syncApplied 규칙).
* · 다단: 배관과 **같은 함수** `buildExtrasAt`. 그리기(폴리곤·이음선·돌·성토부선)는 공용
* `_Cross_Wall`. 노견→벽 성토선은 designTrim의 slope로 그린다.
*
* 3D 코리도는 `tiers[].polygon`·`span`을 그대로 스윕한다.
* 독립 기슭막이 **그리기** — 자리 · 단 · 트림(기하)은 `logic/B06_Section_Cross_Revetment_Geom`(472-3).
* 그리기(폴리곤·이음선·돌·성토부선)는 배관과 같은 공용 `_Cross_Wall`.
* ========================================================================== */
import { tagShape } from "../common_util/common_util_pick_tag";
import type { CrossSection } from "./B06_Section_Api_Fetch";
import {
FILL_SLOPE_RATIO_MIN,
PIPE_WALL_DEFAULT_RUN_M,
REVET_EMBED_DEPTH_M,
REVET_FILL_TOP_M,
WALL_MIN_PURE_HEIGHT_M,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { outlineLeanOf } from "./logic/B06_Section_UI_Cross_Lean";
import { catalogRow } from "../common_util/common_util_structure_catalog";
import type { RevetMaterial } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { fillWallBaseWidth, groundInterpolator } from "./logic/B06_Section_UI_Cross_Culvert_Solve";
import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
import {
ZERO_ADJUST,
type CulvertDesignTrim,
type OutletFillSegment,
type WallAdjust,
type WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation";
import { appendPlanLine, buildRevetWallGeometry, drawRevetWall } from "./B06_Section_UI_Cross_Wall";
import type { RevetmentLayout } from "./logic/B06_Section_Cross_Revetment_Geom";
import { foundationChoice } from "./logic/B06_Section_Cross_Excavation_Geom";
import { appendWallTrench } from "./B06_Section_UI_Cross_Excavation";
import { appendPlanLine, drawRevetWall } from "./B06_Section_UI_Cross_Wall";
const SVG_NS = "http://www.w3.org/2000/svg";
/** 서버가 얹어 주는 독립 기슭막이 제원(`section.revetment`, 구조물 정본 D군). */
export interface RevetmentSpec {
structure_id: string | null;
type_id: string;
name: string;
start_m: number;
end_m: number;
anchor_m: number;
form?: string | null;
height_m?: number | null;
/** 사용자가 고른 설치 측 — "좌" | "우". */
side?: string | null;
/** 기초 축 — "기초유" | "기초버림". **비어 있으면 터파기를 안 그린다**(근거 없음). */
foundation?: string | null;
/** 단 수(다단). 1이면 단일 벽. */
tiers?: number | null;
/** 추가 단(2단부터) 형태 · 높이 — 좌측 폼 「추가 단」 상자(178-3). 빈 값 = 본 벽 형태 · 기본 높이. */
extras?: ReadonlyArray<{ form: string | null; height_m: number | null }> | null;
/** (구 모델) 기준 올림·좌우 이동 — 배관식 전환 뒤 자리는 조정창 4축(x·d)이 정한다. */
lift_m?: number | null;
shift_m?: number | null;
/** 전면 기울기를 B08 과 같은 칸으로 가르는 값 — 성토/절토 · 전면 기울기(2026-09-14 B5). */
face_role?: string | null;
face_slope_ratio?: number | string | null;
}
/** 조정창 조작값 중 이 벽이 쓰는 축 — 배관 벽과 같은 형태(x 좌우·d 사면·h 높이). */
export interface RevetmentAdjust {
x?: number;
d?: number | null;
h?: number | null;
}
export interface RevetPoint {
offset: number;
elevation: number;
}
/** 한 단(段) — 3D 스윕용 폴리곤(배관 벽과 같은 합성 단면). */
export interface RevetmentTier {
polygon: RevetPoint[];
}
export interface RevetmentLayout {
side: "left" | "right";
/** 1단 벽 높이(m) — **바닥~상단**. 조정창 높이 표시·조작의 기준값. */
heightM: number;
/** 이 단면의 누가거리(m) — 부족 안내를 측점 단위로 센다(재렌더 중복 방지). */
chainageM: number;
/** 벽 구조물 id — 그린 벽 신원표(323-5). */
structureId: string | null;
/** 종류 이름(옹벽 · 흙막이 · 돌쌓기 …) — 툴팁 글(323-7 · 「독립 기슭막이」로 뜨던 것). */
name: string;
/** 사용자가 요청한 단 수 — 실제로 선 단 수(`walls.length`)와 다르면 자리가 부족한 것이다. */
requestedTiers: number;
/** 1단 벽 이음선 상단점(성토 설계선 트림 경계). */
top: RevetPoint;
/** 벽 목록(1단 + 다단) — 배관 벽과 같은 `WallLayout`, 그리기가 그대로 쓴다. */
walls: WallLayout[];
/** 기초 축 — 저장 칸과 **같은 글자**(`foundation`). 터파기 그림이 이 값으로 갈린다. */
foundation: string | null;
/** 벽 사이·벽 아래 성토부선(배관 다단과 같은 체계). */
fillSegments: OutletFillSegment[];
/** 성토 설계선을 벽 상단에서 끊고 노견→벽 성토선을 대신 그리는 트림. */
designTrim: CulvertDesignTrim;
/** 한계 절삭 후 **실제 적용된** 1단 조작값 — 화면이 조작 저장소에 되받는다. */
appliedAdjust: WallAdjust;
/** 3D 스윕용 단 목록 — 벽 폴리곤 그대로. */
tiers: RevetmentTier[];
/** 기준 측점 전/후 점유 길이(m) — 3D 스윕 범위. */
span: { beforeM: number; afterM: number };
/** 기준 측점이 아닌 측점(구간 연동) — 주 형상과 색을 갈라 그림(79-6). */
linked: boolean;
}
/** 형태(구조물 옵션) → 재질(높이 한계·라벨). 메 계열은 dry, 찰=wet, 콘크리트=concrete. */
function materialOfForm(form?: string | null): RevetMaterial {
if (!form) return "dry";
if (form.includes("찰")) return "wet";
if (form.includes("콘크리트")) return "concrete";
return "dry"; // 메쌓기·돌망태·통나무·바자 — 메 계열(2.0m 한계)
}
/** 유효 지반 샘플의 바깥 한계 offset(설치 측). 없으면 null. */
function sampleLimit(section: CrossSection, outward: number): number | null {
const offsets = section.samples
.filter((s) => s.valid !== false && typeof s.offset_m === "number")
.map((s) => s.offset_m as number);
if (!offsets.length) return null;
return outward > 0 ? Math.max(...offsets) : Math.min(...offsets);
}
/**
* 노면 횡단물매를 **바깥 방향 1m당 표고 변화**로 돌려준다(하향이면 음수).
* 노면은 측구 쪽으로 내려가는 단일 평면이라(엔진 `road_z`), 노견을 밖으로 연장하면
* 그 평면을 그대로 타고 가야 각도가 맞는다(2026-08-29 사용자).
* 1순위는 **실제 그려진 노견 구간**(차도 끝 → 노면 끝)의 기울기 — 부호를 추측하지
* 않는다. 노견 폭이 0이면 `cross_slope_pct`를 측구 방향 부호로 쓴다.
*/
function roadSlopePerOutward(
design: NonNullable<CrossSection["design"]>,
side: "left" | "right",
outward: number,
): number {
const edge = design.road_edges?.[side];
const inner = design.carriageway_edges?.[side];
if (edge && inner) {
const run = (edge.offset_m - inner.offset_m) * outward;
if (run > 1e-6) return (edge.elevation_m - inner.elevation_m) / run;
}
const pct = Number(design.cross_slope_pct);
if (!Number.isFinite(pct)) return 0;
return (pct / 100) * (design.ditch_side === side ? -1 : 1);
}
/**
* 독립 기슭막이 단면 — 제원·높이·설계선이 없거나 자리를 못 찾으면 null.
* 1단 벽 = 배관 유출 벽과 같은 `placePipeWall`+`buildRevetWallGeometry`,
* 다단 = 배관과 같은 `buildExtrasAt`.
*/
export function computeRevetmentLayout(
section: CrossSection,
adjust?: RevetmentAdjust | null,
): RevetmentLayout | null {
const spec = section.revetment;
const design = section.design;
if (!spec || !design) return null;
const rawHeight = adjust?.h ?? spec.height_m;
// 빈 칸만 안 그림 — 0 이하는 아래 하한(0.1)에서 멈춤(54-51).
if (rawHeight == null || !Number.isFinite(Number(rawHeight))) return null;
const requestedHeight = Number(rawHeight);
// 설치 측이 비어 있으면 **성토가 나는 쪽**에 세운다 — 좌측 「구조물 배치」의 C군 벽
// (옹벽·돌쌓기 등)에는 설치 측 칸이 없기 때문이다(2026-09-06). 양측 절토면 벽이 설
// 자리가 없으므로 그리지 않는다(면적도 그대로).
const fillSide = (): "left" | "right" | null => {
if (design.section_mode === "left_cut") return "right";
if (design.section_mode === "right_cut") return "left";
if (design.section_mode === "both_fill") return "left";
return null;
};
const side = spec.side === "우" ? "right" : spec.side === "좌" ? "left" : fillSide();
if (!side) return null;
const outward = side === "left" ? 1 : -1;
const edge = design.road_edges?.[side];
const groundAt = groundInterpolator(section.samples);
const limit = sampleLimit(section, outward);
if (!edge || !groundAt || limit === null) return null;
// 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준).
// 형태별 높이 한계 없음 — 사용자 입력 그대로(하한만 둠).
const pureHeight = Math.max(requestedHeight, WALL_MIN_PURE_HEIGHT_M);
/** 기준선(근입 위)~상단 — 도형 계산은 종전대로 이 값으로 한다. */
const height = pureHeight - REVET_EMBED_DEPTH_M;
// 전면 기울기 — 관 흙막이와 같은 `outlineLeanOf`(형태 무관 · 형태는 안 무늬만). 단면유형 ·
// 설치 측 · 높이 · 사용자 칸은 그대로 탐. 수량(B08)은 자기 칸으로 따로 셈.
const lean = outlineLeanOf({
height_m: requestedHeight,
section_mode: design.section_mode,
side: spec.side,
face_role: spec.face_role,
face_slope_ratio: spec.face_slope_ratio,
});
const roadSlope = roadSlopePerOutward(design, side, outward);
const autoOffset =
edge.offset_m + outward * (fillWallBaseWidth(height, lean) / 2 - REVET_FILL_TOP_M);
// x 원점 = 성토사면(1:1.2) 경사길이 4m 지점 — 저장된 x 의 절대 자리 그대로(79b).
const autoRun = PIPE_WALL_DEFAULT_RUN_M;
/**
* 조작 축(2026-08-29 사용자): x = **순수 좌우(m)**, d = **노견에서 잰 순수 수직
* 하강(m)**. 성토선 각도는 그대로 두고 **노견 확장 구간**이 길이를 흡수한다
* (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다).
*/
// 손대지 않은 벽(x · d 빈 칸) = 성토사면 제일 위(노견 · 79-5) · 손으로 넣은 벽은 종전 기본(79b).
const untouched = adjust?.d == null && !adjust?.x;
const defaultDrop = untouched ? 0 : autoRun / FILL_SLOPE_RATIO_MIN;
/** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리 — 음수 = 원점보다
* 사면 위(노견 쪽)라 제일 위 자리는 x = −autoRun 이다. */
const xFloor = (drop: number): number => FILL_SLOPE_RATIO_MIN * drop - autoRun;
const requestedX = untouched ? Number.NEGATIVE_INFINITY : (adjust?.x ?? 0);
// 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). 노면 위 금지 = d ≥ 0.
const appliedDrop = Math.round(Math.max(adjust?.d ?? defaultDrop, 0) * 10) / 10;
// 0.1 눈금 반올림(종전과 같음) · 반올림이 하한 아래로 내리면 하한(제일 위 자리가 노견 안쪽으로 안 감).
const floor = xFloor(appliedDrop);
const appliedX = Math.max(Math.round(Math.max(requestedX, floor) * 10) / 10, floor);
const anchorOffset = autoOffset + outward * (autoRun + appliedX);
const baseElevation = edge.elevation_m - appliedDrop - height;
const floatGapM = Math.max(0, baseElevation - groundAt(anchorOffset));
const material = materialOfForm(spec.form);
const lengthM = Number.isFinite(spec.end_m - spec.start_m) ? spec.end_m - spec.start_m : null;
const tier1 = buildRevetWallGeometry({
anchor: { offset: anchorOffset, elevation: baseElevation },
outward,
height,
material,
role: "outlet",
form: spec.form,
lengthM,
floatGapM,
lean,
roadElevation: edge.elevation_m,
});
// 넣은 좌우 이동이 **각도 하한**에 눌려 통째로 무시됐으면 그 하한을 남긴다(2026-09-07).
// 실측 — d 2.6m 자리에서 하한이 1.0m 를 넘어 「1.0m 를 넣어도 0.00m 이동」이 났음.
if (requestedX > 0.005 && floor > requestedX + 0.005) {
tier1.shiftFloorM = Math.round(floor * 100) / 100;
}
// 다단 — 배관과 같은 함수. 1단 벽 전면 하단 꼭짓점을 성토부선 시작점으로.
const requestedTiers = Math.max(1, Math.round(Number(spec.tiers) || 1));
const extras =
requestedTiers > 1
? buildExtrasAt(tier1.bottomFront, {
startBottomElevation: tier1.bottomBack.elevation,
outward,
groundAt,
limitOffset: limit,
// 단마다 상자 형태 · 높이(178-3) — 자리(x · d)는 다단 자동 규칙 그대로
adjusts: Array.from({ length: requestedTiers - 1 }, (_, i) => ({
...ZERO_ADJUST,
m: spec.extras?.[i]?.form ?? null,
h: spec.extras?.[i]?.height_m ?? null,
})),
// 단들도 1단과 같은 형태로 선다 — 안 넘기면 형태가 비어 늘 메쌓기로
// 그려졌다(2026-08-30 사용자: 형태별 모양이 누락되는 경우).
ownerForm: spec.form,
equalize: false,
leanFor: (form, pureHeightM) =>
outlineLeanOf({
form,
height_m: pureHeightM,
section_mode: design.section_mode,
side: spec.side,
}),
})
: { walls: [], segments: [], appliedAdjusts: [], addable: false };
const walls: WallLayout[] = [tier1, ...extras.walls];
// 물빼기 파이프 단면 = 표 형상칸.weep(322-1 · 옹벽)
if (catalogRow(spec.type_id)?.shapeOpts.weep) for (const wall of walls) wall.retaining = true;
const jt = tier1.fillTop;
// 노견 → (물매를 탄 노견 연장) → 벽 이음선 상단점. 연장 구간은 노면 물매 그대로 가고,
// 꺾임점에서 성토 물매 1:1.2로 벽 상단에 닿는다(2026-08-29 사용자 확정).
// D = 노견에서 벽 상단까지 바깥 방향 거리, R0 = 노견과 벽 상단의 낙차
// (D − t) = 1.2 × (R0 + roadSlope × t) → t = (D − 1.2·R0) / (1 + 1.2·roadSlope)
const edgePoint = { offset: edge.offset_m, elevation: edge.elevation_m };
const D = (jt.offset - edge.offset_m) * outward;
const R0 = edge.elevation_m - jt.elevation;
const denominator = 1 + FILL_SLOPE_RATIO_MIN * roadSlope;
const breakRun =
R0 > 1e-6 && D > 1e-6 && Math.abs(denominator) > 1e-9
? Math.min(D, Math.max(0, (D - FILL_SLOPE_RATIO_MIN * R0) / denominator))
: 0;
const slope = {
points:
breakRun > 1e-6
? [
edgePoint,
{
offset: edge.offset_m + outward * breakRun,
elevation: edge.elevation_m + roadSlope * breakRun,
},
jt,
]
: [edgePoint, jt],
};
const designTrim: CulvertDesignTrim =
outward > 0
? { minOffset: -Infinity, maxOffset: jt.offset, maxElevation: jt.elevation, maxSlope: slope }
: { minOffset: jt.offset, maxOffset: Infinity, minElevation: jt.elevation, minSlope: slope };
return {
side,
heightM: pureHeight,
chainageM: Number(section.chainage_m) || 0,
structureId: spec.structure_id ?? null,
name: spec.name || "벽",
requestedTiers,
top: jt,
walls,
foundation: typeof spec.foundation === "string" ? spec.foundation.trim() || null : null,
fillSegments: extras.segments,
designTrim,
appliedAdjust: { x: appliedX, d: appliedDrop, h: adjust?.h ?? null, m: null },
tiers: walls.map((wall) => ({ polygon: wall.points })),
span: {
beforeM: Math.max(spec.anchor_m - spec.start_m, 0),
afterM: Math.max(spec.end_m - spec.anchor_m, 0),
},
linked: Math.abs((Number(section.chainage_m) || 0) - spec.anchor_m) > 0.02,
};
}
/** 횡단 카드에 독립 기슭막이(1단+다단)를 그린다 — 배관 벽과 같은 도형. 그렸으면 setter. */
export function appendRevetmentOverlay(
svg: SVGElement,
@@ -14,7 +14,8 @@
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import { cell, courses, type BandFrame } from "./B06_Section_UI_Cross_Wall_Hatch";
import { cell, type BandFrame } from "./B06_Section_UI_Cross_Wall_Hatch";
import { courses } from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
const SVG_NS = "http://www.w3.org/2000/svg";
+5 -3
View File
@@ -7,7 +7,7 @@
* 횡단은 종/횡 공통 Y스케일을 쓰지 않고 카드마다 자체 1:1 ppm으로 스케일한다.
* ========================================================================== */
import { withoutGuardedFill } from "./B06_Section_UI_Cross_Body_Guard";
import { withoutGuardedFill } from "./logic/B06_Section_Cross_Body_Guard";
import { markLinkedFordPavement } from "./B06_Section_UI_Cross_Ford_Linked";
import type { CrossSection } from "./B06_Section_Api_Fetch";
import {
@@ -24,8 +24,10 @@ import { attachShapePick, shapePickRoute, splitPipePick } from "./B06_Section_UI
import { stackPickTargets } from "./B06_Section_UI_Cross_Pick_Order";
import { appendUnderdrainOverlay } from "./B06_Section_UI_Cross_Underdrain";
import { raiseWalls } from "./B06_Section_UI_Cross_Wall";
import { attachChuteBandPick, groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import { appendRevetmentOverlay, computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment";
import { attachChuteBandPick } from "./B06_Section_UI_Cross_Wall_Hatch";
import { groundLine } from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
import { appendRevetmentOverlay } from "./B06_Section_UI_Cross_Revetment";
import { computeRevetmentLayout } from "./logic/B06_Section_Cross_Revetment_Geom";
import {
appendCrossDesignOverlay,
appendPavementOverlay,
+2 -84
View File
@@ -1,7 +1,7 @@
/* =============================================================================
* B06_Section_UI_Cross_Wall.ts
* 기슭막이 벽 **공용 기하 + 그리기** — 배관 세트 벽(`_Cross_Culvert_Geom`/`_Culvert`)과
* 독립 기슭막이(`_Cross_Revetment`)가 같은 산식·같은 도형을 쓰도록 뽑아낸 층이다
* 기슭막이 벽 **그리기**(기하 = `logic/B06_Section_Cross_Wall_Geom` · 472-3) — 배관 세트 벽
* (`_Cross_Culvert_Geom`/`_Culvert`)과 독립 기슭막이(`_Cross_Revetment`)가 같은 산식·같은 도형을 쓰도록 뽑아낸 층이다
* (2026-08-28 사용자: "배관용에서 배관만 빼면 대부분 동일해야"). 벽 1매의 합성 단면
* (하부 사다리꼴 + 상부 평행사변형, 배면 수직·전면 1:0.3)과 그 돌쌓기 표현은 여기 하나뿐.
*
@@ -9,15 +9,6 @@
* 형태가 바꾸는 것은 안 무늬(`_Cross_Wall_Hatch`)뿐 — 외곽은 높이·자리·기울기로만 정함.
* ========================================================================== */
import {
materialLabel,
REVET_EMBED_DEPTH_M,
REVET_LEAN_RATIO,
REVET_THICKNESS_M,
WALL_FILL_TOP_INSET_M,
WALL_MIN_PURE_HEIGHT_M,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { OffsetPoint, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import { appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
@@ -39,79 +30,6 @@ function polygon(
return shape;
}
/** 벽 1매의 기하 입력 — 자리 기준은 **하단선 중점**(anchor.offset)과 그 자리 기준표고
* (anchor.elevation = 관 invert 또는 지반). 벽은 그 위로 `height`만큼 선다. */
export interface RevetWallGeometryInput {
/** 하단선 중점 offset + 기준표고(벽이 이 위로 선다). */
anchor: OffsetPoint;
/** +1 = 화면 좌측(계류측), −1 = 우측. 전면 1:0.3이 이 부호로 기운다. */
outward: number;
/** 계산용 높이(기준표고~상단, 근입 0.5 제외). */
height: number;
material: RevetMaterial;
role: "inlet" | "outlet" | "extra";
/** 표기용 형태 — 미지정이면 재질 라벨(메/찰/콘크리트). */
form?: string | null;
lengthM?: number | null;
thickness?: number;
floatGapM?: number;
extraIndex?: number;
/** 전면 기울기 1:n 의 n — 표준경사(`leanOf`). 없으면 종전 0.3. */
lean?: number;
/** 노면(계획고) — 벽 윗면이 이보다 높으면 여기서 멈춤. 높이 한도는 이것 하나. */
roadElevation?: number | null;
}
/**
* 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점을 만든다. 배관 벽(`buildWall`,
* `_Cross_Culvert_Geom` 335~376행)과 **같은 산식** — 배면 수직, 상단 변 = 사다리꼴
* 상단(t/2)+띠(t), 전면 1:0.3, 하단선 = 기준표고 −0.5m 수평 기초.
*/
export function buildRevetWallGeometry(input: RevetWallGeometryInput): WallLayout {
const { anchor, outward, material, role } = input;
const road = input.roadElevation;
const height =
road != null && anchor.elevation + input.height > road + 1e-9
? Math.max(road - anchor.elevation, WALL_MIN_PURE_HEIGHT_M - REVET_EMBED_DEPTH_M)
: input.height;
const thickness = input.thickness ?? REVET_THICKNESS_M;
const lean = input.lean ?? REVET_LEAN_RATIO;
const baseWidth = thickness * 1.5 + lean * height;
const backOffset = anchor.offset - outward * (baseWidth / 2);
const topJoint = backOffset + outward * (thickness / 2);
const topElevation = anchor.elevation + height;
const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation };
const topFront = topJoint + outward * thickness;
const frontXAt = (elevation: number): number =>
topFront + outward * lean * (topElevation - elevation);
const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation };
const bottomFront: OffsetPoint = {
offset: frontXAt(bottomElevation),
elevation: bottomElevation,
};
return {
role,
extraIndex: input.extraIndex,
form: input.form ?? materialLabel(material),
lengthM: input.lengthM ?? null,
backOffset,
outerOffset: bottomFront.offset,
base: anchor.elevation,
height,
floatGapM: input.floatGapM ?? 0,
material,
lean,
outward,
topBack,
topJoint: { offset: topJoint, elevation: topElevation },
fillTop: { offset: topFront - outward * WALL_FILL_TOP_INSET_M, elevation: topElevation },
bottomBack,
bottomFront,
points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront],
};
}
/**
* 벽 1매를 SVG로 그린다 — 폴리곤 + 대각 이음선 + 계류측 기운 띠의 돌쌓기 해칭.
* 배관 오버레이(`_Cross_Culvert` 161~246행)와 **같은 그리기**. 선택·강조 배선은 호출자
+15 -195
View File
@@ -14,10 +14,20 @@
* · 돌망태 : 2열 격자 칸(철망 상자) — 칸마다 각진 사각형.
* · 통나무·목재틀 : 원(통나무 마구리)을 한 줄로 쌓는다.
* · 바자 : 세로 말뚝 3줄 + 가로 엮음 눈금.
*
* 띠 · 물빼기 · 도수로 띠 기하(m 좌표)는 `logic/B06_Section_Cross_Wall_Hatch_Geom`(472-3).
* ========================================================================== */
import { REVET_THICKNESS_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { OffsetPoint, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import {
chuteBandOutline,
chuteRun,
courses,
hatchOfForm,
wallDrainRuns,
wallHatchBand,
} from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
const SVG_NS = "http://www.w3.org/2000/svg";
const HATCH_CLASS = "b06-chart__culvert-stone";
@@ -30,19 +40,6 @@ const HATCH_CLASS = "b06-chart__culvert-stone";
*/
let clipSeq = 0;
/** 형태 문자열 → 표현 갈래. 판정 순서는 좁은 말(돌망태·바자)부터. */
export type WallHatch = "dry" | "wet" | "concrete" | "gabion" | "log" | "wattle";
export function hatchOfForm(form: string | null | undefined): WallHatch {
if (!form) return "dry";
if (form.includes("돌망태")) return "gabion";
if (form.includes("통나무") || form.includes("목재")) return "log";
if (form.includes("바자")) return "wattle";
if (form.includes("콘크리트")) return "concrete";
if (form.includes("찰")) return "wet";
return "dry";
}
/** 띠 안의 국소 좌표 → 화면 px. u = 바닥(0)~상단(1), v = 배면(-0.5)~전면(+0.5). */
export interface BandFrame {
point: (u: number, v: number) => [number, number];
@@ -86,11 +83,6 @@ export function cell(
return rect;
}
/** 켜 수 — 켜 높이가 목표치에 가장 가깝게, 최소 2켜. */
export function courses(spanM: number, targetM: number): number {
return Math.max(2, Math.round(spanM / targetM));
}
/** 돌 한 줄 — `thicknessM` = 띠 두께(벽 = 흙막이 띠 · 도수로/돌붙임 = 그 두께 · 90-2). */
export function drawStones(
group: SVGElement,
@@ -158,70 +150,23 @@ function drawWattle(group: SVGElement, frame: BandFrame): void {
}
}
/** 물빼기 파이프 단면(73-2) — 보기용 고정값 · 지식DB 관경 · 간격과 별개. */
const DRAIN_BAND_M = 1.5;
const DRAIN_DIAMETER_M = 0.15;
/** 앞쪽이 낮게 — 벽 두께 1m 당 낙차(보기용). */
const DRAIN_FALL = 0.1;
/** 물빼기 파이프 단면(73-2) — 보기용 · 자리는 `wallDrainRuns`. */
const DRAIN_CLASS = "b06-chart__wall-drain";
/** 직선 a→b 가 볼록 벽 단면 안을 지나는 구간 — clip 이 없는 곳(B07 오프스크린 수확)도 벽 밖으로 안 샘. */
function insideRun(a: OffsetPoint, b: OffsetPoint, polygon: OffsetPoint[]): OffsetPoint[] | null {
const dx = b.offset - a.offset;
const dy = b.elevation - a.elevation;
const hits: number[] = [];
polygon.forEach((p, i) => {
const q = polygon[(i + 1) % polygon.length];
const ex = q.offset - p.offset;
const ey = q.elevation - p.elevation;
const denominator = dx * ey - dy * ex;
if (Math.abs(denominator) < 1e-12) return;
const t = ((p.offset - a.offset) * ey - (p.elevation - a.elevation) * ex) / denominator;
const u = ((p.offset - a.offset) * dy - (p.elevation - a.elevation) * dx) / denominator;
if (u >= -1e-9 && u <= 1 + 1e-9) hits.push(t);
});
if (hits.length < 2) return null;
const at = (t: number): OffsetPoint => ({
offset: a.offset + dx * t,
elevation: a.elevation + dy * t,
});
return [at(Math.min(...hits)), at(Math.max(...hits))];
}
/**
* 옹벽 물빼기 — 높이(바닥~상단)를 반올림(높이 / 1.5)개(최소 1) 띠로 고르게 나눠 띠마다
* 가운데 하나. 배면 → 전면으로 꿰는 두 줄 평행선(앞쪽 조금 낮게) · 벽 두께만큼.
*/
/** 옹벽 물빼기 — 물빼기마다 두 줄 평행선(벽 안 구간 · `wallDrainRuns`). */
function drawDrains(
group: SVGElement,
wall: WallLayout,
x: (offset: number) => number,
y: (elevation: number) => number,
): void {
const bottom = wall.bottomBack.elevation;
const height = wall.topBack.elevation - bottom;
const count = Math.max(1, Math.round(height / DRAIN_BAND_M));
const back = wall.backOffset;
const [, , topFront, bottomFront] = wall.points;
for (let i = 0; i < count; i += 1) {
const center = bottom + (height * (i + 0.5)) / count;
// 그 높이의 전면 — 전면은 기울어 위로 갈수록 배면에 가까움.
const front =
bottomFront.offset + (topFront.offset - bottomFront.offset) * ((center - bottom) / height);
const fall = (DRAIN_FALL * Math.abs(front - back)) / 2;
for (const runs of wallDrainRuns(wall)) {
const pipe = document.createElementNS(SVG_NS, "g");
pipe.setAttribute("class", DRAIN_CLASS);
const title = document.createElementNS(SVG_NS, "title");
title.textContent = "물빼기(보기용)";
pipe.append(title);
for (const side of [-0.5, 0.5]) {
const level = center + side * DRAIN_DIAMETER_M;
const run = insideRun(
{ offset: back, elevation: level + fall },
{ offset: front, elevation: level - fall },
wall.points,
);
if (!run) continue;
for (const run of runs) {
const el = line(
[x(run[0].offset), y(run[0].elevation)],
[x(run[1].offset), y(run[1].elevation)],
@@ -244,15 +189,6 @@ export function appendWallHatch(
toDisplayY: (elevation: number) => number,
keyId: string,
): void {
// 하단선이 원지반을 따라 기울어 있으므로 띠 바닥도 하단선 위에서 잡는다.
const bottomSpan = wall.bottomFront.offset - wall.bottomBack.offset;
const bottomAtOffset = (offset: number): number =>
Math.abs(bottomSpan) > 1e-9
? wall.bottomBack.elevation +
(wall.bottomFront.elevation - wall.bottomBack.elevation) *
((offset - wall.bottomBack.offset) / bottomSpan)
: wall.bottomBack.elevation;
// 벽을 가로지르던 대각 이음선은 **긋지 않는다**(2026-09-12 사용자: 필요 없음).
// 종전에는 상단 변 중간점에서 바닥까지 형태와 무관하게 늘 그어, 돌쌓기·콘크리트 해칭
// 위로 선이 하나 더 지나가 도면이 지저분했다.
@@ -271,10 +207,7 @@ export function appendWallHatch(
group.setAttribute("clip-path", `url(#${clipId})`);
layer.append(clip, group);
const centerBase = wall.outerOffset - wall.outward * (REVET_THICKNESS_M / 2);
const centerTop = wall.topJoint.offset + wall.outward * (REVET_THICKNESS_M / 2);
const embedBase = bottomAtOffset(centerBase);
const spanM = Math.max(0.3, wall.topBack.elevation - embedBase);
const { centerBase, centerTop, embedBase, spanM } = wallHatchBand(wall);
const pixelsPerMeter = Math.abs(x(1) - x(0)) || 1;
const frame: BandFrame = {
spanM,
@@ -315,119 +248,6 @@ export function appendWallHatch(
if (wall.retaining) drawDrains(group, wall, x, toDisplayY);
}
/**
* 도수로 · 돌붙임 띠 단면(90-2) — 윗면 = 사면선(겹친 점 뺌) · 변마다 아래쪽(사면 안쪽) 단위 법선 ·
* 둘레 = 윗면 → 두께만큼 오프셋한 아랫면(거꾸로). 횡단 그림과 B05 3D 띠 솔리드가 같은 것을 쓴다.
*/
export function chuteBandOutline(
raw: OffsetPoint[],
thickness: number,
): { top: OffsetPoint[]; normals: OffsetPoint[]; outline: OffsetPoint[] } | null {
if (!(thickness > 0)) return null;
const top = raw.filter(
(p, i) =>
i === 0 ||
Math.hypot(p.offset - raw[i - 1].offset, p.elevation - raw[i - 1].elevation) > 1e-6,
);
if (top.length < 2) return null;
// 변마다 아래쪽(사면 안쪽) 단위 법선
const normals = top.slice(1).map((p, i) => {
const dx = p.offset - top[i].offset;
const dz = p.elevation - top[i].elevation;
const length = Math.hypot(dx, dz);
const sign = dx / length > 0 ? -1 : 1;
return { offset: (sign * -dz) / length, elevation: (sign * dx) / length };
});
// 꺾임은 두 오프셋 선의 교점(마이터 · 너무 뾰족하면 두 배까지)
const bottom = top.map((p, i) => {
const a = normals[Math.max(i - 1, 0)];
const b = normals[Math.min(i, normals.length - 1)];
const sum = { offset: a.offset + b.offset, elevation: a.elevation + b.elevation };
const k = thickness / Math.max(sum.offset * a.offset + sum.elevation * a.elevation, 0.5);
return { offset: p.offset + sum.offset * k, elevation: p.elevation + sum.elevation * k };
});
return { top, normals, outline: [...top, ...bottom.reverse()] };
}
/** 꺾은선 앞에서부터 `lengthM` 까지(쓴 길이와 함께). */
function trimPolyline(
points: readonly OffsetPoint[],
lengthM: number,
): { points: OffsetPoint[]; used: number } {
const out = points.slice(0, 1);
let used = 0;
for (let i = 1; i < points.length && lengthM - used > 1e-9; i += 1) {
const [a, b] = [points[i - 1], points[i]];
const step = Math.hypot(b.offset - a.offset, b.elevation - a.elevation);
if (step < 1e-9) continue;
const t = Math.min((lengthM - used) / step, 1);
out.push({
offset: a.offset + (b.offset - a.offset) * t,
elevation: a.elevation + (b.elevation - a.elevation) * t,
});
used += step * t;
}
return { points: out, used };
}
/** 원지반 샘플 → 꺾은선(유효 점만 · 오프셋 차례). */
export function groundLine(
samples: ReadonlyArray<{
offset_m?: number | null;
elevation_m?: number | null;
valid?: boolean;
}>,
): OffsetPoint[] {
return samples
.filter((s) => s.valid !== false && s.offset_m != null && s.elevation_m != null)
.map((s) => ({ offset: s.offset_m as number, elevation: s.elevation_m as number }))
.sort((a, b) => a.offset - b.offset);
}
/**
* 도수로 · 돌붙임 선 길이(294-4) — 선들을 차례로 사면 따라 `lengthM` 까지 자름 · 사면보다 길면 마지막 끝에서
* 원지반(`ground`)을 바깥으로 따라 이어 그 길이까지 · 길이 없으면 그대로(원지반 닿으면 끝). 점 하나뿐인 선
* (사면 없음 · 끝이 이미 원지반)은 길이가 있으면 원지반 따라 · 없으면 안 그림. 그림 전용(수량 · 내역 무관).
*/
export function chuteRun<L extends { points: OffsetPoint[] }>(
lines: readonly L[],
lengthM?: number | null,
ground?: readonly OffsetPoint[],
): L[] {
const drawn = (list: L[]) => list.filter((line) => line.points.length > 1);
if (!(lengthM && lengthM > 0) || !lines.length) return drawn([...lines]);
let left = lengthM;
const out: L[] = [];
for (const line of lines) {
if (left <= 1e-9) break;
const cut = trimPolyline(line.points, left);
out.push({ ...line, points: cut.points });
left -= cut.used;
}
const last = out[out.length - 1];
if (left > 1e-9 && ground?.length && last) {
const first = lines[0].points[0];
const end = last.points[last.points.length - 1];
const outward = Math.sign(end.offset - first.offset) || Math.sign(end.offset) || 1;
const along = [
end,
...ground
.filter((p) => (p.offset - end.offset) * outward > 1e-6)
.sort((a, b) => (a.offset - b.offset) * outward),
];
out.push({ ...last, points: trimPolyline(along, left).points });
}
return drawn(out);
}
/** 띠가 서는 유출 조각(152-3) — 성토 조각 + 내림 · 수평 절토 조각(3도 절토선 · 끝 ≤ 시작).
* 3도 선이 못 만나 복합경사로 오르는 절토 조각은 뺌. B06 관 · 흙막이 띠와 3D 띠가 같은 거름. */
export function isChuteSegment(segment: { kind?: string; points: OffsetPoint[] }): boolean {
if (segment.kind !== "cut") return true;
const { points } = segment;
return points.length > 1 && points[points.length - 1].elevation <= points[0].elevation + 1e-6;
}
/**
* 도수로 · 돌붙임 띠(90-2) — 유출 성토사면 선(윗면 = 사면선)을 사면 안쪽(아래)으로 두께만큼 오프셋한
* 띠 · 안에 돌(흙막이 돌과 같은 그리기 · 두께 인자). 선마다 한 띠 · 그림만(면적 · 터파기 무관).
@@ -1,5 +1,5 @@
/* =============================================================================
* B06_Section_UI_Cross_Body_Guard.ts
* B06_Section_Cross_Body_Guard.ts
* 세월교 · 박스 유입 · 유출 흙막이(73-4) — 관보호공 흙막이와 **같은 벽 · 같은 자리 규칙**.
*
* 세트는 배관 흙막이와 같은 `section.culvert`(관 숨김 · `body`)로 얹혀 그림 · 연동 · 경사 · 3D ·
@@ -10,20 +10,20 @@
* 본체가 이 측점에 없으면(연동 옆 측점) 배관 규칙 그대로 — 소유 측점 값을 절대값으로 넘겨 받음.
* ========================================================================== */
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
import type { BoxLayout } from "./B06_Section_UI_Cross_Box_Geom";
import { FILL_SLOPE_RATIO_MIN } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { CrossSection, SectionSample } from "../B06_Section_Api_Fetch";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "../B06_Section_UI_Cross_Box_Geom";
import type { BoxLayout } from "../B06_Section_UI_Cross_Box_Geom";
import { FILL_SLOPE_RATIO_MIN } from "./B06_Section_UI_Cross_Culvert_Const";
import type {
CulvertDesignTrim,
CulvertLayout,
OffsetPoint,
WallAdjust,
WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { computeFordLayout, fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
import type { FordLayout } from "./B06_Section_UI_Cross_Ford_Geom";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
} from "../B06_Section_UI_Cross_Culvert_Types";
import { computeFordLayout, fordAdjustOf } from "../B06_Section_UI_Cross_Ford_Geom";
import type { FordLayout } from "../B06_Section_UI_Cross_Ford_Geom";
import { buildRevetWallGeometry } from "./B06_Section_Cross_Wall_Geom";
type Role = "inlet" | "outlet";
@@ -0,0 +1,71 @@
/* =============================================================================
* B06_Section_Cross_Design_Surface_Geom.ts
* 암선 · 포장층 **기하**(m 좌표) — 그리기는 `../B06_Section_UI_Cross_Design_Surface`(472-3).
* ========================================================================== */
import type { PavementEntry } from "@util/common_util_structure_pavement";
import type { CrossDesign } from "../B06_Section_Api_Fetch";
export type Edge = { offset_m: number; elevation_m: number };
/** 암선 — 유효 지반 샘플을 `offsetM` 만큼 올린 꺾은선들(빈 · 무효 샘플에서 끊음 · 두 점 이상만). */
export function rockBoundarySegments(
samples: Array<{ offset_m?: number; elevation_m?: number | null; valid: boolean }>,
offsetM: number,
): Edge[][] {
const segments: Edge[][] = [];
let current: Edge[] = [];
for (const sample of samples) {
const elevation = sample.elevation_m;
if (sample.valid === false || elevation === null || !Number.isFinite(elevation ?? NaN)) {
if (current.length > 1) segments.push(current);
current = [];
continue;
}
current.push({ offset_m: sample.offset_m ?? 0, elevation_m: (elevation as number) + offsetM });
}
if (current.length > 1) segments.push(current);
return segments;
}
/** 확폭 끈 구간 — 규격 폭 끝(좌 = left − 확폭좌 · 우 = right + 확폭우 · 표고는 두 끝 사이 직선). */
function standardEdges(edges: { left: Edge; right: Edge }, design: CrossDesign) {
const { left, right } = edges;
const span = right.offset_m - left.offset_m || 1;
const at = (offset: number): Edge => ({
offset_m: offset,
elevation_m:
left.elevation_m + ((right.elevation_m - left.elevation_m) * (offset - left.offset_m)) / span,
});
return {
left: at(left.offset_m - (design.widening_left_m ?? 0)),
right: at(right.offset_m + (design.widening_right_m ?? 0)),
};
}
/**
* 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`section.pavement`)면 그 두께 ·
* 확폭 켬/끔(여러 건은 아래로 차례로 쌓음) · 없으면 카드 포장 켬일 때 표준 두께.
* 층마다 네 꼭짓점(좌상 · 우상 · 우하 · 좌하).
*/
export function pavementLayers(
design: CrossDesign,
pavement?: readonly PavementEntry[] | null,
): Array<{ entry: PavementEntry | null; corners: Edge[] }> {
const edges = design.carriageway_edges ?? design.road_edges;
if (!edges) return [];
const layers = pavement?.length ? pavement : design.paved ? [null] : [];
let depth = 0;
return layers.map((entry) => {
const thickness = entry?.thickness_m ?? design.pavement_thickness_m ?? 0.2;
const { left, right } = entry && !entry.widening_on ? standardEdges(edges, design) : edges;
const corners = [
{ offset_m: left.offset_m, elevation_m: left.elevation_m - depth },
{ offset_m: right.offset_m, elevation_m: right.elevation_m - depth },
{ offset_m: right.offset_m, elevation_m: right.elevation_m - depth - thickness },
{ offset_m: left.offset_m, elevation_m: left.elevation_m - depth - thickness },
];
depth += thickness;
return { entry, corners };
});
}
@@ -0,0 +1,57 @@
/* =============================================================================
* B06_Section_Cross_Excavation_Geom.ts
* 벽 터파기 **기하** — 값은 `common_util_excavation` 한 곳(파이썬 짝 · 거울 시험). 그리기는
* `../B06_Section_UI_Cross_Excavation`(472-3).
* ⚠ **비탈을 주지 않는다 — 수직.** 정본 식에 기울기 항이 없다.
* ========================================================================== */
import {
WALL_BLINDING_DEPTH_M,
WALL_BLINDING_WIDTH_M,
WALL_FOUNDATION_DEPTH_M,
WALL_FOUNDATION_WIDTH_M,
WALL_TRENCH_CLEARANCE_M,
} from "@util/common_util_excavation";
import type { WallLayout } from "../B06_Section_UI_Cross_Culvert_Types";
/** 「기초유」인가 — 저장 제원 `foundation` 의 값. 비어 있으면 **그리지 않는다**(근거 없음). */
export function foundationChoice(value: unknown): "기초유" | "기초버림" | null {
const text = typeof value === "string" ? value.trim() : "";
if (text === "기초유") return "기초유";
if (text === "기초버림") return "기초버림";
return null;
}
/**
* 벽 터파기 — 벽 바닥선 아래로 **수직**. 폭은 바닥 폭 + 여유 0.2, 깊이는 기초 몫.
* `foundation` 이 비어 있으면 null — 사용자가 고르기 전에는 근거가 없다.
*/
export function wallTrench(
wall: WallLayout,
foundation: "기초유" | "기초버림" | null,
): {
widthM: number;
depthM: number;
left: number;
right: number;
top: number;
bottom: number;
} | null {
if (foundation === null) return null;
const hasFoundation = foundation === "기초유";
const depth = hasFoundation ? WALL_FOUNDATION_DEPTH_M : WALL_BLINDING_DEPTH_M;
// 폭: 기초 몫은 정본이 정한 값(0.9 / 0.7), 다만 벽 바닥이 그보다 넓으면 그 폭 + 여유.
const baseWidth = Math.abs(wall.bottomFront.offset - wall.bottomBack.offset);
const minWidth = hasFoundation ? WALL_FOUNDATION_WIDTH_M : WALL_BLINDING_WIDTH_M;
const width = Math.max(minWidth, baseWidth + (hasFoundation ? WALL_TRENCH_CLEARANCE_M : 0));
const center = (wall.bottomFront.offset + wall.bottomBack.offset) / 2;
const top = Math.max(wall.bottomFront.elevation, wall.bottomBack.elevation);
return {
widthM: width,
depthM: depth,
left: center - width / 2,
right: center + width / 2,
top,
bottom: top - depth,
};
}
@@ -0,0 +1,334 @@
/* =============================================================================
* B06_Section_Cross_Revetment_Geom.ts
* 독립 기슭막이(배관과 무관) **기하** — 배관 세트 기슭막이와 **같은 배치·같은 산식·같은 도형**.
* 배관만 없을 뿐, 벽 자리·이동·한계는 배관 유출 벽과 한 몸으로 푼다(2026-08-28 사용자:
* "배관용에서 배관만 빼면 대부분 동일해야"). 그리기는 `../B06_Section_UI_Cross_Revetment`(472-3).
*
* · 자리(2026-08-29 사용자 개정): 조작은 **순수 좌우(x)·순수 상하(d)** 0.1m다.
* x = 자동 자리에서의 수평 이동, d = 노견에서 잰 수직 하강. 성토선 각도는 유지하고
* **노견 확장 구간**이 길이를 흡수한다. 한계는 노견 안쪽 금지·확장 0(각도 유지)·매몰 금지.
* 배관 기준벽(`placePipeWall`)의 이동은 손대지 않는다.
* · 실제 적용값(한계 절삭 후)은 `appliedAdjust`로 돌려주고, 화면이 조작 저장소에 되받는다
* — 눌러도 안 움직이는데 숫자만 커지는 것 방지(배관과 같은 syncApplied 규칙).
* · 다단: 배관과 **같은 함수** `buildExtrasAt`. 노견→벽 성토선은 designTrim의 slope.
*
* 3D 코리도는 `tiers[].polygon`·`span`을 그대로 스윕한다.
* ========================================================================== */
import type { CrossSection } from "../B06_Section_Api_Fetch";
import {
FILL_SLOPE_RATIO_MIN,
PIPE_WALL_DEFAULT_RUN_M,
REVET_EMBED_DEPTH_M,
REVET_FILL_TOP_M,
WALL_MIN_PURE_HEIGHT_M,
} from "./B06_Section_UI_Cross_Culvert_Const";
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
import { catalogRow } from "../../common_util/common_util_structure_catalog";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
import { fillWallBaseWidth, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
import { buildExtrasAt } from "../B06_Section_UI_Cross_Culvert_Extra";
import {
ZERO_ADJUST,
type CulvertDesignTrim,
type OutletFillSegment,
type WallAdjust,
type WallLayout,
} from "../B06_Section_UI_Cross_Culvert_Types";
import { buildRevetWallGeometry } from "./B06_Section_Cross_Wall_Geom";
/** 서버가 얹어 주는 독립 기슭막이 제원(`section.revetment`, 구조물 정본 D군). */
export interface RevetmentSpec {
structure_id: string | null;
type_id: string;
name: string;
start_m: number;
end_m: number;
anchor_m: number;
form?: string | null;
height_m?: number | null;
/** 사용자가 고른 설치 측 — "좌" | "우". */
side?: string | null;
/** 기초 축 — "기초유" | "기초버림". **비어 있으면 터파기를 안 그린다**(근거 없음). */
foundation?: string | null;
/** 단 수(다단). 1이면 단일 벽. */
tiers?: number | null;
/** 추가 단(2단부터) 형태 · 높이 — 좌측 폼 「추가 단」 상자(178-3). 빈 값 = 본 벽 형태 · 기본 높이. */
extras?: ReadonlyArray<{ form: string | null; height_m: number | null }> | null;
/** (구 모델) 기준 올림·좌우 이동 — 배관식 전환 뒤 자리는 조정창 4축(x·d)이 정한다. */
lift_m?: number | null;
shift_m?: number | null;
/** 전면 기울기를 B08 과 같은 칸으로 가르는 값 — 성토/절토 · 전면 기울기(2026-09-14 B5). */
face_role?: string | null;
face_slope_ratio?: number | string | null;
}
/** 조정창 조작값 중 이 벽이 쓰는 축 — 배관 벽과 같은 형태(x 좌우·d 사면·h 높이). */
export interface RevetmentAdjust {
x?: number;
d?: number | null;
h?: number | null;
}
export interface RevetPoint {
offset: number;
elevation: number;
}
/** 한 단(段) — 3D 스윕용 폴리곤(배관 벽과 같은 합성 단면). */
export interface RevetmentTier {
polygon: RevetPoint[];
}
export interface RevetmentLayout {
side: "left" | "right";
/** 1단 벽 높이(m) — **바닥~상단**. 조정창 높이 표시·조작의 기준값. */
heightM: number;
/** 이 단면의 누가거리(m) — 부족 안내를 측점 단위로 센다(재렌더 중복 방지). */
chainageM: number;
/** 벽 구조물 id — 그린 벽 신원표(323-5). */
structureId: string | null;
/** 종류 이름(옹벽 · 흙막이 · 돌쌓기 …) — 툴팁 글(323-7 · 「독립 기슭막이」로 뜨던 것). */
name: string;
/** 사용자가 요청한 단 수 — 실제로 선 단 수(`walls.length`)와 다르면 자리가 부족한 것이다. */
requestedTiers: number;
/** 1단 벽 이음선 상단점(성토 설계선 트림 경계). */
top: RevetPoint;
/** 벽 목록(1단 + 다단) — 배관 벽과 같은 `WallLayout`, 그리기가 그대로 쓴다. */
walls: WallLayout[];
/** 기초 축 — 저장 칸과 **같은 글자**(`foundation`). 터파기 그림이 이 값으로 갈린다. */
foundation: string | null;
/** 벽 사이·벽 아래 성토부선(배관 다단과 같은 체계). */
fillSegments: OutletFillSegment[];
/** 성토 설계선을 벽 상단에서 끊고 노견→벽 성토선을 대신 그리는 트림. */
designTrim: CulvertDesignTrim;
/** 한계 절삭 후 **실제 적용된** 1단 조작값 — 화면이 조작 저장소에 되받는다. */
appliedAdjust: WallAdjust;
/** 3D 스윕용 단 목록 — 벽 폴리곤 그대로. */
tiers: RevetmentTier[];
/** 기준 측점 전/후 점유 길이(m) — 3D 스윕 범위. */
span: { beforeM: number; afterM: number };
/** 기준 측점이 아닌 측점(구간 연동) — 주 형상과 색을 갈라 그림(79-6). */
linked: boolean;
}
/** 형태(구조물 옵션) → 재질(높이 한계·라벨). 메 계열은 dry, 찰=wet, 콘크리트=concrete. */
function materialOfForm(form?: string | null): RevetMaterial {
if (!form) return "dry";
if (form.includes("찰")) return "wet";
if (form.includes("콘크리트")) return "concrete";
return "dry"; // 메쌓기·돌망태·통나무·바자 — 메 계열(2.0m 한계)
}
/** 유효 지반 샘플의 바깥 한계 offset(설치 측). 없으면 null. */
function sampleLimit(section: CrossSection, outward: number): number | null {
const offsets = section.samples
.filter((s) => s.valid !== false && typeof s.offset_m === "number")
.map((s) => s.offset_m as number);
if (!offsets.length) return null;
return outward > 0 ? Math.max(...offsets) : Math.min(...offsets);
}
/**
* 노면 횡단물매를 **바깥 방향 1m당 표고 변화**로 돌려준다(하향이면 음수).
* 노면은 측구 쪽으로 내려가는 단일 평면이라(엔진 `road_z`), 노견을 밖으로 연장하면
* 그 평면을 그대로 타고 가야 각도가 맞는다(2026-08-29 사용자).
* 1순위는 **실제 그려진 노견 구간**(차도 끝 → 노면 끝)의 기울기 — 부호를 추측하지
* 않는다. 노견 폭이 0이면 `cross_slope_pct`를 측구 방향 부호로 쓴다.
*/
function roadSlopePerOutward(
design: NonNullable<CrossSection["design"]>,
side: "left" | "right",
outward: number,
): number {
const edge = design.road_edges?.[side];
const inner = design.carriageway_edges?.[side];
if (edge && inner) {
const run = (edge.offset_m - inner.offset_m) * outward;
if (run > 1e-6) return (edge.elevation_m - inner.elevation_m) / run;
}
const pct = Number(design.cross_slope_pct);
if (!Number.isFinite(pct)) return 0;
return (pct / 100) * (design.ditch_side === side ? -1 : 1);
}
/**
* 독립 기슭막이 단면 — 제원·높이·설계선이 없거나 자리를 못 찾으면 null.
* 1단 벽 = 배관 유출 벽과 같은 `placePipeWall`+`buildRevetWallGeometry`,
* 다단 = 배관과 같은 `buildExtrasAt`.
*/
export function computeRevetmentLayout(
section: CrossSection,
adjust?: RevetmentAdjust | null,
): RevetmentLayout | null {
const spec = section.revetment;
const design = section.design;
if (!spec || !design) return null;
const rawHeight = adjust?.h ?? spec.height_m;
// 빈 칸만 안 그림 — 0 이하는 아래 하한(0.1)에서 멈춤(54-51).
if (rawHeight == null || !Number.isFinite(Number(rawHeight))) return null;
const requestedHeight = Number(rawHeight);
// 설치 측이 비어 있으면 **성토가 나는 쪽**에 세운다 — 좌측 「구조물 배치」의 C군 벽
// (옹벽·돌쌓기 등)에는 설치 측 칸이 없기 때문이다(2026-09-06). 양측 절토면 벽이 설
// 자리가 없으므로 그리지 않는다(면적도 그대로).
const fillSide = (): "left" | "right" | null => {
if (design.section_mode === "left_cut") return "right";
if (design.section_mode === "right_cut") return "left";
if (design.section_mode === "both_fill") return "left";
return null;
};
const side = spec.side === "우" ? "right" : spec.side === "좌" ? "left" : fillSide();
if (!side) return null;
const outward = side === "left" ? 1 : -1;
const edge = design.road_edges?.[side];
const groundAt = groundInterpolator(section.samples);
const limit = sampleLimit(section, outward);
if (!edge || !groundAt || limit === null) return null;
// 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준).
// 형태별 높이 한계 없음 — 사용자 입력 그대로(하한만 둠).
const pureHeight = Math.max(requestedHeight, WALL_MIN_PURE_HEIGHT_M);
/** 기준선(근입 위)~상단 — 도형 계산은 종전대로 이 값으로 한다. */
const height = pureHeight - REVET_EMBED_DEPTH_M;
// 전면 기울기 — 관 흙막이와 같은 `outlineLeanOf`(형태 무관 · 형태는 안 무늬만). 단면유형 ·
// 설치 측 · 높이 · 사용자 칸은 그대로 탐. 수량(B08)은 자기 칸으로 따로 셈.
const lean = outlineLeanOf({
height_m: requestedHeight,
section_mode: design.section_mode,
side: spec.side,
face_role: spec.face_role,
face_slope_ratio: spec.face_slope_ratio,
});
const roadSlope = roadSlopePerOutward(design, side, outward);
const autoOffset =
edge.offset_m + outward * (fillWallBaseWidth(height, lean) / 2 - REVET_FILL_TOP_M);
// x 원점 = 성토사면(1:1.2) 경사길이 4m 지점 — 저장된 x 의 절대 자리 그대로(79b).
const autoRun = PIPE_WALL_DEFAULT_RUN_M;
/**
* 조작 축(2026-08-29 사용자): x = **순수 좌우(m)**, d = **노견에서 잰 순수 수직
* 하강(m)**. 성토선 각도는 그대로 두고 **노견 확장 구간**이 길이를 흡수한다
* (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다).
*/
// 손대지 않은 벽(x · d 빈 칸) = 성토사면 제일 위(노견 · 79-5) · 손으로 넣은 벽은 종전 기본(79b).
const untouched = adjust?.d == null && !adjust?.x;
const defaultDrop = untouched ? 0 : autoRun / FILL_SLOPE_RATIO_MIN;
/** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리 — 음수 = 원점보다
* 사면 위(노견 쪽)라 제일 위 자리는 x = −autoRun 이다. */
const xFloor = (drop: number): number => FILL_SLOPE_RATIO_MIN * drop - autoRun;
const requestedX = untouched ? Number.NEGATIVE_INFINITY : (adjust?.x ?? 0);
// 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). 노면 위 금지 = d ≥ 0.
const appliedDrop = Math.round(Math.max(adjust?.d ?? defaultDrop, 0) * 10) / 10;
// 0.1 눈금 반올림(종전과 같음) · 반올림이 하한 아래로 내리면 하한(제일 위 자리가 노견 안쪽으로 안 감).
const floor = xFloor(appliedDrop);
const appliedX = Math.max(Math.round(Math.max(requestedX, floor) * 10) / 10, floor);
const anchorOffset = autoOffset + outward * (autoRun + appliedX);
const baseElevation = edge.elevation_m - appliedDrop - height;
const floatGapM = Math.max(0, baseElevation - groundAt(anchorOffset));
const material = materialOfForm(spec.form);
const lengthM = Number.isFinite(spec.end_m - spec.start_m) ? spec.end_m - spec.start_m : null;
const tier1 = buildRevetWallGeometry({
anchor: { offset: anchorOffset, elevation: baseElevation },
outward,
height,
material,
role: "outlet",
form: spec.form,
lengthM,
floatGapM,
lean,
roadElevation: edge.elevation_m,
});
// 넣은 좌우 이동이 **각도 하한**에 눌려 통째로 무시됐으면 그 하한을 남긴다(2026-09-07).
// 실측 — d 2.6m 자리에서 하한이 1.0m 를 넘어 「1.0m 를 넣어도 0.00m 이동」이 났음.
if (requestedX > 0.005 && floor > requestedX + 0.005) {
tier1.shiftFloorM = Math.round(floor * 100) / 100;
}
// 다단 — 배관과 같은 함수. 1단 벽 전면 하단 꼭짓점을 성토부선 시작점으로.
const requestedTiers = Math.max(1, Math.round(Number(spec.tiers) || 1));
const extras =
requestedTiers > 1
? buildExtrasAt(tier1.bottomFront, {
startBottomElevation: tier1.bottomBack.elevation,
outward,
groundAt,
limitOffset: limit,
// 단마다 상자 형태 · 높이(178-3) — 자리(x · d)는 다단 자동 규칙 그대로
adjusts: Array.from({ length: requestedTiers - 1 }, (_, i) => ({
...ZERO_ADJUST,
m: spec.extras?.[i]?.form ?? null,
h: spec.extras?.[i]?.height_m ?? null,
})),
// 단들도 1단과 같은 형태로 선다 — 안 넘기면 형태가 비어 늘 메쌓기로
// 그려졌다(2026-08-30 사용자: 형태별 모양이 누락되는 경우).
ownerForm: spec.form,
equalize: false,
leanFor: (form, pureHeightM) =>
outlineLeanOf({
form,
height_m: pureHeightM,
section_mode: design.section_mode,
side: spec.side,
}),
})
: { walls: [], segments: [], appliedAdjusts: [], addable: false };
const walls: WallLayout[] = [tier1, ...extras.walls];
// 물빼기 파이프 단면 = 표 형상칸.weep(322-1 · 옹벽)
if (catalogRow(spec.type_id)?.shapeOpts.weep) for (const wall of walls) wall.retaining = true;
const jt = tier1.fillTop;
// 노견 → (물매를 탄 노견 연장) → 벽 이음선 상단점. 연장 구간은 노면 물매 그대로 가고,
// 꺾임점에서 성토 물매 1:1.2로 벽 상단에 닿는다(2026-08-29 사용자 확정).
// D = 노견에서 벽 상단까지 바깥 방향 거리, R0 = 노견과 벽 상단의 낙차
// (D − t) = 1.2 × (R0 + roadSlope × t) → t = (D − 1.2·R0) / (1 + 1.2·roadSlope)
const edgePoint = { offset: edge.offset_m, elevation: edge.elevation_m };
const D = (jt.offset - edge.offset_m) * outward;
const R0 = edge.elevation_m - jt.elevation;
const denominator = 1 + FILL_SLOPE_RATIO_MIN * roadSlope;
const breakRun =
R0 > 1e-6 && D > 1e-6 && Math.abs(denominator) > 1e-9
? Math.min(D, Math.max(0, (D - FILL_SLOPE_RATIO_MIN * R0) / denominator))
: 0;
const slope = {
points:
breakRun > 1e-6
? [
edgePoint,
{
offset: edge.offset_m + outward * breakRun,
elevation: edge.elevation_m + roadSlope * breakRun,
},
jt,
]
: [edgePoint, jt],
};
const designTrim: CulvertDesignTrim =
outward > 0
? { minOffset: -Infinity, maxOffset: jt.offset, maxElevation: jt.elevation, maxSlope: slope }
: { minOffset: jt.offset, maxOffset: Infinity, minElevation: jt.elevation, minSlope: slope };
return {
side,
heightM: pureHeight,
chainageM: Number(section.chainage_m) || 0,
structureId: spec.structure_id ?? null,
name: spec.name || "벽",
requestedTiers,
top: jt,
walls,
foundation: typeof spec.foundation === "string" ? spec.foundation.trim() || null : null,
fillSegments: extras.segments,
designTrim,
appliedAdjust: { x: appliedX, d: appliedDrop, h: adjust?.h ?? null, m: null },
tiers: walls.map((wall) => ({ polygon: wall.points })),
span: {
beforeM: Math.max(spec.anchor_m - spec.start_m, 0),
afterM: Math.max(spec.end_m - spec.anchor_m, 0),
},
linked: Math.abs((Number(section.chainage_m) || 0) - spec.anchor_m) > 0.02,
};
}
@@ -0,0 +1,89 @@
/* =============================================================================
* B06_Section_Cross_Wall_Geom.ts
* 기슭막이 벽 1매 **기하** — 배관 세트 벽(`_Cross_Culvert_Geom`/`_Culvert`)과 독립 기슭막이
* (`_Cross_Revetment`)가 같은 산식을 쓴다. 그리기는 `../B06_Section_UI_Cross_Wall`(472-3).
* ========================================================================== */
import {
materialLabel,
REVET_EMBED_DEPTH_M,
REVET_LEAN_RATIO,
REVET_THICKNESS_M,
WALL_FILL_TOP_INSET_M,
WALL_MIN_PURE_HEIGHT_M,
} from "./B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
import type { OffsetPoint, WallLayout } from "../B06_Section_UI_Cross_Culvert_Types";
/** 벽 1매의 기하 입력 — 자리 기준은 **하단선 중점**(anchor.offset)과 그 자리 기준표고
* (anchor.elevation = 관 invert 또는 지반). 벽은 그 위로 `height`만큼 선다. */
export interface RevetWallGeometryInput {
/** 하단선 중점 offset + 기준표고(벽이 이 위로 선다). */
anchor: OffsetPoint;
/** +1 = 화면 좌측(계류측), −1 = 우측. 전면 1:0.3이 이 부호로 기운다. */
outward: number;
/** 계산용 높이(기준표고~상단, 근입 0.5 제외). */
height: number;
material: RevetMaterial;
role: "inlet" | "outlet" | "extra";
/** 표기용 형태 — 미지정이면 재질 라벨(메/찰/콘크리트). */
form?: string | null;
lengthM?: number | null;
thickness?: number;
floatGapM?: number;
extraIndex?: number;
/** 전면 기울기 1:n 의 n — 표준경사(`leanOf`). 없으면 종전 0.3. */
lean?: number;
/** 노면(계획고) — 벽 윗면이 이보다 높으면 여기서 멈춤. 높이 한도는 이것 하나. */
roadElevation?: number | null;
}
/**
* 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점을 만든다. 배관 벽(`buildWall`,
* `_Cross_Culvert_Geom` 335~376행)과 **같은 산식** — 배면 수직, 상단 변 = 사다리꼴
* 상단(t/2)+띠(t), 전면 1:0.3, 하단선 = 기준표고 −0.5m 수평 기초.
*/
export function buildRevetWallGeometry(input: RevetWallGeometryInput): WallLayout {
const { anchor, outward, material, role } = input;
const road = input.roadElevation;
const height =
road != null && anchor.elevation + input.height > road + 1e-9
? Math.max(road - anchor.elevation, WALL_MIN_PURE_HEIGHT_M - REVET_EMBED_DEPTH_M)
: input.height;
const thickness = input.thickness ?? REVET_THICKNESS_M;
const lean = input.lean ?? REVET_LEAN_RATIO;
const baseWidth = thickness * 1.5 + lean * height;
const backOffset = anchor.offset - outward * (baseWidth / 2);
const topJoint = backOffset + outward * (thickness / 2);
const topElevation = anchor.elevation + height;
const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation };
const topFront = topJoint + outward * thickness;
const frontXAt = (elevation: number): number =>
topFront + outward * lean * (topElevation - elevation);
const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation };
const bottomFront: OffsetPoint = {
offset: frontXAt(bottomElevation),
elevation: bottomElevation,
};
return {
role,
extraIndex: input.extraIndex,
form: input.form ?? materialLabel(material),
lengthM: input.lengthM ?? null,
backOffset,
outerOffset: bottomFront.offset,
base: anchor.elevation,
height,
floatGapM: input.floatGapM ?? 0,
material,
lean,
outward,
topBack,
topJoint: { offset: topJoint, elevation: topElevation },
fillTop: { offset: topFront - outward * WALL_FILL_TOP_INSET_M, elevation: topElevation },
bottomBack,
bottomFront,
points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront],
};
}
@@ -0,0 +1,223 @@
/* =============================================================================
* B06_Section_Cross_Wall_Hatch_Geom.ts
* 벽 안 무늬 · 물빼기 · 도수로 · 돌붙임 띠 **기하**(m 좌표) — 그리기는
* `../B06_Section_UI_Cross_Wall_Hatch`(472-3). 횡단 그림과 B05 3D 띠 솔리드가 같은 것을 쓴다.
* ========================================================================== */
import { REVET_THICKNESS_M } from "./B06_Section_UI_Cross_Culvert_Const";
import type { OffsetPoint, WallLayout } from "../B06_Section_UI_Cross_Culvert_Types";
/** 형태 문자열 → 표현 갈래. 판정 순서는 좁은 말(돌망태·바자)부터. */
export type WallHatch = "dry" | "wet" | "concrete" | "gabion" | "log" | "wattle";
export function hatchOfForm(form: string | null | undefined): WallHatch {
if (!form) return "dry";
if (form.includes("돌망태")) return "gabion";
if (form.includes("통나무") || form.includes("목재")) return "log";
if (form.includes("바자")) return "wattle";
if (form.includes("콘크리트")) return "concrete";
if (form.includes("찰")) return "wet";
return "dry";
}
/** 켜 수 — 켜 높이가 목표치에 가장 가깝게, 최소 2켜. */
export function courses(spanM: number, targetM: number): number {
return Math.max(2, Math.round(spanM / targetM));
}
/** 벽 전면 기운 띠(이음선~전면, 두께 REVET_THICKNESS_M)의 중심축 — 바닥 · 상단 offset · 바닥 표고 · 길이. */
export function wallHatchBand(wall: WallLayout): {
centerBase: number;
centerTop: number;
embedBase: number;
spanM: number;
} {
// 하단선이 원지반을 따라 기울어 있으므로 띠 바닥도 하단선 위에서 잡는다.
const bottomSpan = wall.bottomFront.offset - wall.bottomBack.offset;
const bottomAtOffset = (offset: number): number =>
Math.abs(bottomSpan) > 1e-9
? wall.bottomBack.elevation +
(wall.bottomFront.elevation - wall.bottomBack.elevation) *
((offset - wall.bottomBack.offset) / bottomSpan)
: wall.bottomBack.elevation;
const centerBase = wall.outerOffset - wall.outward * (REVET_THICKNESS_M / 2);
const centerTop = wall.topJoint.offset + wall.outward * (REVET_THICKNESS_M / 2);
const embedBase = bottomAtOffset(centerBase);
const spanM = Math.max(0.3, wall.topBack.elevation - embedBase);
return { centerBase, centerTop, embedBase, spanM };
}
/** 물빼기 파이프 단면(73-2) — 보기용 고정값 · 지식DB 관경 · 간격과 별개. */
const DRAIN_BAND_M = 1.5;
const DRAIN_DIAMETER_M = 0.15;
/** 앞쪽이 낮게 — 벽 두께 1m 당 낙차(보기용). */
const DRAIN_FALL = 0.1;
/** 직선 a→b 가 볼록 벽 단면 안을 지나는 구간 — clip 이 없는 곳(B07 오프스크린 수확)도 벽 밖으로 안 샘. */
function insideRun(a: OffsetPoint, b: OffsetPoint, polygon: OffsetPoint[]): OffsetPoint[] | null {
const dx = b.offset - a.offset;
const dy = b.elevation - a.elevation;
const hits: number[] = [];
polygon.forEach((p, i) => {
const q = polygon[(i + 1) % polygon.length];
const ex = q.offset - p.offset;
const ey = q.elevation - p.elevation;
const denominator = dx * ey - dy * ex;
if (Math.abs(denominator) < 1e-12) return;
const t = ((p.offset - a.offset) * ey - (p.elevation - a.elevation) * ex) / denominator;
const u = ((p.offset - a.offset) * dy - (p.elevation - a.elevation) * dx) / denominator;
if (u >= -1e-9 && u <= 1 + 1e-9) hits.push(t);
});
if (hits.length < 2) return null;
const at = (t: number): OffsetPoint => ({
offset: a.offset + dx * t,
elevation: a.elevation + dy * t,
});
return [at(Math.min(...hits)), at(Math.max(...hits))];
}
/**
* 옹벽 물빼기 — 높이(바닥~상단)를 반올림(높이 / 1.5)개(최소 1) 띠로 고르게 나눠 띠마다
* 가운데 하나. 배면 → 전면으로 꿰는 두 줄 평행선(앞쪽 조금 낮게) · 벽 두께만큼.
* 물빼기마다 벽 안 구간 목록(0~2 줄).
*/
export function wallDrainRuns(wall: WallLayout): OffsetPoint[][][] {
const bottom = wall.bottomBack.elevation;
const height = wall.topBack.elevation - bottom;
const count = Math.max(1, Math.round(height / DRAIN_BAND_M));
const back = wall.backOffset;
const [, , topFront, bottomFront] = wall.points;
const drains: OffsetPoint[][][] = [];
for (let i = 0; i < count; i += 1) {
const center = bottom + (height * (i + 0.5)) / count;
// 그 높이의 전면 — 전면은 기울어 위로 갈수록 배면에 가까움.
const front =
bottomFront.offset + (topFront.offset - bottomFront.offset) * ((center - bottom) / height);
const fall = (DRAIN_FALL * Math.abs(front - back)) / 2;
const runs: OffsetPoint[][] = [];
for (const side of [-0.5, 0.5]) {
const level = center + side * DRAIN_DIAMETER_M;
const run = insideRun(
{ offset: back, elevation: level + fall },
{ offset: front, elevation: level - fall },
wall.points,
);
if (run) runs.push(run);
}
drains.push(runs);
}
return drains;
}
/**
* 도수로 · 돌붙임 띠 단면(90-2) — 윗면 = 사면선(겹친 점 뺌) · 변마다 아래쪽(사면 안쪽) 단위 법선 ·
* 둘레 = 윗면 → 두께만큼 오프셋한 아랫면(거꾸로). 횡단 그림과 B05 3D 띠 솔리드가 같은 것을 쓴다.
*/
export function chuteBandOutline(
raw: OffsetPoint[],
thickness: number,
): { top: OffsetPoint[]; normals: OffsetPoint[]; outline: OffsetPoint[] } | null {
if (!(thickness > 0)) return null;
const top = raw.filter(
(p, i) =>
i === 0 ||
Math.hypot(p.offset - raw[i - 1].offset, p.elevation - raw[i - 1].elevation) > 1e-6,
);
if (top.length < 2) return null;
// 변마다 아래쪽(사면 안쪽) 단위 법선
const normals = top.slice(1).map((p, i) => {
const dx = p.offset - top[i].offset;
const dz = p.elevation - top[i].elevation;
const length = Math.hypot(dx, dz);
const sign = dx / length > 0 ? -1 : 1;
return { offset: (sign * -dz) / length, elevation: (sign * dx) / length };
});
// 꺾임은 두 오프셋 선의 교점(마이터 · 너무 뾰족하면 두 배까지)
const bottom = top.map((p, i) => {
const a = normals[Math.max(i - 1, 0)];
const b = normals[Math.min(i, normals.length - 1)];
const sum = { offset: a.offset + b.offset, elevation: a.elevation + b.elevation };
const k = thickness / Math.max(sum.offset * a.offset + sum.elevation * a.elevation, 0.5);
return { offset: p.offset + sum.offset * k, elevation: p.elevation + sum.elevation * k };
});
return { top, normals, outline: [...top, ...bottom.reverse()] };
}
/** 꺾은선 앞에서부터 `lengthM` 까지(쓴 길이와 함께). */
function trimPolyline(
points: readonly OffsetPoint[],
lengthM: number,
): { points: OffsetPoint[]; used: number } {
const out = points.slice(0, 1);
let used = 0;
for (let i = 1; i < points.length && lengthM - used > 1e-9; i += 1) {
const [a, b] = [points[i - 1], points[i]];
const step = Math.hypot(b.offset - a.offset, b.elevation - a.elevation);
if (step < 1e-9) continue;
const t = Math.min((lengthM - used) / step, 1);
out.push({
offset: a.offset + (b.offset - a.offset) * t,
elevation: a.elevation + (b.elevation - a.elevation) * t,
});
used += step * t;
}
return { points: out, used };
}
/** 원지반 샘플 → 꺾은선(유효 점만 · 오프셋 차례). */
export function groundLine(
samples: ReadonlyArray<{
offset_m?: number | null;
elevation_m?: number | null;
valid?: boolean;
}>,
): OffsetPoint[] {
return samples
.filter((s) => s.valid !== false && s.offset_m != null && s.elevation_m != null)
.map((s) => ({ offset: s.offset_m as number, elevation: s.elevation_m as number }))
.sort((a, b) => a.offset - b.offset);
}
/**
* 도수로 · 돌붙임 선 길이(294-4) — 선들을 차례로 사면 따라 `lengthM` 까지 자름 · 사면보다 길면 마지막 끝에서
* 원지반(`ground`)을 바깥으로 따라 이어 그 길이까지 · 길이 없으면 그대로(원지반 닿으면 끝). 점 하나뿐인 선
* (사면 없음 · 끝이 이미 원지반)은 길이가 있으면 원지반 따라 · 없으면 안 그림. 그림 전용(수량 · 내역 무관).
*/
export function chuteRun<L extends { points: OffsetPoint[] }>(
lines: readonly L[],
lengthM?: number | null,
ground?: readonly OffsetPoint[],
): L[] {
const drawn = (list: L[]) => list.filter((line) => line.points.length > 1);
if (!(lengthM && lengthM > 0) || !lines.length) return drawn([...lines]);
let left = lengthM;
const out: L[] = [];
for (const line of lines) {
if (left <= 1e-9) break;
const cut = trimPolyline(line.points, left);
out.push({ ...line, points: cut.points });
left -= cut.used;
}
const last = out[out.length - 1];
if (left > 1e-9 && ground?.length && last) {
const first = lines[0].points[0];
const end = last.points[last.points.length - 1];
const outward = Math.sign(end.offset - first.offset) || Math.sign(end.offset) || 1;
const along = [
end,
...ground
.filter((p) => (p.offset - end.offset) * outward > 1e-6)
.sort((a, b) => (a.offset - b.offset) * outward),
];
out.push({ ...last, points: trimPolyline(along, left).points });
}
return drawn(out);
}
/** 띠가 서는 유출 조각(152-3) — 성토 조각 + 내림 · 수평 절토 조각(3도 절토선 · 끝 ≤ 시작).
* 3도 선이 못 만나 복합경사로 오르는 절토 조각은 뺌. B06 관 · 흙막이 띠와 3D 띠가 같은 거름. */
export function isChuteSegment(segment: { kind?: string; points: OffsetPoint[] }): boolean {
if (segment.kind !== "cut") return true;
const { points } = segment;
return points.length > 1 && points[points.length - 1].elevation <= points[0].elevation + 1e-6;
}
@@ -8,26 +8,26 @@
* · `routeCarryPatches` — 노선 바뀐 [저장]: 옛 노선 세션 칸 → 새 노선 서버 상세(측점 열쇠 넘김
* `logic/B06_Section_Route_Carry`) → 얹기 → patch(옛 브라우저 길 `carryAfterRouteSave` 와 같은 차례) ·
* 서버 Node 입구 `stations.carry` 가 부름(`A00_Common/design/logic/design_calc_node_carry.ts`).
* logic 밖 = 구조물 그림 기하(관 벽 · 단 · 구간 · 세월 · BOX 기본값)를 끔 — 서버 Node 묶음 알려진 끌림.
* 관 벽 · 단 · 구간 · 세월 · BOX 기본값 = 구조물 기하 파일(`_Cross_Culvert_Types` · `_Wire` · `_Ford_Geom` · `_Box_Geom`).
* ========================================================================== */
import type {
CrossDesign,
CrossSectionPatch,
SectionDetailResponse,
} from "./B06_Section_Api_Fetch";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import { fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
import { DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
import { PIPE_MATCH_M } from "../common_util/common_util_pipe_match";
import { inferStationInterval } from "../common_util/common_util_svg";
import { applySpanPatch, WALL_MIN_PURE_HEIGHT_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { tierSpanOf } from "./B06_Section_UI_Cross_Culvert_Wire";
} from "../B06_Section_Api_Fetch";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "../B06_Section_UI_Cross_Culvert_Types";
import type { WallAdjust } from "../B06_Section_UI_Cross_Culvert_Types";
import { fordAdjustOf } from "../B06_Section_UI_Cross_Ford_Geom";
import { DEFAULT_BOX_SIDE_ADJUST } from "../B06_Section_UI_Cross_Box_Geom";
import { PIPE_MATCH_M } from "../../common_util/common_util_pipe_match";
import { inferStationInterval } from "../../common_util/common_util_station";
import { applySpanPatch, WALL_MIN_PURE_HEIGHT_M } from "./B06_Section_UI_Cross_Culvert_Const";
import { tierSpanOf } from "../B06_Section_UI_Cross_Culvert_Wire";
import { computeStoredLayouts } from "./B06_Section_Structure_Layouts";
import { ROW_CARRY_KEYS } from "./logic/B06_Section_Station_Swap";
import { mergeCrossPatches } from "./logic/B06_Section_UI_Page_Patches";
import { STRUCTURE_ADJUST_KEYS, USER_TOUCHED_KEYS } from "./logic/B06_Section_Design_Keys";
import { ROW_CARRY_KEYS } from "./B06_Section_Station_Swap";
import { mergeCrossPatches } from "./B06_Section_UI_Page_Patches";
import { STRUCTURE_ADJUST_KEYS, USER_TOUCHED_KEYS } from "./B06_Section_Design_Keys";
import {
carryChainageStates,
carryKeyedStates,
@@ -37,7 +37,7 @@ import {
type KeyedState,
type Owner,
type Raw,
} from "./logic/B06_Section_Route_Carry";
} from "./B06_Section_Route_Carry";
export type Cells = Record<string, unknown>;
/** 누가거리(숫자) → 쪽마다 값(세션에 있는 쪽만 · 노견 확장 · 소단 깊이). */
@@ -11,28 +11,28 @@
* ========================================================================== */
import { computeStructureAreas, wallBodyInFillM2 } from "@util/common_util_cross_structure_areas";
import type { CrossSection } from "./B06_Section_Api_Fetch";
import { withoutGuardedFill } from "./B06_Section_UI_Cross_Body_Guard";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
import type { BoxLayout } from "./B06_Section_UI_Cross_Box_Geom";
import { REVET_EMBED_DEPTH_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import { groundInterpolator } from "./logic/B06_Section_UI_Cross_Culvert_Solve";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
import type { BasinShape, WallAdjust, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import type { CrossSection } from "../B06_Section_Api_Fetch";
import { withoutGuardedFill } from "./B06_Section_Cross_Body_Guard";
import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "../B06_Section_UI_Cross_Box_Geom";
import type { BoxLayout } from "../B06_Section_UI_Cross_Box_Geom";
import { REVET_EMBED_DEPTH_M } from "./B06_Section_UI_Cross_Culvert_Const";
import { groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "../B06_Section_UI_Cross_Culvert_Types";
import type { BasinShape, WallAdjust, WallLayout } from "../B06_Section_UI_Cross_Culvert_Types";
import {
computeCardCulvert,
culvertLinkFor,
ownWallsDrawAt,
tierSpanOf,
} from "./B06_Section_UI_Cross_Culvert_Wire";
} from "../B06_Section_UI_Cross_Culvert_Wire";
import type {
ExtraWallControl,
InletStructureControl,
RevetOffsetControl,
} from "./B06_Section_UI_Cross_Culvert_Wire";
import { computeFordLayout, fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
import type { FordLayout } from "./B06_Section_UI_Cross_Ford_Geom";
import { computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment";
} from "../B06_Section_UI_Cross_Culvert_Wire";
import { computeFordLayout, fordAdjustOf } from "../B06_Section_UI_Cross_Ford_Geom";
import type { FordLayout } from "../B06_Section_UI_Cross_Ford_Geom";
import { computeRevetmentLayout } from "./B06_Section_Cross_Revetment_Geom";
/** 같은 측점으로 볼 누가거리 오차(m) — 정본 반올림 자릿수보다 크게 잡는다. */
const CHAINAGE_TOLERANCE_M = 0.02;
@@ -12,13 +12,13 @@ import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
import {
computeStoredLayouts as computeLayouts,
type StoredLayouts,
} from "../B06_Section/B06_Section_Structure_Layouts";
} from "../B06_Section/logic/B06_Section_Structure_Layouts";
import { appendBoxOverlay } from "../B06_Section/B06_Section_UI_Cross_Box";
import { appendCulvertOverlay } from "../B06_Section/B06_Section_UI_Cross_Culvert";
import { appendFordOverlay } from "../B06_Section/B06_Section_UI_Cross_Ford";
import { appendFordPavementOverlay } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement";
import { appendUnderdrainOverlay } from "../B06_Section/B06_Section_UI_Cross_Underdrain";
import { groundLine } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch";
import { groundLine } from "../B06_Section/logic/B06_Section_Cross_Wall_Hatch_Geom";
import {
appendCrossDesignOverlay,
appendPavementOverlay,
+16
View File
@@ -29,3 +29,19 @@ export function stationPartsLabel(
const scale = decimals ? 10 : 1;
return `${stationNumber}+${Math.round(remainder * scale) / scale}`;
}
/** 측점 간격을 실제 측점 배열에서 되짚는다(최빈 간격). 라벨 표기의 분모다. */
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
);
}
+1 -17
View File
@@ -33,20 +33,4 @@ export function svgText(
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
);
}
export { stationLabel } from "./common_util_station";
export { inferStationInterval, stationLabel } from "./common_util_station";
+1 -1
View File
@@ -54,7 +54,7 @@ bodies.box.control.select(352.14, "left");
out.released = released;
// 107-2 — 원지반을 바꾼 사본: 세월교 바닥 = 노면 − (관경 + 토피) · 박스 본체 · 끝 · 날개 · 흙막이 자리 그대로
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
);
+1 -1
View File
@@ -120,7 +120,7 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts"));
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
const { wallStandsAt } = loadTs(B06("B06_Section_UI_Cross_Culvert_Wire"));
const { applyStructurePick } = loadTs(B06("B06_Section_UI_Page_Structure_Pick"));
@@ -32,7 +32,7 @@ const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { buildCorridorStructures } = loadTs(B05("B05_Profile_UI_Corridor_Structures"));
const { structureHashParts } = loadTs(B05("logic/B05_Profile_UI_Corridor_Structures_Hash"));
const { structureTopLoops } = loadTs(B05("logic/B05_Profile_UI_Corridor_Tin_Design"));
const { chuteBandOutline, isChuteSegment } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
const { chuteBandOutline, isChuteSegment } = loadTs(B06("logic/B06_Section_Cross_Wall_Hatch_Geom"));
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom"));
const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford"));
const { outletFillLine } = loadTs(B06("B06_Section_UI_Cross_Ford_Pavement"));
+1 -1
View File
@@ -219,7 +219,7 @@ out.zone = {
};
// ⑤ 실제 측점 — 설계선 + 배관 오버레이를 그린 뒤 고름을 붙임(카드와 같은 차례)
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts"));
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
const { appendCrossDesignOverlay } = loadTs(B06("B06_Section_UI_Cross_Design"));
const { raiseWalls } = loadTs(B06("B06_Section_UI_Cross_Wall"));
@@ -29,7 +29,7 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts"));
const { computeStructureAreas } = loadTs(
path.join(ROOT, "common_util", "common_util_cross_structure_areas.ts"),
);
@@ -30,7 +30,7 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
+2 -2
View File
@@ -29,8 +29,8 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { isChuteSegment, chuteBandOutline } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts"));
const { isChuteSegment, chuteBandOutline } = loadTs(B06("logic/B06_Section_Cross_Wall_Hatch_Geom"));
const { chuteBands } = loadTs(
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Corridor_Structures_Chute.ts"),
);
+1 -1
View File
@@ -125,7 +125,7 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts"));
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
const { attachChuteBandPick } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
const { applyStructurePick } = loadTs(B06("B06_Section_UI_Page_Structure_Pick"));
@@ -47,7 +47,11 @@ function loadTs(file, stubs = {}) {
}).loadTs(file);
}
const surface = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Design_Surface.ts"));
const surface = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Design_Surface.ts"), {
"./logic/B06_Section_Cross_Design_Surface_Geom": loadTs(
path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Design_Surface_Geom.ts"),
),
});
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
"../b_page_state": { readState: () => null, writeState: () => {} },
@@ -35,7 +35,7 @@ const { ADDON_SPECS } = loadTs(design("logic/design_structure_slope_tier.ts"));
const { STRUCTURE_SPECS } = loadTs(design("logic/design_structure_pipe.ts"));
const { wallSpecsFrom } = loadTs(path.join(ROOT, "common_util", "common_util_structure_walls.ts"));
const { computeRevetmentLayout } = loadTs(
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Revetment.ts"),
path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Revetment_Geom.ts"),
);
const r3 = (v) => Math.round(v * 1000) / 1000;
@@ -30,7 +30,7 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { computeStoredLayouts, structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
const { computeStoredLayouts, structureAreaRows } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts"));
const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts"));
const read = (name) =>
JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", name), "utf8")).cross_sections;
+1 -1
View File
@@ -80,7 +80,7 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts"));
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
const { outlineBox } = loadTs(B06("B06_Section_UI_Cross_Culvert_Types"));
@@ -223,7 +223,7 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts");
const storeMod = load("A00_Common", "design", "design_store.ts");
const sectionStore = load("B06_Section", "B06_Section_Section_Store.ts");
const common = load("B06_Section", "B06_Section_UI_Section_Common.ts");
const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts");
const layouts = load("B06_Section", "logic", "B06_Section_Structure_Layouts.ts");
const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts");
const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts");
const baseMod = load("A00_Common", "design", "design_profile_base.ts");
@@ -23,11 +23,20 @@ function loadTs(file, stubs = {}) {
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const station = loadTs(path.join(ROOT, "common_util", "common_util_structure_station.ts"));
// 띠 기하(472-3 logic) — 그림 파일 · 물넘이포장이 같은 모듈
const hatchGeom = loadTs(
path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Wall_Hatch_Geom.ts"),
{ "./B06_Section_UI_Cross_Culvert_Const": {} },
);
const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"), {
"./B06_Section_UI_Cross_Wall_Hatch": loadTs(
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"),
{ "./logic/B06_Section_UI_Cross_Culvert_Const": {} },
{
"./logic/B06_Section_UI_Cross_Culvert_Const": {},
"./logic/B06_Section_Cross_Wall_Hatch_Geom": hatchGeom,
},
),
"./logic/B06_Section_Cross_Wall_Hatch_Geom": hatchGeom,
"./B06_Section_UI_Cross_Design_Surface": { appendPavementOverlay() {} },
// 관 신원표(323-5 · 472-2 에서 common_util 로) — 진짜 · 장부 안 꽂음 = 옛 자리 이름
"../common_util/common_util_pick_tag": loadTs(
@@ -56,7 +56,7 @@ pave.appendFordPavementOverlay(
const paveLayer = svg.children.find((k) => (k.attrs.class ?? "").includes("ford-pavement"));
// 294-4 도수로 · 돌붙임 끝단 · 길이
const hatch = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"));
const hatch = load(path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Wall_Hatch_Geom.ts"));
const P = (offset, elevation) => ({ offset, elevation });
const slope = [{ points: [P(0, 0), P(3, -4)], kind: "fill" }]; // 사면 5 m
const flat = [P(3, -4), P(5, -4), P(8, -4)]; // 사면 끝 바깥 원지반
@@ -44,7 +44,7 @@ globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
const hatch = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"));
const hatch = load(path.join(ROOT, "B06_Section", "logic", "B06_Section_Cross_Wall_Hatch_Geom.ts"));
const fillGround = hatch.groundLine(input.fillSamples);
const dipGround = hatch.groundLine(input.dipSamples);
const chute = (design, ground, length) =>
+10 -7
View File
@@ -592,10 +592,16 @@ const loadDrafts = makeLoader(
);
// ── 6. B05 [저장](캐시 patch) — 종류 바꾼 측점 = structure_reset · 옛 조정값 안 실음 ─────
const DESIGN_KEYS = {
USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"],
STRUCTURE_ADJUST_KEYS: makeLoader([B06("logic/B06_Section_Design_Keys")])(
B06("logic/B06_Section_Design_Keys"),
).STRUCTURE_ADJUST_KEYS,
};
const loadStore = makeLoader(
[
B06("B06_Section_Section_Store"),
B06("B06_Section_Route_Carry_Patch"), // patch 바탕 한 벌(472-6)
B06("logic/B06_Section_Route_Carry_Patch"), // patch 바탕 한 벌(472-6)
B06("B06_Section_Api_Culvert_Options"),
path.join(ROOT, "A00_Common", "b_page_state.ts"),
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
@@ -605,12 +611,9 @@ const loadStore = makeLoader(
"./B06_Section_Cross_Refresh": {
USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"],
},
"./logic/B06_Section_Design_Keys": {
USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"],
STRUCTURE_ADJUST_KEYS: makeLoader([B06("logic/B06_Section_Design_Keys")])(
B06("logic/B06_Section_Design_Keys"),
).STRUCTURE_ADJUST_KEYS,
},
"./logic/B06_Section_Design_Keys": DESIGN_KEYS,
// patch 바탕(logic/Route_Carry_Patch · 472-3)이 부르는 이름
"./B06_Section_Design_Keys": DESIGN_KEYS,
},
);
+2 -2
View File
@@ -118,7 +118,7 @@ const STUBS = new Map([
{ kindChangedChainages: () => [], STRUCTURE_ADJUST_KEYS: [] },
],
[
B06("B06_Section_Structure_Layouts"),
B06("logic/B06_Section_Structure_Layouts"),
{ applyStructureAreaRows: () => {}, structureAreaRows: () => [] },
],
[
@@ -212,7 +212,7 @@ const {
const { loadSectionDetail } = loadTs(B06("B06_Section_Section_Store"));
const { forgetDraftStations } = loadTs(B06("B06_Section_Section_Drafts"));
const { buildCorridorStructures } = loadTs(B05("B05_Profile_UI_Corridor_Structures"));
const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment"));
const { computeRevetmentLayout } = loadTs(B06("logic/B06_Section_Cross_Revetment_Geom"));
const { buildStructureLane } = loadTs(B05("B05_Profile_UI_Structures_Marks"));
const { pickGroups } = loadTs(B05("B05_Profile_UI_Structures_Panel_Types"));
const { structureDisplayName } = loadTs(B06("B06_Section_UI_Section_Common"));
+1 -1
View File
@@ -193,7 +193,7 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts");
const storeMod = load("A00_Common", "design", "design_store.ts");
const sectionStore = load("B06_Section", "B06_Section_Section_Store.ts");
const common = load("B06_Section", "B06_Section_UI_Section_Common.ts");
const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts");
const layouts = load("B06_Section", "logic", "B06_Section_Structure_Layouts.ts");
const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts");
const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts");
const baseMod = load("A00_Common", "design", "design_profile_base.ts");
+1 -1
View File
@@ -231,7 +231,7 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts");
const storeMod = load("A00_Common", "design", "design_store.ts");
const sectionStore = load("B06_Section", "B06_Section_Section_Store.ts");
const common = load("B06_Section", "B06_Section_UI_Section_Common.ts");
const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts");
const layouts = load("B06_Section", "logic", "B06_Section_Structure_Layouts.ts");
const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts");
const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts");
const baseMod = load("A00_Common", "design", "design_profile_base.ts");
+1 -1
View File
@@ -224,7 +224,7 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts");
const storeMod = load("A00_Common", "design", "design_store.ts");
const sectionStore = load("B06_Section", "B06_Section_Section_Store.ts");
const common = load("B06_Section", "B06_Section_UI_Section_Common.ts");
const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts");
const layouts = load("B06_Section", "logic", "B06_Section_Structure_Layouts.ts");
const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts");
const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts");
const baseMod = load("A00_Common", "design", "design_profile_base.ts");
+1 -1
View File
@@ -42,7 +42,7 @@ function memoryStorage() {
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts"));
const { structureAreaRows } = loadTs(B06("logic/B06_Section_Structure_Layouts"));
const { withDraftAdjusts } = loadTs(B06("B06_Section_Section_Adjusts"));
const { writeState } = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts"));
@@ -43,7 +43,7 @@ function memoryStorage() {
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { structureAreaRows, computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { structureAreaRows, computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts"));
const { layPipeOptions } = loadTs(B06("B06_Section_Section_Adjusts"));
const { applyPipeOptionsToCache } = loadTs(B06("B06_Section_UI_Page_Pipe_Options"));
const { ZERO_ADJUST } = loadTs(B06("B06_Section_UI_Cross_Culvert_Types"));
@@ -5,7 +5,7 @@ export {
applyStructureAreaRows,
computeStoredLayouts,
structureAreaRows,
} from "../../B06_Section/B06_Section_Structure_Layouts";
} from "../../B06_Section/logic/B06_Section_Structure_Layouts";
export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel";
export { writeState } from "../../A00_Common/b_page_state";
export { rebasePipeDraft } from "../../common_util/common_util_route_follow";
+1 -1
View File
@@ -76,7 +76,7 @@ const load = makeLoader(
B06("logic/B06_Section_UI_Page_Patches"),
B06("logic/B06_Section_Design_Keys"), // 칸 목록 · 얹기 · patch 꼴 한 벌(472-6)
B06("logic/B06_Section_Route_Carry"),
B06("B06_Section_Route_Carry_Patch"),
B06("logic/B06_Section_Route_Carry_Patch"),
path.join(ROOT, "common_util", "common_util_pipe_match.ts"),
path.join(ROOT, "A00_Common", "b_page_state.ts"),
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
@@ -29,7 +29,7 @@ export { createCrossStage } from "../../B06_Section/logic/B06_Section_Cross_Stag
export {
applyStructureAreaRows,
structureAreaRows,
} from "../../B06_Section/B06_Section_Structure_Layouts";
} from "../../B06_Section/logic/B06_Section_Structure_Layouts";
export { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist";
export { parseDtmGrid } from "../../common_util/common_util_surface_sampler";
export { computeMassHaul } from "../../common_util/common_util_mass_haul";
+1 -1
View File
@@ -20,7 +20,7 @@ export {
withCarriedEdits,
withDraftAdjusts,
} from "../../B06_Section/B06_Section_Section_Adjusts";
export { crossPatchesFromCache } from "../../B06_Section/B06_Section_Route_Carry_Patch";
export { crossPatchesFromCache } from "../../B06_Section/logic/B06_Section_Route_Carry_Patch";
export { mergeCrossPatches } from "../../B06_Section/logic/B06_Section_UI_Page_Patches";
export { kindChangedChainages } from "../../B06_Section/B06_Section_Api_Culvert_Options";
export { pipeChainageOf } from "../../A00_Common/design/design_pipe_ids";
+2 -2
View File
@@ -31,9 +31,9 @@ const resolve = (base) =>
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { computeStoredLayouts, structureAreaRows, trimOfLayouts } = loadTs(
B06("B06_Section_Structure_Layouts.ts"),
B06("logic/B06_Section_Structure_Layouts.ts"),
);
const { bodyGuardBase } = loadTs(B06("B06_Section_UI_Cross_Body_Guard.ts"));
const { bodyGuardBase } = loadTs(B06("logic/B06_Section_Cross_Body_Guard.ts"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
+3 -3
View File
@@ -29,11 +29,11 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { computeStoredLayouts, structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
const { bodyGuardBase } = loadTs(B06("B06_Section_UI_Cross_Body_Guard.ts"));
const { computeStoredLayouts, structureAreaRows } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts"));
const { bodyGuardBase } = loadTs(B06("logic/B06_Section_Cross_Body_Guard.ts"));
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts"));
const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts"));
const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment.ts"));
const { computeRevetmentLayout } = loadTs(B06("logic/B06_Section_Cross_Revetment_Geom.ts"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
+1 -1
View File
@@ -29,7 +29,7 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
+1 -1
View File
@@ -29,7 +29,7 @@ const resolve = (base) =>
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", f);
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
const { computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts.ts"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
@@ -167,7 +167,9 @@ def test_B06_새_키_한_벌():
assert CrossSectionPatch(chainage_m=1, ford_pipe_length_m=9).ford_pipe_length_m == 9
with pytest.raises(ValueError):
CrossSectionPatch(chainage_m=1, ford_pipe_length_m=-1)
layouts = (ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts").read_text("utf-8")
layouts = (ROOT / "B06_Section" / "logic" / "B06_Section_Structure_Layouts.ts").read_text(
"utf-8"
)
block = layouts[layouts.index("STRUCTURE_ROW_KEYS = [") :][:900]
assert '"ford_pipe_length_m"' in block
@@ -76,7 +76,7 @@ def test_관_벽이_서는_측점은_독립_벽을_안_그린다(bundle, tmp_pat
def test_카드_서버_3D_가_가드_한_벌을_쓴다():
files = {
"카드": ROOT / "B06_Section" / "B06_Section_UI_Cross_View.ts",
"서버 면적 · B07": ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts",
"서버 면적 · B07": ROOT / "B06_Section" / "logic" / "B06_Section_Structure_Layouts.ts",
"3D": ROOT / "B05_Profile" / "B05_Profile_UI_Corridor_Structures.ts",
}
for where, path in files.items():
@@ -134,7 +134,7 @@ def test_넘김_알림은_결과_한_줄에_합침():
keys = _read("B06_Section/B06_Section_Station_Keys.ts")
assert "{ quiet = false, applyTo = toRouteId }: { quiet?: boolean; applyTo?: number }" in keys
assert "if (outside.size && !quiet)" in keys
patch = _read("B06_Section/B06_Section_Route_Carry_Patch.ts")
patch = _read("B06_Section/logic/B06_Section_Route_Carry_Patch.ts")
assert "outside: keyed.outside.size + chain.outside.size" in patch
# 498-3 — 결과 한 줄 걷음 · 남김 = 따라감 실패 · 새 교차 관
save = _read("A00_Common/design/design_save.ts")
+7 -2
View File
@@ -233,7 +233,9 @@ def test_M5_새_행은_버린_수에_안_셈():
def test_H5_노선_저장_뒤_본문_M7_kindreset_옮김():
# 472-6 6-B — 노선 저장 뒤 본문 = 서버 Node 넘김(`routeCarryPatches` · 옛 `saveCachedCrossPatches` ·
# `sessionOverlay` 몸통 그대로) · 표준 횡단면 · 풍선 = 브라우저 횡단 조각(`routeCarryPart`)
patch = (ROOT / "B06_Section" / "B06_Section_Route_Carry_Patch.ts").read_text(encoding="utf-8")
patch = (ROOT / "B06_Section" / "logic" / "B06_Section_Route_Carry_Patch.ts").read_text(
encoding="utf-8"
)
body = patch[patch.index("export function routeCarryPatches") :]
for needle in (
"crossPatchesFromCache(detail, kindChanged, savedAt)",
@@ -245,7 +247,10 @@ def test_H5_노선_저장_뒤_본문_M7_kindreset_옮김():
assert needle in body, needle
apply = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
part = apply[apply.index("export function routeCarryPart") :]
for needle in ("readStandardCrossSession(projectId)", "balloon_offsets: balloonOffsetsPayload()"):
for needle in (
"readStandardCrossSession(projectId)",
"balloon_offsets: balloonOffsetsPayload()",
):
assert needle in part, needle
persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
assert (
+3 -8
View File
@@ -95,8 +95,6 @@ GEOMETRY_PULLS = (
"B05_Profile/B05_Profile_UI_Corridor_Structures_Box.ts",
"B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts",
"B05_Profile/B05_Profile_UI_Corridor_Structures_Path.ts",
"B06_Section/B06_Section_Structure_Layouts.ts",
"B06_Section/B06_Section_UI_Cross_Body_Guard.ts",
"B06_Section/B06_Section_UI_Cross_Box.ts",
"B06_Section/B06_Section_UI_Cross_Box_Geom.ts",
"B06_Section/B06_Section_UI_Cross_Culvert.ts",
@@ -105,16 +103,13 @@ GEOMETRY_PULLS = (
"B06_Section/B06_Section_UI_Cross_Culvert_Tiers.ts",
"B06_Section/B06_Section_UI_Cross_Culvert_Types.ts",
"B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts",
"B06_Section/B06_Section_UI_Cross_Design_Surface.ts",
"B06_Section/B06_Section_UI_Cross_Excavation.ts",
"B06_Section/B06_Section_UI_Cross_Ford.ts",
"B06_Section/B06_Section_UI_Cross_Ford_Geom.ts",
"B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts",
"B06_Section/B06_Section_UI_Cross_Revetment.ts",
"B06_Section/B06_Section_UI_Cross_Wall.ts",
# 518-3 기하는 logic 으로 · 그리기 함수(띠 · 터파기 · 포장층)를 위 Culvert · Box · Ford 그림 파일이 부름 — 518-2 뒤 걷음
"B06_Section/B06_Section_UI_Cross_Design_Surface.ts",
"B06_Section/B06_Section_UI_Cross_Excavation.ts",
"B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts",
# 노선 바뀐 [저장] 넘김 patch(472-6 6-B) — 세션 칸 → 설계 칸 얹기가 관 벽 · 단 · 세월 · BOX 기본값(위 그림 파일)을 씀
"B06_Section/B06_Section_Route_Carry_Patch.ts",
)
# 묶음 단계 입구(511-B B-1) planline 저장 기준(`plantedBase`) 끌림 11 = B-3 이 logic 으로 옮겨 걷음
KNOWN_PULLS = set(SESSION_PULLS) | set(GEOMETRY_PULLS)
+1 -1
View File
@@ -75,7 +75,7 @@ def test_quantity_F7_C4_E1_서버_브라우저_같음(tmp_path):
conv = _fixture("context.json")["earthwork_conversion"]
long_ = detail["longitudinal"]
calls = [
{"mod": "B06_Section/B06_Section_Structure_Layouts.ts", "fn": "structureAreaRows",
{"mod": "B06_Section/logic/B06_Section_Structure_Layouts.ts", "fn": "structureAreaRows",
"args": [sections]},
{"mod": "common_util/common_util_mass_haul.ts", "fn": "computeMassHaulSeries",
"args": [long_, sections, conv, None, ["cross"]]},
+1 -1
View File
@@ -185,7 +185,7 @@ def test_저장_정본_새_길_브라우저_같음(name, monkeypatch, tmp_path):
detail = json.loads((GOLDEN / "detail.json").read_text(encoding="utf-8"))
node_input = {"detail": detail, "context": _contexts()[name]}
new = prebuild._node_output(copy.deepcopy(node_input))
layouts = "B06_Section/B06_Section_Structure_Layouts.ts"
layouts = "B06_Section/logic/B06_Section_Structure_Layouts.ts"
sections = detail["cross_sections"]
areas, walls = _browser(
tmp_path,
+1 -1
View File
@@ -20,7 +20,7 @@ if str(PROJECT_ROOT) not in sys.path:
from B06_Section.B06_Section_Schema import CrossSectionPatch # noqa: E402
_LAYOUTS = PROJECT_ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts"
_LAYOUTS = PROJECT_ROOT / "B06_Section" / "logic" / "B06_Section_Structure_Layouts.ts"
_PERSIST = PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts"
_TYPES = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Types.ts"
+3 -2
View File
@@ -29,7 +29,7 @@ const make = (tag) => ({
set textContent(v) { this.text = v; },
});
globalThis.document = { createElementNS: (_ns, tag) => make(tag) };
const { buildRevetWallGeometry } = await import("./B06_Section_UI_Cross_Wall.js");
const { buildRevetWallGeometry } = await import("./logic/B06_Section_Cross_Wall_Geom.js");
const { appendWallHatch } = await import("./B06_Section_UI_Cross_Wall_Hatch.js");
const walk = (node, out = []) => {
if (node.tag === "g" && node.attrs.class === "b06-chart__wall-drain") out.push(node);
@@ -80,6 +80,7 @@ def test_옹벽만_높이_15m_띠마다_파이프_하나(tmp_path: Path) -> None
"node",
str(TSC),
str(B06 / "B06_Section_UI_Cross_Wall.ts"),
str(B06 / "logic" / "B06_Section_Cross_Wall_Geom.ts"),
"--outDir",
str(out),
"--module",
@@ -121,7 +122,7 @@ def test_옹벽만_높이_15m_띠마다_파이프_하나(tmp_path: Path) -> None
def test_옹벽_표식은_구조물_종류로() -> None:
source = (B06 / "B06_Section_UI_Cross_Revetment.ts").read_text(encoding="utf-8")
source = (B06 / "logic" / "B06_Section_Cross_Revetment_Geom.ts").read_text(encoding="utf-8")
# 물빼기 표식 = 구조물 표 형상칸.weep(322-1) — 처음값은 옹벽만
assert "catalogRow(spec.type_id)?.shapeOpts.weep" in source
start = json.loads(
+1 -1
View File
@@ -20,7 +20,7 @@ DATA_DIR = ROOT / "resources" / "data_masonry"
GEOMETRY = [
"B06_Section_UI_Cross_Culvert_Geom.ts",
"B06_Section_UI_Cross_Wall.ts",
"logic/B06_Section_Cross_Wall_Geom.ts",
"B06_Section_UI_Cross_Culvert_Extra.ts",
"logic/B06_Section_UI_Cross_Culvert_Solve.ts",
]
@@ -21,7 +21,7 @@ TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
_RUNNER = """
import { writeFileSync } from "node:fs";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall.js";
import { buildRevetWallGeometry } from "./logic/B06_Section_Cross_Wall_Geom.js";
import { revetWallSpec, REVET_FORMS } from "./logic/B06_Section_UI_Cross_Culvert_Const.js";
const anchor = { offset: 5, elevation: 100 };
const outline = (form, role, extra = {}) =>
@@ -54,6 +54,7 @@ def test_벽_외곽_한_벌_높이_입력_그대로_노면_멈춤(tmp_path: Path
"node",
str(TSC),
str(B06 / "B06_Section_UI_Cross_Wall.ts"),
str(B06 / "logic" / "B06_Section_Cross_Wall_Geom.ts"),
"--outDir",
str(out),
"--module",
@@ -95,8 +96,12 @@ def test_벽_외곽_한_벌_높이_입력_그대로_노면_멈춤(tmp_path: Path
def test_네_벽이_공용_함수로_서고_형태_한도가_없다() -> None:
for name in ("Culvert_Geom", "Culvert_Extra", "Revetment"):
source = (B06 / f"B06_Section_UI_Cross_{name}.ts").read_text(encoding="utf-8")
for name in (
"B06_Section_UI_Cross_Culvert_Geom",
"B06_Section_UI_Cross_Culvert_Extra",
"logic/B06_Section_Cross_Revetment_Geom",
):
source = (B06 / f"{name}.ts").read_text(encoding="utf-8")
assert "buildRevetWallGeometry(" in source, f"{name} 가 공용 벽 함수를 안 씀"
for path in B06.rglob("*.ts"): # logic/ 까지(348-1)
source = path.read_text(encoding="utf-8")