Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1

This commit is contained in:
2026-09-08 21:37:01 +09:00
9 changed files with 428 additions and 0 deletions
@@ -787,6 +787,15 @@
"required": false,
"phase": "detail"
},
{
"key": "foundation",
"label": "기초",
"input": "select",
"choices": ["기초유", "기초버림"],
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
@@ -878,6 +887,15 @@
"required": false,
"phase": "detail"
},
{
"key": "foundation",
"label": "기초",
"input": "select",
"choices": ["기초유", "기초버림"],
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
@@ -1034,6 +1052,15 @@
"required": false,
"phase": "detail"
},
{
"key": "foundation",
"label": "기초",
"input": "select",
"choices": ["기초유", "기초버림"],
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "side",
"label": "설치 측",
@@ -1147,6 +1174,15 @@
"required": false,
"phase": "detail"
},
{
"key": "foundation",
"label": "기초",
"input": "select",
"choices": ["기초유", "기초버림"],
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "length_m",
"label": "길이",
@@ -1200,6 +1236,15 @@
"required": false,
"phase": "detail"
},
{
"key": "foundation",
"label": "기초",
"input": "select",
"choices": ["기초유", "기초버림"],
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "area_m2",
"label": "면적",
@@ -1316,6 +1361,15 @@
"required": false,
"phase": "detail"
},
{
"key": "foundation",
"label": "기초",
"input": "select",
"choices": ["기초유", "기초버림"],
"default": null,
"required": true,
"phase": "detail"
},
{
"key": "height_m",
"label": "높이",
+3
View File
@@ -233,6 +233,9 @@ export interface CulvertSet {
side?: string;
/** 독립 기슭막이 다단 요청 수(1이면 단일 벽). */
tiers?: number;
/** 기초 축 — "기초유" | "기초버림"(실무 정본 탭 제목). **비어 있으면 터파기를 안 그린다**
* — 고르기 전에는 근거가 없다(2026-09-09 사용자 확정 ④). */
foundation?: string | null;
}
/** 세월교 날개벽 한쪽 — 횡단면엔 안 보이고 바닥판 연장량만 넘긴다. */
@@ -263,6 +263,10 @@ def _revet_set(options: dict[str, Any] | None) -> dict[str, Any]:
"hidden_pipe": True,
"side": str(values.get("side") or "양쪽"),
"tiers": int(_number(values.get("tiers"), 1.0) or 1),
# 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다. 비어 있으면 안 그린다.
"foundation": (str(values["foundation"]).strip() or None)
if values.get("foundation") is not None
else None,
"pipe_kind": None,
"diameter_m": 0.3,
"min_cover_m": 0.0,
@@ -23,6 +23,11 @@ import type {
PipeEnd,
WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Geom";
import {
appendPipeTrench,
appendWallTrench,
foundationChoice,
} from "./B06_Section_UI_Cross_Excavation";
import { appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
// 기하 계산 진입점과 공개 상수·타입은 여기서 재수출한다 — B05(최소 토피)와 횡단 뷰가
@@ -162,6 +167,12 @@ export function appendCulvertOverlay(
const revetShapes = new Map<RevetKey, SVGPolygonElement>();
/** 집수정 부재 도형 — 유입 선택 강조에 쓴다(2026-08-22 사용자 ③). */
const basinShapes: SVGPolygonElement[] = [];
// 터파기는 구조물보다 **먼저** 그린다 — 파선이 벽 밑에 깔려야 도면처럼 보인다.
// 저장 제원 `foundation`(기초유/기초버림)이 비어 있으면 그리지 않는다 — 근거가 없다.
const wallFoundation = foundationChoice(culvert.foundation);
for (const { wall } of wallsInOrder) {
appendWallTrench(layer, wall, wallFoundation, x, toDisplayY);
}
for (const { wall, key: wallKey } of wallsInOrder) {
// 합성 단면(하부 사다리꼴 + 상부 평행사변형) — 상단 배면이 사면선 접점(사용자 ①·②).
const revetShape = polygon(
@@ -272,6 +283,26 @@ export function appendCulvertOverlay(
const inletFinal = pushOut(pipeCorners.inlet, -1);
const outletFinal = pushOut(pipeCorners.outlet, 1);
// 관 터파기 — 표값 폭으로 관 위 지반선에서 관 바닥까지 수직(KCS 44 40 10 그림 3.2-1).
if (!hidePipe) {
const invert = Math.min(
pipeCorners.inlet.bottom.elevation,
pipeCorners.outlet.bottom.elevation,
);
const centerOffset = (pipeCorners.inlet.bottom.offset + pipeCorners.outlet.bottom.offset) / 2;
appendPipeTrench(
layer,
{
centerOffsetM: centerOffset,
invertM: invert,
topM: invert + diameter + culvert.min_cover_m,
diameterMm: Math.round(diameter * 1000),
},
x,
toDisplayY,
);
}
const kindLabel = culvert.pipe_kind ? `${culvert.pipe_kind} ` : "";
const wallThickness = pipeWallThicknessM(culvert.pipe_kind, diameter);
const tip =
@@ -0,0 +1,119 @@
/* =============================================================================
* B06_Section_UI_Cross_Excavation.ts
* 횡단도에 **터파기 선**을 얹는다 — 관·벽 두 갈래(2026-09-09 사용자 확정 ④).
*
* 왜 있나 — 「터파기가 현재 횡단도에서 표현이 안 된다」가 사용자 지적이었다. 수량을
* 세기 전에 **도면에 그것이 보여야** 한다.
*
* ⚠ 근거의 급이 다르므로 **노티스를 갈라 적는다** — 값은 `common_util_excavation` 한 곳에서
* 온다(파이썬 짝과 거울 시험으로 묶여 있다). 여기서는 **그리기만** 한다.
* 관 : 법정 시방서 표(KCS 44 40 10 그림 3.2-1)
* 벽 : 법정 근거 없음 — 실무 정본 식(구조도 기슭막이 xls)
* ⚠ **비탈을 주지 않는다 — 수직으로 그린다.** 정본 식에 기울기 항이 없다.
* ========================================================================== */
import {
BASIS_PIPE,
BASIS_WALL,
WALL_BLINDING_DEPTH_M,
WALL_BLINDING_WIDTH_M,
WALL_FOUNDATION_DEPTH_M,
WALL_FOUNDATION_WIDTH_M,
WALL_TRENCH_CLEARANCE_M,
pipeTrenchWidthM,
} from "@util/common_util_excavation";
import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
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(" "));
line.setAttribute("class", "b06-chart__excavation");
const title = document.createElementNS(SVG_NS, "title");
title.textContent = tooltip;
line.append(title);
layer.append(line);
}
/**
* 관 터파기 — **표값 폭**으로 관 위 지반선에서 관 바닥까지 수직으로 판다.
* 표에 없는 관경이면 **아무것도 그리지 않는다**(폭을 지어내지 않는다).
*/
export function appendPipeTrench(
layer: SVGElement,
input: {
/** 관 중심 offset(m). */
centerOffsetM: number;
/** 관 바닥(invert) 표고. */
invertM: number;
/** 터파기 윗면 표고 — 관 위 지반(또는 노면)선. */
topM: number;
diameterMm: number | null | undefined;
},
x: (offset: number) => number,
toDisplayY: (elevation: number) => number,
): number | null {
const width = pipeTrenchWidthM(input.diameterMm);
if (width === null || !(input.topM > input.invertM)) return null;
const half = width / 2;
const left = input.centerOffsetM - half;
const right = input.centerOffsetM + half;
trenchPath(
layer,
[
[x(left), toDisplayY(input.topM)],
[x(left), toDisplayY(input.invertM)],
[x(right), toDisplayY(input.invertM)],
[x(right), toDisplayY(input.topM)],
],
`관 터파기 폭 ${width.toFixed(2)}m · 깊이 ${(input.topM - input.invertM).toFixed(2)}m\n` +
`Φ${Math.round(Number(input.diameterMm))}㎜ — ${BASIS_PIPE}`,
);
return width;
}
/**
* 벽 터파기 — 벽 바닥선 아래로 **수직**. 폭은 바닥 폭 + 여유 0.2, 깊이는 기초 몫.
* `foundation` 이 비어 있으면 **그리지 않는다** — 사용자가 고르기 전에는 근거가 없다.
*/
export function appendWallTrench(
layer: SVGElement,
wall: WallLayout,
foundation: "기초유" | "기초버림" | null,
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;
trenchPath(
layer,
[
[x(left), toDisplayY(top)],
[x(left), toDisplayY(bottom)],
[x(right), toDisplayY(bottom)],
[x(right), toDisplayY(top)],
],
`${foundation} 터파기 폭 ${width.toFixed(2)}m · 깊이 ${depth.toFixed(2)}m (수직)\n` +
`품셈에 규정이 없어 실무 관행으로 정한 값 — ${BASIS_WALL}`,
);
return { widthM: width, depthM: depth };
}
@@ -379,3 +379,12 @@
stroke-dasharray: 4 3;
stroke-linejoin: round;
}
/* 터파기 선(2026-09-09 사용자 확정 ④) — **파선**으로 그린다. 구조물 실체가 아니라
「여기를 판다」는 표시이고, 근거의 급(법정 표 / 실무 관행)은 말풍선에 적힌다. */
.b06-chart__excavation {
fill: none;
stroke: #8a5c3c;
stroke-dasharray: 4 3;
stroke-width: 1;
}
+2
View File
@@ -174,6 +174,8 @@ function revetSet(values: Record<string, unknown>): CulvertSetSpec {
hidden_pipe: true,
side: String(values.side ?? "양쪽"),
tiers: Math.trunc(num(values.tiers, 1.0) || 1),
// 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다. 비어 있으면 안 그린다.
foundation: text(values.foundation),
pipe_kind: null,
diameter_m: 0.3,
min_cover_m: 0.0,
+118
View File
@@ -0,0 +1,118 @@
"""터파기 단면 — 관·벽 두 갈래. **근거의 급이 다르므로 갈라 둔다.**
TS 파일과 ** **이다 `common_util/common_util_excavation.ts`.
거울 시험: `tmp/tests/test_excavation_mirror.py` (고칠 같이 돌릴 ).
화면(B06 횡단도) 그리고 서버(B08 수량) 세는 값이라 쪽에 같은 수가 있어야 한다.
**법정 시방서에 표가 있다**
`KCS 44 40 10 배수관(23.01) §3.2(4) 그림 3.2-1`.
설계도서에 따로 표기되지 않았을 때에는 그림 3.2-1 터파기 기준 폭에 따라야 한다
기본값으로 쓰고 사용자가 덮어쓰는 구조가 원문과 맞다.
**관경 + 2b 계산하지 않는다** 표가 두께·작업여유를 이미 담고 있다
(Ø800 800+2×300=1400 아니라 **1600**).
여유폭 b 같은 그림의 : 보통지반 0.30 m · 연약지반 0.50 m(한쪽).
(돌쌓기·기슭막이) **법정 근거 없음. 실무 정본 식이 있다**
법령·교본은 설계도서 지시 ·기울기·깊이까지만 정한다
(지식DB `01_임도/02_상세설계/구조물/구조물_기초_토공.md` §1 전수 확인).
실무 정본 `original/구조물도/기슭막이/04.구조도(기슭막이).xls` 7:
터파기 = 기초 0.5 × (0.7 + 0.2) × 1.0 = 0.45 (버림만 있는 탭은 0.1 × 0.7 = 0.07)
+ 비탈 H × (평균두께 + 0.2) × 1.0
**여유폭은 +0.2**(평균두께에 더함), **기초는 깊이 0.5 · 0.9**.
정본 제목이 **기초유 / 기초버림**이다 기초를 두는 것이 아니라
**버림으로 얕게 두는 **이라 뜻이 다르다. 축이 **0.45 0.07 이냐** 가른다.
**비탈을 주지 않는다 정본이 수직으로 센다.** 기울기 항이 식에 아예 없다.
임의로 기울기를 주면 그림과 물량이 어긋난다.
값을 지어내지 않는다 여기 있는 수는 전부 출처의 것이고, 사용자가 덮어쓰면
값이 이긴다. 화면에는 **근거의 **(법정 / 실무 ) 노티스로 드러낸다.
"""
from __future__ import annotations
#: 관경(㎜) → 터파기 폭(㎜). `KCS 44 40 10` 그림 3.2-1 표 그대로.
PIPE_TRENCH_WIDTH_MM: dict[int, int] = {
300: 700,
350: 800,
400: 900,
450: 1000,
500: 1100,
600: 1200,
700: 1400,
800: 1600,
900: 1700,
1000: 1800,
1100: 2000,
1200: 2100,
1300: 2200,
1400: 2400,
1500: 2500,
}
#: 한쪽 여유폭(m) — 같은 그림. 표에 없는 관경을 사용자가 넣었을 때의 안내용이다.
PIPE_TRENCH_CLEARANCE_NORMAL_M = 0.30
PIPE_TRENCH_CLEARANCE_SOFT_M = 0.50
#: 벽 터파기 — 실무 정본 식의 수(위 문서 참조).
WALL_TRENCH_CLEARANCE_M = 0.2 # 평균두께에 더하는 여유
WALL_FOUNDATION_DEPTH_M = 0.5 # 기초 깊이
WALL_FOUNDATION_WIDTH_M = 0.9 # 기초 폭 (0.7 + 0.2)
WALL_BLINDING_DEPTH_M = 0.1 # 기초 없이 버림만 있을 때의 깊이
WALL_BLINDING_WIDTH_M = 0.7 # 그때의 폭 (여유폭 0.0 — 버림 탭에는 여유가 없다)
#: 근거의 급 — 화면 노티스가 이 값을 그대로 쓴다.
BASIS_PIPE = "법정 표(KCS 44 40 10 그림 3.2-1) — 설계도서에 따로 없으면 이 값"
BASIS_WALL = "법정 근거 없음 · 실무 정본 식(구조도 기슭막이 xls) 기본값 — 확인 후 쓸 것"
def pipe_trench_width_m(diameter_mm: float | int | None) -> float | None:
"""관경(㎜)에 대응하는 터파기 폭(m). 표에 없으면 `None`(지어내지 않는다)."""
if diameter_mm is None:
return None
try:
key = int(round(float(diameter_mm)))
except (TypeError, ValueError):
return None
width = PIPE_TRENCH_WIDTH_MM.get(key)
return None if width is None else width / 1000.0
def wall_trench_area_m2(
height_m: float | None,
average_thickness_m: float | None,
*,
has_foundation: bool,
) -> float | None:
"""벽 터파기 단면적(㎡/m). 높이나 평균두께가 없으면 `None`.
실무 정본 그대로: `기초분 + H × (평균두께 + 0.2)`.
`has_foundation=False` 기초분이 **버림 (0.1 × 0.7)**으로 줄어든다.
"""
if height_m is None or average_thickness_m is None:
return None
try:
height = float(height_m)
thickness = float(average_thickness_m)
except (TypeError, ValueError):
return None
if height <= 0 or thickness <= 0:
return None
base = (
WALL_FOUNDATION_DEPTH_M * WALL_FOUNDATION_WIDTH_M
if has_foundation
else WALL_BLINDING_DEPTH_M * WALL_BLINDING_WIDTH_M
)
return base + height * (thickness + WALL_TRENCH_CLEARANCE_M)
def wall_trench_width_m(average_thickness_m: float | None) -> float | None:
"""벽 터파기 폭(m) — 평균두께 + 여유 0.2. 값이 없으면 `None`."""
if average_thickness_m is None:
return None
try:
thickness = float(average_thickness_m)
except (TypeError, ValueError):
return None
return None if thickness <= 0 else thickness + WALL_TRENCH_CLEARANCE_M
+88
View File
@@ -0,0 +1,88 @@
/* =============================================================================
* common_util_excavation.ts
* · . ** .**
*
* ** ** `common_util/common_util_excavation.py`.
* : `tmp/tests/test_excavation_mirror.py`. .
* (B06 ) (B08 ) .
*
* : `KCS 44 40 10 배수관(23.01) §3.2(4) 그림 3.2-1`.
*
* ** + 2b ** · .
* . (`구조도(기슭막이).xls` 7):
* = 0.5 × 0.9 ( 0.1 × 0.7) + H × ( + 0.2)
* ** .** / .
* ========================================================================== */
/** 관경(㎜) → 터파기 폭(㎜). KCS 44 40 10 그림 3.2-1 표 그대로. */
export const PIPE_TRENCH_WIDTH_MM: Record<number, number> = {
300: 700,
350: 800,
400: 900,
450: 1000,
500: 1100,
600: 1200,
700: 1400,
800: 1600,
900: 1700,
1000: 1800,
1100: 2000,
1200: 2100,
1300: 2200,
1400: 2400,
1500: 2500,
};
/** 한쪽 여유폭(m) — 같은 그림. 표에 없는 관경을 넣었을 때의 안내용. */
export const PIPE_TRENCH_CLEARANCE_NORMAL_M = 0.3;
export const PIPE_TRENCH_CLEARANCE_SOFT_M = 0.5;
/** 벽 터파기 — 실무 정본 식의 수. */
export const WALL_TRENCH_CLEARANCE_M = 0.2;
export const WALL_FOUNDATION_DEPTH_M = 0.5;
export const WALL_FOUNDATION_WIDTH_M = 0.9;
export const WALL_BLINDING_DEPTH_M = 0.1;
export const WALL_BLINDING_WIDTH_M = 0.7;
/** 근거의 급 — 화면 노티스가 이 문구를 그대로 쓴다. */
export const BASIS_PIPE = "법정 표(KCS 44 40 10 그림 3.2-1) — 설계도서에 따로 없으면 이 값";
export const BASIS_WALL =
"법정 근거 없음 · 실무 정본 식(구조도 기슭막이 xls) 기본값 — 확인 후 쓸 것";
/** 관경(㎜)에 대응하는 터파기 폭(m). 표에 없으면 `null`(지어내지 않는다). */
export function pipeTrenchWidthM(diameterMm: number | null | undefined): number | null {
if (diameterMm === null || diameterMm === undefined) return null;
const key = Math.round(Number(diameterMm));
if (!Number.isFinite(key)) return null;
const width = PIPE_TRENCH_WIDTH_MM[key];
return width === undefined ? null : width / 1000;
}
/**
* (/m). · `null`.
* `hasFoundation=false` (0.1 × 0.7) .
*/
export function wallTrenchAreaM2(
heightM: number | null | undefined,
averageThicknessM: number | null | undefined,
hasFoundation: boolean,
): number | null {
if (heightM === null || heightM === undefined) return null;
if (averageThicknessM === null || averageThicknessM === undefined) return null;
const height = Number(heightM);
const thickness = Number(averageThicknessM);
if (!Number.isFinite(height) || !Number.isFinite(thickness)) return null;
if (height <= 0 || thickness <= 0) return null;
const base = hasFoundation
? WALL_FOUNDATION_DEPTH_M * WALL_FOUNDATION_WIDTH_M
: WALL_BLINDING_DEPTH_M * WALL_BLINDING_WIDTH_M;
return base + height * (thickness + WALL_TRENCH_CLEARANCE_M);
}
/** 벽 터파기 폭(m) — 평균두께 + 여유 0.2. 값이 없으면 `null`. */
export function wallTrenchWidthM(averageThicknessM: number | null | undefined): number | null {
if (averageThicknessM === null || averageThicknessM === undefined) return null;
const thickness = Number(averageThicknessM);
if (!Number.isFinite(thickness) || thickness <= 0) return null;
return thickness + WALL_TRENCH_CLEARANCE_M;
}