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

This commit is contained in:
2026-10-06 10:08:41 +09:00
9 changed files with 223 additions and 9 deletions
@@ -88,6 +88,7 @@ export function structureHashParts(section: CrossSection): Array<string | number
spec.basin_length_m ?? "",
spec.basin_before_m ?? "",
spec.basin_after_m ?? "",
spec.basin_height_m ?? "",
].join(",");
const basin = section.design?.basin_adjust;
const revet = section.design?.revet_adjust;
+2
View File
@@ -207,6 +207,8 @@ export interface CulvertSideSpec {
/** 집수정 기준측점 전/후 몫(m) — 기슭막이와 같은 체계(2026-08-24). 기본 각 1m. */
basin_before_m?: number | null;
basin_after_m?: number | null;
/** 집수정 높이(m) — 좌측 폼 「높이」. 없으면 횡단도 자동(I형 노견 접점 · ㄴ·ㄷ형 내공 높이). */
basin_height_m?: number | null;
/** 기슭막이 전면 기울기(1:n) 종전값 — 그림은 `revetWallSpec().lean`(표준경사 표)을 씀. */
face_slope?: number;
/** 독립 기슭막이 기울기 판정 칸(저장 원본) — 설치 측 · 성토/절토 · 전면 기울기(B08 과 같은 칸). */
@@ -185,6 +185,8 @@ def _side_spec(
options.get("inlet_basin_after_m"),
_number(defaults.get("inlet_basin_after_m"), None),
)
# 집수정 높이(좌측 폼 · 비면 횡단도 자동) — 횡단도 집수정 벽 높이.
spec["basin_height_m"] = _number(options.get("inlet_basin_height_m"), None)
return spec
# 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(2026-09-14 · 옛 기본 2.5 걷음).
@@ -94,6 +94,8 @@ export interface BasinBuildInput {
* (2026-08-30 사용자 확정: 집수정은 그대로 두고 세월교만 세운다).
*/
leanRatio?: number;
/** 벽 높이(m) — 좌측 폼 집수정 「높이」. 없으면 I형 노견 접점 · ㄴ·ㄷ형 내공 높이. */
heightM?: number | null;
}
/** 집수정 구성 결과 — 본체가 트림·관 끝단면·관 시작점에 나눠 꽂는다. */
@@ -143,10 +145,12 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
const memberT = input.memberThicknessM ?? BASIN_MEMBER_THICKNESS_M;
const floorThickness = input.floorThicknessM ?? memberT;
// 벽 높이: I형은 노견 접점까지 성장, ㄴ·ㄷ형은 내부 높이 1.2m 고정(임시값).
// 폼 「높이」를 적으면 세 형태 모두 그 높이.
const wallHeight =
shape === "I"
input.heightM ??
(shape === "I"
? Math.max(diameterM + REVET_FREEBOARD_M, edge.elevation_m - anchor.elevation)
: adjust.innerHeightM;
: adjust.innerHeightM);
// 기움은 **수직 기준 반전** — 상단이 도로측(내공 쪽)으로 1:0.3 물러난다(2026-08-20).
// bottomElevation을 내리면 면 기울기를 그대로 연장한다(I형 근입용).
const defaultBottom = anchor.elevation - floorThickness;
@@ -2,7 +2,7 @@
* B06_Section_UI_Cross_Culvert_Geom.ts
* 배수관 세트(배관·기슭막이·성토부) **기하 계산** — 그리기(`_Cross_Culvert.ts`)와 분리.
* 백엔드 `section.culvert` 제원을 좌표로 옮긴다(치수 결정 금지). 기슭막이는 전면
* 1:n 평행사변형(n = 품셈 표준경사 · `leanOf`), 관 끝단면은 구조물 변과 평행, 성토 경사선은 벽 접점에서 끊는다
* 1:n 평행사변형(n = 품셈 표준경사 · `outlineLeanOf`), 관 끝단면은 구조물 변과 평행, 성토 경사선은 벽 접점에서 끊는다
* (designTrim). 유입 = 상단측. 집수정은 `_Basin.ts`, 성토부·다단은 `_Extra.ts`.
* ========================================================================== */
@@ -24,7 +24,7 @@ import {
revetTargetHeight,
} from "./B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
import { leanOf } from "./B06_Section_UI_Cross_Lean";
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
import type {
BasinLayout,
@@ -110,6 +110,8 @@ export function computeCulvertLayout(
};
const requestedBasin = basinAdjustment ?? DEFAULT_BASIN_ADJUST;
const basinAdjust = skipBasinClamp ? requestedBasin : clampBasinMove(requestedBasin, probeBasin);
const basinHeightM =
(culvert.inlet.basin_height_m ?? 0) > 0 ? culvert.inlet.basin_height_m : null;
// 좌표 규약: +offset = 좌측. 상단측 = 유입(미상이면 좌측 폴백).
const uphill = section.uphill_side ?? "left";
@@ -157,7 +159,7 @@ export function computeCulvertLayout(
const basinReason = culvert.hidden_pipe ? null : choiceReason;
// ㄴ·ㄷ형은 사이즈 유지한 채 통째로 노견 끝점 일치 — 바닥(=관 invert)이 따라 오른다.
if (basinReason && basinShape !== "I") {
inlet.elevation = inletInfo.edge.elevation_m - basinAdjust.innerHeightM;
inlet.elevation = inletInfo.edge.elevation_m - (basinHeightM ?? basinAdjust.innerHeightM);
} else if (basinReason) {
// I형 — 관 시작점(내공 1.0m 자리) invert = 그 x의 원지반(토피 상한 이내).
const startOffset =
@@ -172,10 +174,10 @@ export function computeCulvertLayout(
const adjustOf = (value?: WallAdjust): WallAdjust => ({ ...ZERO_ADJUST, ...(value ?? {}) });
const adjInlet = adjustOf(revetShift?.inlet);
const adjOutlet = adjustOf(revetShift?.outlet);
// 전면 기울기 = 표준경사 표(단면유형 × 설치 측 × 형태 × 높이 — B08 과 한 벌, 2026-09-14 B5).
// 전면 기울기 = 표준경사 표(단면유형 × 설치 측 × 높이) — 형태는 안 무늬만(외곽 같음).
const pipeWallSpec = (spec: CulvertSideSpec, adjust: WallAdjust) => {
const wall = revetWallSpec(spec, adjust, culvert.hidden_pipe === true, diameter);
const lean = leanOf({
const lean = outlineLeanOf({
form: wall.form,
height_m: wall.pureHeight,
section_mode: section.design?.section_mode,
@@ -343,6 +345,7 @@ export function computeCulvertLayout(
groundAt,
cutSlopeRatio: section.design?.cut_slope_ratio ?? 1.0,
adjust: basinAdjust,
heightM: basinHeightM,
});
basin = built.basin;
basinPipeEnd = built.pipeEnd;
@@ -624,9 +627,9 @@ export function computeCulvertLayout(
)
: null;
// 다단 벽 기울기 — 같은 측점·같은 설치 측 규칙으로 형태·높이마다 표준경사(B08 은 다단을 안 셈).
// 다단 벽 기울기 — 같은 측점·같은 설치 측 규칙으로 높이마다 표준경사(형태 무관 · B08 은 다단을 안 셈).
const extraLean = (form: string, pureHeightM: number): number =>
leanOf({
outlineLeanOf({
form,
height_m: pureHeightM,
section_mode: section.design?.section_mode,
+6
View File
@@ -11,8 +11,14 @@
import { type WallLeanInput, wallLeanRatio } from "../common_util/common_util_masonry_slope";
import masonrySlope from "../resources/data_masonry/masonry_slope_2026-01-01.json";
import { REVET_FORM_DEFAULT } from "./B06_Section_UI_Cross_Culvert_Const";
/** 벽 전면 기울기 n — 표를 못 고르면 종전 0.3. */
export function leanOf(input: WallLeanInput): number {
return wallLeanRatio(masonrySlope, input);
}
/** 배관 흙막이 외곽 기울기 — 형태와 무관(형태는 안 무늬만) · 기본 형태의 표준경사. */
export function outlineLeanOf(input: WallLeanInput): number {
return leanOf({ ...input, form: REVET_FORM_DEFAULT });
}
@@ -132,6 +132,7 @@ export function applyPipeOptionsToCache(
own(culvert.inlet, "basin_length_m", num("inlet_basin_length_m"));
own(culvert.inlet, "basin_before_m", num("inlet_basin_before_m"));
own(culvert.inlet, "basin_after_m", num("inlet_basin_after_m"));
own(culvert.inlet, "basin_height_m", num("inlet_basin_height_m"));
// 연장이 바뀌면 옆 측점 링크도 달라진다 — 그 구조물이 덮는 범위만 다시 그린다.
const reach = Math.max(
culvert.inlet.revet_before_m ?? 0,
+1
View File
@@ -108,6 +108,7 @@ function sideSpec(
num(defaults.inlet_basin_before_m, null),
);
spec.basin_after_m = num(options.inlet_basin_after_m, num(defaults.inlet_basin_after_m, null));
spec.basin_height_m = num(options.inlet_basin_height_m, null);
return spec;
}
@@ -0,0 +1,194 @@
/* 54-3 — B06 횡단도 배관 그림: 집수정 높이 → 형상 · 흙막이 여섯 형태 = 외곽 같음 · 안 무늬만 다름
* 시작 화면: B06 횡단설계(#/b06-section) · 검증 프로젝트(sub7_검증) · 목록에 「배관 1」(4+5)
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m543 = "단계";`
* basin → 유입 구조 집수정 · I · L · ㄷ형마다 높이 1.0 → 2.0 → 그림 높이(m, SVG 실좌표 ÷ 세로 눈금)
* soil → 유입 · 유출 흙막이 형태 여섯 → 벽 외곽 꼭짓점(SVG) 같음 · 무늬(해칭 묶음) 서로 다름
* extra → 추가 흙막이(다단) — 상세의 배관 측점마다 기하를 형태 여섯으로 돌려 외곽 꼭짓점 같음
* 쓰는 것: 좌측 폼 값(캐시만 · 저장 안 함) — 단계 끝에 원래 값으로 되돌림
* 기대 수치: basin 형태마다 h1 ≈ 1.0 · h2 ≈ 2.0 · ratio ≈ 2.0 / soil sameOutline true · distinctHatch 6 /
* extra sameLean true · sameOutlineAtSameHeight true(다단이 선 측점 1곳 이상) · heightCapped = 지형 맞춤 키가
* 형태 높이 한계에 걸린 측점(키만 다름)
*/
(async () => {
const MODE = window.__m543 || "basin";
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 20000) => {
for (let t = 0; t < ms; t += 200) {
if (fn()) return true;
await w(200);
}
return false;
};
const spec = () =>
[...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null);
const ctl = (c, item) =>
spec()
?.querySelector(`[data-spec-container="${c}"] [data-spec-item="${c}.${item}"]`)
?.querySelector("select, input");
const set = async (c, item, v) => {
const el = ctl(c, item);
el.value = v;
el.dispatchEvent(new Event("change", { bubbles: true }));
await w(900);
};
const pickPipe = async () => {
await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0);
if (spec()) return;
const li = [...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
/배관 1$/.test(l.textContent.trim()),
);
(li.querySelector("button") ?? li).click();
await until(() => !!spec());
await w(800);
};
const card = () => document.querySelector(".b06-cross-card--selected svg");
/** 세로 눈금 — 같은 x 에 놓인 숫자 글(표고)에서 1m 당 픽셀. */
const pxPerM = () => {
const nums = [...card().querySelectorAll("text")]
.filter((t) => /^-?\d+(\.\d+)?$/.test(t.textContent.trim()))
.map((t) => ({
v: Number(t.textContent),
x: t.getAttribute("x"),
y: Number(t.getAttribute("y")),
}));
const byX = {};
for (const n of nums) (byX[n.x] ??= []).push(n);
const col = Object.values(byX)
.filter((g) => new Set(g.map((n) => n.y)).size > 1)
.sort((a, b) => b.length - a.length)[0];
const a = col[0];
const b = col[col.length - 1];
return Math.abs(b.y - a.y) / Math.abs(b.v - a.v);
};
const pts = (poly) =>
poly
.getAttribute("points")
.trim()
.split(/\s+/)
.map((p) => p.split(",").map(Number));
const minY = (poly) => Math.min(...pts(poly).map((p) => p[1]));
const round = (v) => Math.round(v * 100) / 100;
const out = { mode: MODE };
await pickPipe();
if (MODE === "basin") {
const keep = ["structure", "form", "height"].map((k) => ctl("inlet", k).value);
await set("inlet", "structure", "집수정");
out.shapes = {};
for (const shape of ["I", "L", "U"]) {
await set("inlet", "form", shape);
const hs = [];
for (const h of ["1.0", "2.0"]) {
await set("inlet", "height", h);
const k = pxPerM();
const parts = [...card().querySelectorAll(".b06-chart__culvert-basin")];
const title = (p) => p.querySelector("title")?.textContent ?? "";
const wall = parts.find((p) => / 벽 /.test(title(p)));
const floor = parts.find((p) => / 바닥 /.test(title(p)));
const label = [...card().querySelectorAll(".b06-chart__culvert-label")].find((t) =>
t.textContent.startsWith("집수정"),
);
// ㄴ · ㄷ = 바닥 윗면 ~ 벽 상단 · I = 관 유입 기준(라벨 자리 + 0.2) ~ 벽 상단
const base = floor ? minY(floor) : Number(label.getAttribute("y")) - 11 - 0.2 * k;
hs.push({ name: title(wall).match(/\((.+?)\)/)?.[1], h: round((base - minY(wall)) / k) });
}
out.shapes[shape] = {
name: hs[0].name,
h1: hs[0].h,
h2: hs[1].h,
ratio: round(hs[1].h / hs[0].h),
};
}
await set("inlet", "structure", keep[0]);
await set("inlet", "form", keep[1]);
await set("inlet", "height", keep[2]);
out.restored = ["structure", "form", "height"].map((k) => ctl("inlet", k).value);
}
if (MODE === "soil") {
for (const [side, word] of [
["inlet", "유입"],
["outlet", "유출"],
]) {
// 구조가 비면 형태 칸이 안 섬 — 흙막이로 고른 뒤 끝에 되돌림
const keepStructure = ctl(side, "structure").value;
if (keepStructure !== "기슭막이") await set(side, "structure", "기슭막이");
const forms = [...ctl(side, "form").options].map((o) => o.value).filter(Boolean);
out.forms = forms;
const keep = ctl(side, "form").value;
const rows = [];
for (const form of forms) {
await set(side, "form", form);
const poly = [...card().querySelectorAll(".b06-chart__culvert-revet")].find((p) =>
(p.querySelector("title")?.textContent ?? "").startsWith(`${word} 기슭막이`),
);
const hatch = poly.nextElementSibling?.nextElementSibling;
rows.push({
form,
title: poly.querySelector("title").textContent.slice(0, 40),
outline: pts(poly)
.map((p) => p.map(round).join(","))
.join(" "),
hatch: `${hatch?.childElementCount}:${hatch?.innerHTML.length}`,
});
}
await set(side, "form", keep);
if (keepStructure !== "기슭막이") await set(side, "structure", keepStructure);
out[side] = {
sameOutline: rows.every((r) => r.outline === rows[0].outline),
distinctHatch: new Set(rows.map((r) => r.hatch)).size,
outline: rows[0].outline,
rows: rows.map((r) => [r.form, r.hatch, r.title]),
restored: ctl(side, "structure").value === keepStructure,
};
}
}
if (MODE === "extra") {
const modUrl = (part) =>
performance
.getEntriesByType("resource")
.map((e) => e.name)
.filter((n) => n.includes(part))
.pop();
const geom = await import(modUrl("/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts"));
const key = Object.keys(sessionStorage).find((k) => k.includes("section-detail"));
const detail = JSON.parse(sessionStorage.getItem(key));
const forms = geom.REVET_FORMS;
out.checked = 0;
out.sameLean = true;
out.sameOutlineAtSameHeight = true;
// 지형 맞춤 키는 형태 높이 한계(메 2.0 · 찰 3.0 · 콘 5.0 — 교본)에서 멈춤 — 같은 키끼리만 외곽 대조
out.heightCapped = [];
for (const section of detail.cross_sections ?? detail.value?.cross_sections ?? []) {
if (!section.culvert || section.culvert.hidden_pipe) continue;
const rows = forms.map((m) => {
const walls =
geom.computeCulvertLayout(section, section.samples, { extras: [{ m }, { m }] })
?.extraWalls ?? [];
return {
m,
lean: walls.map((wall) => wall.lean).join(","),
height: walls.map((wall) => round(wall.height)).join(","),
outline: JSON.stringify(
walls.map((wall) => wall.points.map((p) => [round(p.offset), round(p.elevation)])),
),
};
});
if (rows[0].outline === "[]") continue;
out.checked += 1;
if (!rows.every((r) => r.lean === rows[0].lean)) out.sameLean = false;
for (const r of rows) {
const twin = rows.find((o) => o.height === r.height);
if (twin.outline !== r.outline) out.sameOutlineAtSameHeight = false;
}
const heights = [...new Set(rows.map((r) => r.height))];
if (heights.length > 1)
out.heightCapped.push({
chainage: section.chainage_m,
byForm: rows.map((r) => `${r.m}=${r.height}`),
});
}
}
return JSON.stringify(out);
})();