fix(B06): ㄷ형 막음벽 미러·절/성토선, I형 관 시작 접지, 집수정 강조

2026-08-22 사용자 3건:
- ㄷ형 막음벽 = I형 벽을 바닥부 중심 기준 수직 반전(대칭오차 0.000).
  구조물 계류측 끝이 원지반 안이면 절토선(1:n), 밖이면 성토(되메움)선
  5°(임시값)로 지반 교차점까지(fillLine 추가).
- I형 관 시작 invert = 시작점 x(내공 1.0m)의 원지반(토피 상한 이내).
  종전 노견 x 평가로 13+15.7에서 관이 지반에 1.07m 파묻히던 문제 해소
  (현재 차 -0.016 = 토피 캡). 기슭막이 유입은 접점 0.000 정상 확인.
- 집수정 선택 강조(is-active) — 기슭막이와 같은 규칙, 부재 전체.
- 검증: 13+15.7 실조작 — I형/기슭막이 화면·수치, ㄷ형 미러·성토선,
  강조 토글, 형식 전환 세션 왕복.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 11:32:17 +09:00
co-authored by Claude Opus 5
parent 1774d61101
commit 11429654c1
5 changed files with 81 additions and 19 deletions
+25 -7
View File
@@ -109,6 +109,8 @@ export function appendCulvertOverlay(
...layout.walls.filter((w) => w.role === "inlet"),
];
const revetShapes = new Map<RevetKey, SVGPolygonElement>();
/** 집수정 부재 도형 — 유입 선택 강조에 쓴다(2026-08-22 사용자 ③). */
const basinShapes: SVGPolygonElement[] = [];
for (const wall of wallsInOrder) {
// 합성 단면(하부 사다리꼴 + 상부 평행사변형) — 상단 배면이 사면선 접점(사용자 ①·②).
const revetShape = polygon(
@@ -204,14 +206,14 @@ export function appendCulvertOverlay(
? "사용자 선택 집수정(규칙상 기슭막이 자리)"
: "유입측 성토사면 3m 이하 — 기슭막이 대신 집수정 기본";
for (const part of basin.parts) {
layer.append(
polygon(
part.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]),
"b06-chart__culvert-basin",
`유입 집수정(${shapeName}) ${part.kind === "floor" ? "바닥" : "벽"}` +
`${reasonText} · 내공 1.0m(실무 돌집수정), 벽은 기슭막이와 동일 단면`,
),
const shape = polygon(
part.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]),
"b06-chart__culvert-basin",
`유입 집수정(${shapeName}) ${part.kind === "floor" ? "바닥" : "벽"}` +
`${reasonText} · 내공 1.0m(실무 돌집수정) · 부재 두께 0.2m`,
);
basinShapes.push(shape);
layer.append(shape);
}
if (basin.cutLine) {
const cut = document.createElementNS(SVG_NS, "line");
@@ -227,6 +229,20 @@ export function appendCulvertOverlay(
cut.append(cutTitle);
layer.append(cut);
}
if (basin.fillLine) {
// 구조물이 원지반 밖으로 나온 경우의 성토(되메움) 계획선 — 경사 5도(임시값,
// 2026-08-22 사용자 지정).
const fill = document.createElementNS(SVG_NS, "line");
fill.setAttribute("x1", String(x(basin.fillLine.from.offset)));
fill.setAttribute("y1", String(toDisplayY(basin.fillLine.from.elevation)));
fill.setAttribute("x2", String(x(basin.fillLine.to.offset)));
fill.setAttribute("y2", String(toDisplayY(basin.fillLine.to.elevation)));
fill.setAttribute("class", "b06-chart__design-cross");
const fillTitle = document.createElementNS(SVG_NS, "title");
fillTitle.textContent = `집수정(${shapeName}) 성토(되메움)선 — 구조물이 원지반 밖으로 나와 5° 경사로 채움`;
fill.append(fillTitle);
layer.append(fill);
}
const label = document.createElementNS(SVG_NS, "text");
label.setAttribute("x", String(x(basin.label.offset)));
// 라벨은 I형 벽 **하단** 바깥에 붙인다(2026-08-20 사용자) — 글자 높이만큼 내린다.
@@ -333,5 +349,7 @@ export function appendCulvertOverlay(
// 강조는 클래스만 갈아 끼운다 — 카드를 다시 그리면 휠 줌·팬이 초기화된다.
return (key) => {
for (const [role, shape] of revetShapes) shape.classList.toggle("is-active", role === key);
// 유입이 집수정이면 부재 전체를 함께 강조한다(2026-08-22 사용자 ③).
for (const shape of basinShapes) shape.classList.toggle("is-active", key === "inlet");
};
}
@@ -54,8 +54,12 @@ export interface BasinBuildResult {
trimElevation: number;
}
/** 실무 내공 폭(m) — 울진 돌집수정. */
const INNER_WIDTH_M = 1.0;
/** 실무 내공 폭(m) — 울진 돌집수정. I형 관 시작점(노견+1.0m) 계산에도 쓴다. */
export const BASIN_INNER_WIDTH_M = 1.0;
const INNER_WIDTH_M = BASIN_INNER_WIDTH_M;
/** 집수정 주변 성토(되메움) 표면 경사(도) — 2026-08-22 사용자 지정 "일단 5도". */
const BASIN_FILL_ANGLE_DEG = 5;
/** 집수정 단면을 만든다. 형태별 규칙은 파일 머리말 참조. */
export function buildBasin(input: BasinBuildInput): BasinBuildResult {
@@ -112,19 +116,43 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
],
});
}
// ㄷ형 = ㄴ형 + 반대측(도로측) 막음벽(I형 수직반전 — 2026-08-22 사용자 확정).
// ㄷ형 = ㄴ형 + 막음벽 — I형 벽을 **바닥부 중심 기준 수직 반전**한 자리·형상
// (2026-08-22 사용자 ① — 종전에는 노견 쪽에 세워 방향이 틀렸다).
const floorCenter = (floorTopInner + floorOuter) / 2;
const mirroredWallBase = 2 * floorCenter - wallBase;
if (shape === "U") {
parts.push({ kind: "wall", points: wallOf(anchor.offset, -outward) });
parts.push({ kind: "wall", points: wallOf(mirroredWallBase, -outward) });
}
// ㄴ·ㄷ형이 원지반 **안쪽**에 박히면 그만큼 절토가 필요하다(2026-08-20 사용자 ①).
// 구조물 계류측 끝(ㄴ형 = 바닥 바깥 끝 상단, ㄷ형 = 막음벽 계류측 상단 꼭짓점)과
// 원지반의 관계(2026-08-22 사용자 ①):
// · 원지반 안쪽에 박히면 → 표준단면 절토경사(1:n)로 **절토선**
// · 원지반 밖으로 나오면 → **성토(되메움)선** — 경사 5도(임시값)로 지반까지.
let basinCut: BasinLayout["cutLine"] = null;
let basinFill: BasinLayout["cutLine"] = null;
if (shape !== "I") {
const outerTop = { offset: floorOuter, elevation: anchor.elevation };
if (groundAt(floorOuter) > anchor.elevation + 0.05) {
const refTop =
shape === "U"
? {
offset: mirroredWallBase + outward * REVET_LEAN_RATIO * wallHeight,
elevation: anchor.elevation + wallHeight,
}
: { offset: floorOuter, elevation: anchor.elevation };
const groundHere = groundAt(refTop.offset);
if (groundHere > refTop.elevation + 0.05) {
for (let h = 0.05; h <= 20; h += 0.05) {
const probe = floorOuter + outward * cutSlopeRatio * h;
if (groundAt(probe) <= anchor.elevation + h) {
basinCut = { from: outerTop, to: { offset: probe, elevation: anchor.elevation + h } };
const probe = refTop.offset + outward * cutSlopeRatio * h;
if (groundAt(probe) <= refTop.elevation + h) {
basinCut = { from: refTop, to: { offset: probe, elevation: refTop.elevation + h } };
break;
}
}
} else if (groundHere < refTop.elevation - 0.05) {
const dropPerM = Math.tan((BASIN_FILL_ANGLE_DEG * Math.PI) / 180);
for (let run = 0.05; run <= 30; run += 0.05) {
const probe = refTop.offset + outward * run;
const elevation = refTop.elevation - dropPerM * run;
if (elevation <= groundAt(probe)) {
basinFill = { from: refTop, to: { offset: probe, elevation } };
break;
}
}
@@ -141,6 +169,7 @@ export function buildBasin(input: BasinBuildInput): BasinBuildResult {
},
reason,
cutLine: basinCut,
fillLine: basinFill,
},
// 관 끝선 = I형 벽의 안쪽(물받는 쪽 = 내공측) 변 복사(2026-08-22 사용자 확정).
endFace: {
@@ -45,7 +45,7 @@ import type {
export * from "./B06_Section_UI_Cross_Culvert_Const";
export * from "./B06_Section_UI_Cross_Culvert_Types";
import { buildBasin } from "./B06_Section_UI_Cross_Culvert_Basin";
import { BASIN_INNER_WIDTH_M, buildBasin } from "./B06_Section_UI_Cross_Culvert_Basin";
import type { FillSlopeSegment, WallVertical } from "./B06_Section_UI_Cross_Culvert_Solve";
import {
clampToFace,
@@ -146,6 +146,12 @@ export function computeCulvertLayout(
// 벽 높이로만 노견에 연결한다(buildWall).
if (basinReason && basinShape !== "I") {
inlet.elevation = inletInfo.edge.elevation_m - BASIN_INNER_HEIGHT_M;
} else if (basinReason) {
// I형 — 관 시작점(내공 1.0m 자리) invert는 **그 x의 원지반 위**에 앉힌다(토피
// 상한 이내). 종전에는 노견 x의 원지반을 써서 지반이 오르내리는 자리에서 관이
// 지반에 파묻히거나 떴다(2026-08-22 사용자 ② — 13+15.7 확인).
const startOffset = inlet.offset + inletInfo.outward * BASIN_INNER_WIDTH_M;
inlet.elevation = Math.min(groundAt(startOffset), invertCap(inletInfo.edge));
}
// 성토측 유입 기슭막이는 노견에 붙여 두지 않는다 — 사면선이 벽 이음선 상단점을 지나야
@@ -77,6 +77,8 @@ export interface BasinLayout {
reason: "cut" | "short" | "manual";
/** 집수정이 원지반 안으로 들어갈 때의 절토선(구조물 바깥 끝 → 지반 교차점). */
cutLine: { from: OffsetPoint; to: OffsetPoint } | null;
/** 구조물이 원지반 밖으로 나올 때의 성토(되메움)선 — 경사 5도(임시), 지반 교차점까지. */
fillLine: { from: OffsetPoint; to: OffsetPoint } | null;
}
/** 관 끝단을 자를 마감면 — 구조물(기슭막이·집수정)의 계류측 변을 **그대로 복사**한다. */
@@ -233,7 +233,7 @@
opacity: 0.85;
}
/* 유입 집수정(ㄴ형) — 절토측 또는 원지반 30% 폐색 시 자동 반영되는 박스 단면 */
/* 유입 집수정 — 성토사면 ≤3m(절토 포함) 자동 또는 사용자 선택 단면 */
.b06-chart__culvert-basin {
fill: color-mix(in srgb, #8a7a5c 32%, transparent);
stroke: #6f6248;
@@ -241,6 +241,13 @@
stroke-linejoin: round;
}
/* 집수정 선택 강조(2026-08-22 사용자) — 기슭막이와 같은 규칙. */
.b06-chart__culvert-basin.is-active {
fill: color-mix(in srgb, var(--color-danger) 45%, transparent);
stroke: var(--color-danger);
stroke-width: 2.2;
}
/* 유출부 돌붙임 — 성토사면을 따라 원지반까지 덮는 돌붙임 띠(실무 도면의 돌 연쇄) */
.b06-chart__culvert-pitching {
fill: color-mix(in srgb, #3fa34d 18%, transparent);