Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user