feat(B06): 기슭막이 높이·자리 체계 개편 + 조정창 방향키 조작
- 조정창 십자 키패드 키보드 방향키 조작(집수정 9키는 십자 축만) - 배관 기슭막이 높이 = 순수 높이(바닥~상단), 하한·기본 = 관경+1.0m (고정 2.0m 폐기) - 매몰 시 전면 발끝 지반 추적 바닥 심화 삭제 — 바닥 = invert-0.5 고정 - 상하(d) 0점 = 성토선 0 지점(벽 상단=노견), 기본 자리 = 성토사면 경사길이 4m 지점 (d=null 자동, 저장·표시·정수맞춤 pinned 판정 연동, 구세션 d=0 호환) - 유입 접속 절토선: 지반 교차 없으면 1m 공간 확보에서 끝 - 자동 집수정 기본 형상 L형 -> I형 (선택지 밖 구조물 그려지던 불일치) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,7 @@ export function appendCulvertOverlay(
|
||||
planLine(
|
||||
layout.inletFill.points,
|
||||
layout.inletFill.kind === "cut"
|
||||
? `유입부 접속 절토선 — 관 하단에서 3도(계류 쪽 오름) 1m 후 표준 절토 경사로 원지반까지 (연장 ${layout.inletFill.lengthM.toFixed(2)}m)`
|
||||
? `유입부 접속선 — 관 하단에서 3도(계류 쪽 오름) 1m 공간 확보에서 끝 (연장 ${layout.inletFill.lengthM.toFixed(2)}m)`
|
||||
: `유입부 3도 성토선(계류 쪽 오름 — 물이 관으로) — 원지반 교차에서 정지 (연장 ${layout.inletFill.lengthM.toFixed(2)}m)`,
|
||||
);
|
||||
}
|
||||
@@ -168,7 +168,10 @@ export function appendCulvertOverlay(
|
||||
const revetShape = polygon(
|
||||
wall.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]),
|
||||
"b06-chart__culvert-revet",
|
||||
`${roleLabel(wall.role)} 기슭막이 ${wall.form ?? ""} H=${wall.height.toFixed(1)}m` +
|
||||
// 배관 벽 높이 표기 = 순수 높이(바닥~상단, 관 아래 0.5 포함 — 2026-08-23 사용자).
|
||||
`${roleLabel(wall.role)} 기슭막이 ${wall.form ?? ""} H=${(
|
||||
wall.height + (wall.role === "extra" ? 0 : REVET_EMBED_DEPTH_M)
|
||||
).toFixed(1)}m` +
|
||||
`(상단 = 사면선 접점, 전면 1:${REVET_LEAN_RATIO}` +
|
||||
`, 높이 한계 ${revetHeightLimit(wall.form).toFixed(1)}m — 교본 7-3)` +
|
||||
(wall.floatGapM > 0.01
|
||||
|
||||
@@ -36,7 +36,9 @@ export function resolveBasinChoice(
|
||||
const selected = choice === "I" || choice === "L" || choice === "U";
|
||||
return {
|
||||
reason: choice === "revet" ? null : selected ? (ruleReason ?? "manual") : ruleReason,
|
||||
shape: selected ? choice : "L",
|
||||
// 자동(기본) 집수정은 **I형**(2026-08-23 사용자) — 종전 L형 기본은 ㄴ·ㄷ형이
|
||||
// 선택지에서 숨는 지형(basinLUAllowed=false)에서 선택지 밖 구조물이 그려졌다.
|
||||
shape: selected ? choice : "I",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -89,8 +89,9 @@ export function revetHeightLimit(form: string | null | undefined): number {
|
||||
/* ── 기슭막이 재질·높이 조작(2026-08-22 사용자 확정) ─────────────────────
|
||||
* 높이 조작이 교본 형태별 한계와 부딪히면 **재질을 바꿔야** 더 올릴 수 있다:
|
||||
* 메쌓기 2.0 / 찰쌓기 3.0(돌쌓기.md §1) / 콘크리트 5.0(프로젝트 기본값,
|
||||
* 외부 기준 미확인). 높이 기준은 근입
|
||||
* 0.5m 위 기준선(관 invert 자리)~상단의 **계산용 높이**로 종전과 동일. */
|
||||
* 외부 기준 미확인). 배관 기슭막이의 높이 제어·표기·한계 기준은 **순수 높이**
|
||||
* (벽 바닥~상단, 관 아래 0.5m 포함 — 2026-08-23 사용자 확정). 일반(추가)
|
||||
* 기슭막이는 종전 계산용 높이(근입 0.5 위 기준선~상단) 기준을 유지한다. */
|
||||
export type RevetMaterial = "dry" | "wet" | "concrete";
|
||||
|
||||
export const REVET_MATERIALS: RevetMaterial[] = ["dry", "wet", "concrete"];
|
||||
@@ -114,10 +115,15 @@ export function materialFromForm(form: string | null | undefined): RevetMaterial
|
||||
* 따른다: 유입측은 계류 쪽이 높고(물이 관으로), 유출측은 계류 쪽이 낮다(관에서 밖으로). */
|
||||
export const PIPE_CONNECT_GRADE = Math.tan((3 * Math.PI) / 180);
|
||||
|
||||
/** 배관 기슭막이 높이 조작 하한(m) — 2~3m 구간 지시(2026-08-22)의 하한. */
|
||||
export const PIPE_WALL_MIN_HEIGHT_M = 2.0;
|
||||
/** 배관 기슭막이 기본 높이(m) — 재질과 무관하게 2.0(2026-08-22 사용자 정정). */
|
||||
export const PIPE_WALL_DEFAULT_HEIGHT_M = 2.0;
|
||||
/**
|
||||
* 배관 기슭막이 관 하부 여유(m) — 관 invert 아래 벽 바닥까지의 깊이(2026-08-23
|
||||
* 사용자 확정: 배관 기준 아래 0.5·위 0.5 여유). 값은 근입 깊이와 같은 0.5라
|
||||
* 형상은 종전과 동일하고, **순수 높이 하한 = 관경 + 위 0.5 + 아래 0.5**가 된다.
|
||||
* 종전 고정 하한·기본 2.0m(관경 무관)는 폐기.
|
||||
*/
|
||||
export function pipeWallMinPureHeight(diameterM: number): number {
|
||||
return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;
|
||||
}
|
||||
/** 일반(추가) 기슭막이 높이 조작 하한(m) — 프로젝트 기본값 0.5m. */
|
||||
export const EXTRA_WALL_MIN_HEIGHT_M = 0.5;
|
||||
/** 일반(추가) 기슭막이 기본 높이(m). */
|
||||
@@ -136,6 +142,17 @@ export const FILL_SLOPE_RATIO_MAX = 2.0;
|
||||
/** 성토사면 길이 한계(m) — `성토_비탈면.md` §2 "5m 이내". 초과 시 옹벽·석축 등 의무. */
|
||||
export const FILL_SLOPE_MAX_LENGTH_M = 5.0;
|
||||
|
||||
/**
|
||||
* 배관 기슭막이 기본 자리 — 노견에서 **성토사면 경사길이 4m 지점**(2026-08-23 사용자
|
||||
* 확정). 상하(d) 조작의 0점은 성토선 길이 0 지점(벽 상단 = 노견)이고, d는 그 점에서
|
||||
* 1:1.2 성토선을 타고 내려간 수평 성분(m)이다. 기본 자리가 안 되는 지형(매몰·역경사·
|
||||
* 토피)은 배치 풀이의 기존 한계 논리가 안쪽으로 되돌린다.
|
||||
*/
|
||||
export const PIPE_WALL_DEFAULT_SLOPE_M = 4.0;
|
||||
/** 기본 자리(경사길이 4m)의 수평 성분(m) — d 기본값. */
|
||||
export const PIPE_WALL_DEFAULT_RUN_M =
|
||||
(PIPE_WALL_DEFAULT_SLOPE_M * FILL_SLOPE_RATIO_MIN) / Math.hypot(1, FILL_SLOPE_RATIO_MIN);
|
||||
|
||||
/**
|
||||
* 성토 전체고(노견 − 원지반) 기준(m) — 이 높이를 넘으면 1:1.2로도 사면이 5m를 넘어
|
||||
* 구조물이 필요해진다(3m × √(1+1.2²) = 4.69m, 3.2m에서 5.0m). 기슭막이를 자연 사면
|
||||
|
||||
@@ -85,6 +85,7 @@ export function slopedCrossing(
|
||||
* 유입 기슭막이의 관 시작 접속선(2026-08-22 사용자 ②).
|
||||
* · 관 하단점이 원지반보다 **위**: 0도 성토선 — 지반과 만나는 곳에서 정지.
|
||||
* · **아래**: 0도 직선 1m 후 표준 절토 경사(1:n)로 지반까지 올라가는 절토선.
|
||||
* 지반 교차가 안 나오는 지형은 1m 공간 확보에서 끝(2026-08-23 사용자).
|
||||
*/
|
||||
export function inletGroundConnector(
|
||||
corner: OffsetPoint,
|
||||
@@ -125,13 +126,15 @@ export function inletGroundConnector(
|
||||
};
|
||||
}
|
||||
if (corner.elevation < ground - 0.02) {
|
||||
// 3도 직선 1m(계류 쪽이 높다) 후 표준 절토 경사.
|
||||
// 3도 직선 1m(계류 쪽이 높다) 후 표준 절토 경사로 지반까지. 교차가 안
|
||||
// 나오면 절토선 없이 **1m 공간 확보에서 끝**(2026-08-23 사용자 — 종전에는
|
||||
// 샘플 한계까지 올라가는 긴 선이 남았다).
|
||||
const bend: OffsetPoint = {
|
||||
offset: corner.offset + outward * 1.0,
|
||||
elevation: corner.elevation + PIPE_CONNECT_GRADE * 1.0,
|
||||
};
|
||||
let last = bend;
|
||||
const cutSpan = (limitOffset - bend.offset) * outward;
|
||||
let cross: OffsetPoint | null = null;
|
||||
let previousGap = groundAt(bend.offset) - bend.elevation; // 지반 − 절토선
|
||||
for (let t = 0.25; t <= Math.max(cutSpan, 0) + 1e-9; t += 0.25) {
|
||||
const x = bend.offset + outward * t;
|
||||
@@ -140,18 +143,19 @@ export function inletGroundConnector(
|
||||
if (gap <= 0) {
|
||||
const fraction = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
|
||||
const tCross = t - 0.25 * (1 - fraction);
|
||||
last = {
|
||||
cross = {
|
||||
offset: bend.offset + outward * tCross,
|
||||
elevation: bend.elevation + tCross / Math.max(cutSlopeRatio, 0.1),
|
||||
};
|
||||
break;
|
||||
}
|
||||
last = { offset: x, elevation };
|
||||
previousGap = gap;
|
||||
}
|
||||
return {
|
||||
points: [corner, bend, last],
|
||||
lengthM: 1 + Math.hypot(last.offset - bend.offset, last.elevation - bend.elevation),
|
||||
points: cross ? [corner, bend, cross] : [corner, bend],
|
||||
lengthM: cross
|
||||
? 1 + Math.hypot(cross.offset - bend.offset, cross.elevation - bend.elevation)
|
||||
: 1,
|
||||
overLimit: false,
|
||||
kind: "cut",
|
||||
};
|
||||
@@ -217,7 +221,6 @@ function buildExtraWall(
|
||||
height: number,
|
||||
material: RevetMaterial,
|
||||
floatGapM: number,
|
||||
groundAt: (offset: number) => number,
|
||||
): WallLayout {
|
||||
const thickness = REVET_THICKNESS_M;
|
||||
const baseWidth = thickness * 1.5 + REVET_LEAN_RATIO * height;
|
||||
@@ -228,16 +231,10 @@ function buildExtraWall(
|
||||
const topFront = topJoint + outward * thickness;
|
||||
const frontXAt = (elevation: number): number =>
|
||||
topFront + outward * REVET_LEAN_RATIO * (topElevation - elevation);
|
||||
// 하단 = 기준선(anchor) 아래 근입 0.5m — 근입은 **경사선(전면) 측 깊이** 기준이라
|
||||
// 전면 발끝 지반이 낮으면 그 아래 0.5까지 더 내린다(2026-08-22 사용자 ②).
|
||||
let bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
|
||||
if (floatGapM <= 1e-6) {
|
||||
for (let pass = 0; pass < 6; pass += 1) {
|
||||
const toeGround = Math.min(groundAt(frontXAt(bottomElevation)), anchor.elevation);
|
||||
if (toeGround - REVET_EMBED_DEPTH_M >= bottomElevation - 1e-6) break;
|
||||
bottomElevation = toeGround - REVET_EMBED_DEPTH_M;
|
||||
}
|
||||
}
|
||||
// 하단 = 기준선(anchor) 아래 근입 0.5m **고정** — 원지반에 묻혀도 바닥을 더
|
||||
// 내리지 않는다. 벽 형상은 높이값으로만 정한다(2026-08-23 사용자 확정 —
|
||||
// 종전 전면 발끝 지반 추적 수렴 삭제).
|
||||
const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
|
||||
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation };
|
||||
const bottomFront: OffsetPoint = {
|
||||
offset: frontXAt(bottomElevation),
|
||||
@@ -342,7 +339,7 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
// 등간격 배치: 1차는 남은 구간(끝 포함) 균등 분할 근사, 이후 라운드는 보정 d.
|
||||
const requestedD =
|
||||
plan === "user"
|
||||
? adjust.d
|
||||
? (adjust.d ?? 0)
|
||||
: plan === "greedy"
|
||||
? (trailing.lengthM / (input.adjusts.length - i + 1)) *
|
||||
(FILL_SLOPE_RATIO_MIN / Math.hypot(1, FILL_SLOPE_RATIO_MIN)) -
|
||||
@@ -350,24 +347,9 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
: (plan[i] ?? 0);
|
||||
const requestedX = plan === "user" ? adjust.x : 0;
|
||||
const minD = Math.ceil((minJointRun - autoJointRun) * 10) / 10;
|
||||
/** 후보 자리의 바닥(전면 근입 심화 반영) — buildExtraWall과 같은 계산. */
|
||||
const bottomAt = (anchorX: number, baseElevation: number): number => {
|
||||
const baseWidth = REVET_THICKNESS_M * 1.5 + REVET_LEAN_RATIO * height;
|
||||
const jointX = anchorX - outward * (baseWidth / 2 - REVET_THICKNESS_M / 2);
|
||||
const topFrontX = jointX + outward * REVET_THICKNESS_M;
|
||||
const topElevation2 = baseElevation + height;
|
||||
const frontXAt = (e: number): number =>
|
||||
topFrontX + outward * REVET_LEAN_RATIO * (topElevation2 - e);
|
||||
let bottom = baseElevation - REVET_EMBED_DEPTH_M;
|
||||
if (baseElevation - groundAt(anchorX) <= 1e-6) {
|
||||
for (let pass = 0; pass < 6; pass += 1) {
|
||||
const toe = Math.min(groundAt(frontXAt(bottom)), baseElevation);
|
||||
if (toe - REVET_EMBED_DEPTH_M >= bottom - 1e-6) break;
|
||||
bottom = toe - REVET_EMBED_DEPTH_M;
|
||||
}
|
||||
}
|
||||
return bottom;
|
||||
};
|
||||
/** 후보 자리의 바닥 — buildExtraWall과 같은 근입 0.5m 고정(2026-08-23). */
|
||||
const bottomAt = (_anchorX: number, baseElevation: number): number =>
|
||||
baseElevation - REVET_EMBED_DEPTH_M;
|
||||
// 이동 한계 = 원지반 매몰 자리 금지(2026-08-22 사용자 확정): ① 이음선 상단이
|
||||
// 지반 아래(벽이 통째로 묻힘) ② 자기 성토선 시작점(실바닥+0.5 전면 교차점)이
|
||||
// 지반 아래 0.05m 이상(= 0.5m 이상 매몰 → 성토 불필요 자리).
|
||||
@@ -426,7 +408,6 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
height,
|
||||
material,
|
||||
Math.max(0, base - groundAt(anchorX)),
|
||||
groundAt,
|
||||
);
|
||||
walls.push(wall);
|
||||
// 성토선: src → (수평 선반 x>0이면 선반 끝) → 이음선 상단점. 사면길이 = 경사부.
|
||||
@@ -443,7 +424,7 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
ratio: rise > 1e-9 ? run / rise : FILL_SLOPE_RATIO_MIN,
|
||||
overLimit: Math.hypot(run, rise) >= FILL_SLOPE_MAX_LENGTH_M - 1e-6,
|
||||
});
|
||||
// 다음 단 시작점 = 이 벽 **실제 바닥**(근입 심화 반영) +0.5m 수평선과 전면
|
||||
// 다음 단 시작점 = 이 벽 바닥 +0.5m(= 기준선) 수평선과 전면
|
||||
// 경사선(1:0.3)의 교차점(2026-08-22 사용자 — 관 끝점 규칙과 동일 기준).
|
||||
const startElevation = wall.bottomBack.elevation + REVET_EMBED_DEPTH_M;
|
||||
src = {
|
||||
@@ -493,7 +474,7 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
const mean = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
||||
if (Math.max(...lengths.map((l) => Math.abs(l - mean))) < 0.05) break;
|
||||
const dNext = result.appliedAdjusts.map(
|
||||
(applied, i) => applied.d + (mean - (lengths[i] ?? mean)) * horizontalPerSlope,
|
||||
(applied, i) => (applied.d ?? 0) + (mean - (lengths[i] ?? mean)) * horizontalPerSlope,
|
||||
);
|
||||
result = cascadeOnce(dNext);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
BASIN_MAX_FILL_SLOPE_M,
|
||||
FILL_MIN_RISE_M,
|
||||
materialFromForm,
|
||||
materialLabel,
|
||||
materialLimit,
|
||||
PIPE_WALL_DEFAULT_HEIGHT_M,
|
||||
PIPE_WALL_MIN_HEIGHT_M,
|
||||
PIPE_WALL_DEFAULT_RUN_M,
|
||||
pipeWallMinPureHeight,
|
||||
FILL_SLOPE_MAX_LENGTH_M,
|
||||
FILL_SLOPE_RATIO_MAX,
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
REVET_EMBED_DEPTH_M,
|
||||
REVET_LEAN_RATIO,
|
||||
REVET_THICKNESS_M,
|
||||
REVET_TRAP_TOP_M,
|
||||
revetHeightLimit,
|
||||
revetTargetHeight,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
@@ -55,7 +55,7 @@ import { buildExtrasAt, inletGroundConnector } from "./B06_Section_UI_Cross_Culv
|
||||
import type { FillSlopeSegment, WallVertical } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import {
|
||||
cutLength,
|
||||
minShoulderWallOffset,
|
||||
fillWallBaseWidth,
|
||||
outletSlopeFactory,
|
||||
fillSlopeOf,
|
||||
groundInterpolator,
|
||||
@@ -162,20 +162,19 @@ export function computeCulvertLayout(
|
||||
inlet.elevation = Math.min(groundAt(startOffset), invertCap(inletInfo.edge));
|
||||
}
|
||||
|
||||
// 벽 제원(4축): 재질이 높이 한계(메2/찰3/콘5)를 정하고, 높이 = 설정 ?? 기본 2.0.
|
||||
// 하한 = max(2.0, 관경+여유고) — 관이 벽 위로 삐져나오면 안 된다.
|
||||
// 벽 제원(4축): 재질이 높이 한계(메2/찰3/콘5)를 정한다. 높이 기준 = **순수 높이**
|
||||
// (바닥~상단 — 2026-08-23 사용자). 하한·기본 = 관경 + 아래 0.5 + 위 0.5 —
|
||||
// 관이 벽 위로 삐져나오거나 바닥에 붙으면 안 된다. 기하는 종전대로
|
||||
// invert~상단 계산용 높이(= 순수 − 0.5)로 돈다.
|
||||
const adjustOf = (value?: WallAdjust): WallAdjust => ({ ...ZERO_ADJUST, ...(value ?? {}) });
|
||||
const adjInlet = adjustOf(revetShift?.inlet);
|
||||
const adjOutlet = adjustOf(revetShift?.outlet);
|
||||
const pipeWallSpec = (spec: CulvertSideSpec, adjust: WallAdjust) => {
|
||||
const material = adjust.m ?? materialFromForm(spec.revet_form);
|
||||
const limit = materialLimit(material);
|
||||
const floor = Math.max(PIPE_WALL_MIN_HEIGHT_M, revetTargetHeight(diameter));
|
||||
const height = Math.min(
|
||||
Math.max(adjust.h ?? Math.min(PIPE_WALL_DEFAULT_HEIGHT_M, limit), floor),
|
||||
Math.max(limit, floor),
|
||||
);
|
||||
return { material, limit, height };
|
||||
const floor = pipeWallMinPureHeight(diameter);
|
||||
const pureHeight = Math.min(Math.max(adjust.h ?? floor, floor), Math.max(limit, floor));
|
||||
return { material, limit, pureHeight, height: pureHeight - REVET_EMBED_DEPTH_M };
|
||||
};
|
||||
const inletWallSpec = pipeWallSpec(culvert.inlet, adjInlet);
|
||||
const outletWallSpec = pipeWallSpec(culvert.outlet, adjOutlet);
|
||||
@@ -191,33 +190,38 @@ export function computeCulvertLayout(
|
||||
diameterM: diameter,
|
||||
cutSlopeRatio: section.design?.cut_slope_ratio ?? 1.0,
|
||||
});
|
||||
/** 한계에 잘린 뒤의 실제 조작값 — 조정창이 되받는다. */
|
||||
/** 한계에 잘린 뒤의 실제 조작값 — 조정창이 되받는다(높이는 순수 높이 기준). */
|
||||
const appliedAdjust = {
|
||||
inlet: { ...adjInlet, h: adjInlet.h != null ? inletWallSpec.height : null },
|
||||
outlet: { ...adjOutlet, h: adjOutlet.h != null ? outletWallSpec.height : null },
|
||||
inlet: { ...adjInlet, h: adjInlet.h != null ? inletWallSpec.pureHeight : null },
|
||||
outlet: { ...adjOutlet, h: adjOutlet.h != null ? outletWallSpec.pureHeight : null },
|
||||
};
|
||||
/** 손으로 민 벽의 수직 제원(높이 증가·바닥 띄움) — buildWall에 물려 준다. */
|
||||
const wallVertical: { inlet: WallVertical | null; outlet: WallVertical | null } = {
|
||||
inlet: null,
|
||||
outlet: null,
|
||||
};
|
||||
// 자동 자리 = **노폭 연장 최소 지점**(1:1.2가 딱 성립하는 가장 안쪽). 없으면 사면 끝.
|
||||
let inletAutoOffset: number | null = null;
|
||||
// d의 0점 = **성토선 길이 0 지점**(벽 상단 = 노견 — 2026-08-23 사용자 확정).
|
||||
// 그 자리(하단선 중점)는 이음선 상단점이 노견 끝에 오는 offset이고, 기본 자리는
|
||||
// 성토선을 타고 경사길이 4m 내려간 지점(PIPE_WALL_DEFAULT_RUN_M)이다.
|
||||
const slopeZeroAnchor = (
|
||||
edge: { offset_m: number },
|
||||
outward: number,
|
||||
wallHeight: number,
|
||||
): number => edge.offset_m + outward * (fillWallBaseWidth(wallHeight) / 2 - REVET_TRAP_TOP_M);
|
||||
if (!basinReason && designAt) {
|
||||
const inletBaseAt = (offset: number): number =>
|
||||
Math.min(groundAt(offset), invertCap(inletInfo.edge));
|
||||
// 자동 자리 = 가용성 판정과 같은 스캔 결과(노견 최소 지점 ?? 사면 끝) 재사용.
|
||||
inletAutoOffset = inletOptions.revetAutoOffset;
|
||||
// 4축 배치는 공용 풀이(placeInletWall — Solve). 유입을 내리면 유출도 따라
|
||||
// 내려가므로, 유출이 못 받으면 유입도 못 내려간다(outletGuard).
|
||||
const placed = placeInletWall({
|
||||
autoOffset: inletAutoOffset,
|
||||
autoOffset: slopeZeroAnchor(inletInfo.edge, inletInfo.outward, inletWallSpec.height),
|
||||
outward: inletInfo.outward,
|
||||
height: inletWallSpec.height,
|
||||
baseElevation0: inletBaseAt(inletAutoOffset),
|
||||
baseElevation0: inletInfo.edge.elevation_m - inletWallSpec.height,
|
||||
edgeElevation: inletInfo.edge.elevation_m,
|
||||
invertCap: invertCap(inletInfo.edge),
|
||||
adjust: adjInlet,
|
||||
defaultD: PIPE_WALL_DEFAULT_RUN_M,
|
||||
groundAt,
|
||||
limitOffset: inletInfo.limit,
|
||||
outletGuard: {
|
||||
@@ -340,19 +344,12 @@ export function computeCulvertLayout(
|
||||
const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation };
|
||||
// 상단 변: 사다리꼴 상단(t/2) + 띠(t). 이음선은 상단 변 중간점에서 1:0.3으로 바닥까지.
|
||||
const topFront = topJoint + outward * thickness;
|
||||
// 하단선 = **수평 기초**, 깊이 = 기준선 아래 근입 0.5m(전면 측 기준으로 수렴).
|
||||
// 하단선 = **수평 기초**, 깊이 = 기준선(관 invert) 아래 근입 0.5m 고정 —
|
||||
// 원지반에 묻혀도 바닥을 더 내리지 않는다. 벽 형상은 높이값으로만 정한다
|
||||
// (2026-08-23 사용자 확정 — 종전 전면 발끝 지반 추적 수렴 삭제).
|
||||
const frontXAt = (elevation: number): number =>
|
||||
topFront + outward * REVET_LEAN_RATIO * (topElevation - elevation);
|
||||
let bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
|
||||
if (floatGapM <= 1e-6) {
|
||||
// 근입 0.5m는 **경사선(전면) 측 깊이** 기준(2026-08-22 사용자 ②) — 전면 발끝
|
||||
// 지반이 더 낮으면 그 아래 0.5까지 내린다. 내려가면 발끝이 더 나가므로 수렴 반복.
|
||||
for (let pass = 0; pass < 6; pass += 1) {
|
||||
const toeGround = Math.min(groundAt(frontXAt(bottomElevation)), anchor.elevation);
|
||||
if (toeGround - REVET_EMBED_DEPTH_M >= bottomElevation - 1e-6) break;
|
||||
bottomElevation = toeGround - REVET_EMBED_DEPTH_M;
|
||||
}
|
||||
}
|
||||
const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
|
||||
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation };
|
||||
const bottomFront: OffsetPoint = {
|
||||
offset: frontXAt(bottomElevation),
|
||||
@@ -407,29 +404,30 @@ export function computeCulvertLayout(
|
||||
);
|
||||
// 유출 벽 밑 = 그 자리 원지반(역경사는 유입 invert로 클램프 — 2026-08-21).
|
||||
const invertAt = (offset: number): number => Math.min(groundAt(offset), inlet.elevation);
|
||||
// 유출 벽 자동 자리도 **노견 최소 지점**(2026-08-22 사용자 확정) — 못 찾으면 사면 끝.
|
||||
let outletWallAnchor = outletAnchor;
|
||||
const outletFeasible = designAt
|
||||
? minShoulderWallOffset(
|
||||
outletInfo.edge,
|
||||
outletInfo.outward,
|
||||
outletWallSpec.height,
|
||||
invertAt,
|
||||
outletAnchorOffset,
|
||||
outletInfo.limit,
|
||||
)
|
||||
: null;
|
||||
const outletAutoOffset = outletFeasible ?? outletAnchorOffset;
|
||||
// 유출도 성토선 0점 기준(2026-08-23) — 기본 자리는 경사길이 4m 지점. 관 길이
|
||||
// 정수 맞춤 루프가 벽을 옮길 때 invert가 같은 성토선을 타도록 함수로 둔다.
|
||||
const outletZeroAnchor = slopeZeroAnchor(
|
||||
outletInfo.edge,
|
||||
outletInfo.outward,
|
||||
outletWallSpec.height,
|
||||
);
|
||||
const outletZeroInvert = outletInfo.edge.elevation_m - outletWallSpec.height;
|
||||
const outletLineInvertAt = (offset: number): number =>
|
||||
designAt
|
||||
? outletZeroInvert - ((offset - outletZeroAnchor) * outletInfo.outward) / FILL_SLOPE_RATIO_MIN
|
||||
: invertAt(offset);
|
||||
if (designAt) {
|
||||
// 4축 배치(유입과 동일 풀이) + 유출 전용: 매몰-무교차 자리 금지(2026-08-22 ③).
|
||||
const placed = placePipeWall({
|
||||
autoOffset: outletAutoOffset,
|
||||
autoOffset: outletZeroAnchor,
|
||||
outward: outletInfo.outward,
|
||||
height: outletWallSpec.height,
|
||||
baseElevation0: invertAt(outletAutoOffset),
|
||||
baseElevation0: outletZeroInvert,
|
||||
edgeElevation: outletInfo.edge.elevation_m,
|
||||
invertCap: inlet.elevation,
|
||||
adjust: adjOutlet,
|
||||
defaultD: PIPE_WALL_DEFAULT_RUN_M,
|
||||
groundAt,
|
||||
limitOffset: outletInfo.limit,
|
||||
requireCrossing: true,
|
||||
@@ -457,8 +455,7 @@ export function computeCulvertLayout(
|
||||
/** 유출 관 하단 끝점 목표 = 벽 바닥 기준 0.5m 상단(기준선)과 **전면 경사선의
|
||||
* 교차점**(2026-08-22 사용자 ① — 성토부선 시작 규칙과 같은 자리). */
|
||||
const outletPipeEnd = (wall: WallLayout): OffsetPoint => {
|
||||
// 기준은 **실제 바닥**(전면 근입으로 깊어진 값) +0.5 — 기준선(anchor)을 쓰면
|
||||
// 가파른 지반에서 바닥만 내려가고 관이 벽 위쪽에 떠 보인다(2026-08-22 사용자 ②).
|
||||
// 기준 = 바닥 +0.5(= 관 invert 자리 — 2026-08-23부터 바닥은 invert−0.5 고정).
|
||||
const reference = wall.bottomBack.elevation + REVET_EMBED_DEPTH_M;
|
||||
return {
|
||||
offset:
|
||||
@@ -494,11 +491,13 @@ export function computeCulvertLayout(
|
||||
|
||||
// ── 관 길이 m 단위 맞춤(2026-08-21 사용자 ①) — 긴 변 기준 올림, 모자란 만큼 유출
|
||||
// 벽 자리를 관 축 방향 바깥으로 민다(벽 두께 0.45 고정).
|
||||
const outletSlopeAt = outletSlopeFactory(outletInfo, invertAt, outletWallSpec.height);
|
||||
// 정수 맞춤이 옮길 후보 자리도 같은 성토선을 탄다(2026-08-23 — 0점 기준 통일).
|
||||
const outletSlopeAt = outletSlopeFactory(outletInfo, outletLineInvertAt, outletWallSpec.height);
|
||||
/** 유출 벽 두께 — 항상 0.45 고정(2026-08-21 사용자 확정). 폭으로 길이를 맞추지 않는다. */
|
||||
const outletThickness = REVET_THICKNESS_M;
|
||||
// 손으로 민 벽은 정수 맞춤이 **절대 옮기지 않는다**(2026-08-22) — 표기만 올림한다.
|
||||
const outletPinned = Math.abs(adjOutlet.x) > 1e-9 || Math.abs(adjOutlet.d) > 1e-9;
|
||||
// d = null(기본 자리)만 자동으로 본다(2026-08-23 — d는 0점 기준 절대값).
|
||||
const outletPinned = Math.abs(adjOutlet.x) > 1e-9 || adjOutlet.d != null;
|
||||
if (outletWall && !outletPinned) {
|
||||
const target = Math.ceil(cutLength(pipeCorners) - 1e-6);
|
||||
const rebuild = (): void => {
|
||||
@@ -535,21 +534,22 @@ export function computeCulvertLayout(
|
||||
// ① 벽 자리를 관 축 방향으로 gap 만큼 옮긴다(두께 불변 — 사용자 확정 우선순위).
|
||||
const moved = outletWallAnchor.offset + pipeAxis.axis.offset * gap;
|
||||
const movedSlope = outletSlopeAt(moved, outletThickness);
|
||||
// 사면 역전·노견 안쪽으로는 옮기지 않는다 — 길이 맞춤보다 도면 성립이 먼저다.
|
||||
// 사면 역전(벽 상단이 노견 위)·노견 안쪽으로는 옮기지 않는다 — 길이 맞춤보다
|
||||
// 도면 성립이 먼저다. 성토고 0(성토선 0점)까지는 허용(2026-08-23 사용자).
|
||||
if (
|
||||
(moved - outletInfo.edge.offset_m) * outletInfo.outward < 0 ||
|
||||
outletInfo.edge.elevation_m - (invertAt(moved) + outletWallSpec.height) < FILL_MIN_RISE_M
|
||||
outletInfo.edge.elevation_m - (outletLineInvertAt(moved) + outletWallSpec.height) < 0
|
||||
) {
|
||||
break;
|
||||
}
|
||||
// 자동 자리만 옮긴다(수동 벽은 이 루프에 아예 안 들어온다). 폭은 0.45 고정 —
|
||||
// 폭으로 흡수하면 미는 동안 벽 단면이 변한다(2026-08-21 사용자 지적).
|
||||
if (
|
||||
movedSlope.ratio >= FILL_SLOPE_RATIO_MIN &&
|
||||
movedSlope.ratio >= FILL_SLOPE_RATIO_MIN - 1e-6 &&
|
||||
movedSlope.ratio <= FILL_SLOPE_RATIO_MAX &&
|
||||
movedSlope.lengthM <= FILL_SLOPE_MAX_LENGTH_M + 1e-6
|
||||
) {
|
||||
outletWallAnchor = { offset: moved, elevation: invertAt(moved) };
|
||||
outletWallAnchor = { offset: moved, elevation: outletLineInvertAt(moved) };
|
||||
rebuild();
|
||||
if (!outletWall) break;
|
||||
continue;
|
||||
@@ -573,7 +573,8 @@ export function computeCulvertLayout(
|
||||
if (basin) {
|
||||
applyBasinPipeFill(basin, pipeCorners.inlet.bottom, inletInfo.outward, groundAt);
|
||||
}
|
||||
// 유입 기슭막이의 관 시작 접속선(2026-08-22 ②) — 위 0도 성토선 / 아래 0도 1m+절토선.
|
||||
// 유입 기슭막이의 관 시작 접속선 — 위 0도 성토선 / 아래 0도 1m 공간 확보에서 끝
|
||||
// (2026-08-23 사용자 — 절토 경사 연장 삭제).
|
||||
const inletFill = !basin
|
||||
? inletGroundConnector(
|
||||
pipeCorners.inlet.bottom,
|
||||
|
||||
@@ -363,31 +363,36 @@ export interface PipeWallPlacement {
|
||||
|
||||
/**
|
||||
* 배관 기슭막이 4축 배치(2026-08-22 확정 규칙 묶음) — 유입·유출 공용.
|
||||
* · x = 노견 연장 평행이동(안쪽 금지), d = 성토선 대각(위 한계 = 최소 성토고·
|
||||
* invert 상한(토피/역경사)).
|
||||
* · 기준(2026-08-23 사용자 확정): autoOffset·baseElevation0 = **성토선 길이 0
|
||||
* 지점**(벽 상단 = 노견)의 자리·invert. d는 그 점 기준 절대값(null = 기본
|
||||
* defaultD, 경사길이 4m 지점), 위 한계 = 성토선 0 지점(d ≥ 0)·invert 상한.
|
||||
* · x = 노견 연장 평행이동(안쪽 금지).
|
||||
* · requireCrossing(유출): 매몰인데 0도 절토선이 원지반과 다시 못 만나는 자리는
|
||||
* 금지 — 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우).
|
||||
* 금지 — 위쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우).
|
||||
*/
|
||||
export function placePipeWall(input: {
|
||||
/** 성토선 0 지점(벽 상단 = 노견)의 벽 자리(하단선 중점 offset). */
|
||||
autoOffset: number;
|
||||
outward: number;
|
||||
height: number;
|
||||
/** 자동 자리의 invert(= baseAt(autoOffset)). */
|
||||
/** 성토선 0 지점의 invert(= 노견 표고 − 높이). */
|
||||
baseElevation0: number;
|
||||
edgeElevation: number;
|
||||
/** invert 상한 — 유입: 노면−관경−토피, 유출: 유입 invert(역경사 방지). */
|
||||
invertCap: number;
|
||||
adjust: WallAdjust;
|
||||
/** d 미지정(null) 시의 기본 자리 — 성토사면 경사길이 4m 지점의 수평 성분. */
|
||||
defaultD: number;
|
||||
groundAt: (offset: number) => number;
|
||||
limitOffset: number;
|
||||
requireCrossing: boolean;
|
||||
}): PipeWallPlacement {
|
||||
const top0 = input.baseElevation0 + input.height;
|
||||
const dLow = Math.max(
|
||||
-FILL_SLOPE_RATIO_MIN * (input.edgeElevation - FILL_MIN_RISE_M - top0),
|
||||
-FILL_SLOPE_RATIO_MIN * (input.edgeElevation - top0),
|
||||
-FILL_SLOPE_RATIO_MIN * (input.invertCap - (top0 - input.height)),
|
||||
);
|
||||
let d = Math.max(input.adjust.d, Math.ceil(dLow * 10) / 10);
|
||||
let d = Math.max(input.adjust.d ?? input.defaultD, Math.ceil(dLow * 10) / 10);
|
||||
let x = Math.max(input.adjust.x, 0);
|
||||
if (input.requireCrossing) {
|
||||
const placeable = (anchorOffset: number, invertE: number): boolean =>
|
||||
@@ -471,6 +476,7 @@ export function placeInletWall(input: {
|
||||
edgeElevation: number;
|
||||
invertCap: number;
|
||||
adjust: WallAdjust;
|
||||
defaultD: number;
|
||||
groundAt: (offset: number) => number;
|
||||
limitOffset: number;
|
||||
/** 유출 쪽 수용 검사 인자 — 없으면 가드 없이 배치한다. */
|
||||
|
||||
@@ -12,19 +12,23 @@ import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
* x = 좌우(m, + = 계류측 바깥) — 노견(또는 윗단 시작점 수평선)을 늘려 평행이동,
|
||||
* 성토선 물매 1:1.2 불변.
|
||||
* d = 상하(m, 수평 성분, + = 사면 아래) — 성토선을 타고 대각 이동.
|
||||
* h = 높이(m, 계산용: 근입 0.5 위 기준선~상단). null = 기본값
|
||||
* (배관 2.5·일반 1.5, 재질 한계로 잘림).
|
||||
* **배관(유입·유출) 벽**은 0점 = 성토선 길이 0 지점(벽 상단 = 노견)인
|
||||
* 절대값(2026-08-23 사용자 확정), null = 기본 자리(경사길이 4m 지점).
|
||||
* 추가 벽은 종전대로 자동 자리 기준 상대값(null = 0).
|
||||
* h = 높이(m). 배관 벽은 **순수 높이**(바닥~상단, 관 아래 0.5 포함 —
|
||||
* 2026-08-23), 추가 벽은 계산용(근입 0.5 위 기준선~상단). null = 기본값
|
||||
* (배관 관경+1.0·일반 1.5, 재질 한계로 잘림).
|
||||
* m = 재질. null = 기본값(메쌓기). 한계: 메 2.0 / 찰 3.0 / 콘크리트 5.0.
|
||||
*/
|
||||
export interface WallAdjust {
|
||||
x: number;
|
||||
d: number;
|
||||
d: number | null;
|
||||
h: number | null;
|
||||
m: RevetMaterial | null;
|
||||
}
|
||||
|
||||
/** 조작값 기본(전부 자동). */
|
||||
export const ZERO_ADJUST: WallAdjust = { x: 0, d: 0, h: null, m: null };
|
||||
export const ZERO_ADJUST: WallAdjust = { x: 0, d: null, h: null, m: null };
|
||||
|
||||
export interface OffsetPoint {
|
||||
offset: number;
|
||||
|
||||
@@ -149,7 +149,7 @@ export function computeCardCulvert(
|
||||
"info",
|
||||
);
|
||||
}
|
||||
if (Math.abs(requested.d - applied.d) > 0.05) {
|
||||
if (requested.d != null && applied.d != null && Math.abs(requested.d - applied.d) > 0.05) {
|
||||
showToast(
|
||||
L(requested.d < applied.d ? "B06_Cross_Revet_Limit_Up" : "B06_Cross_Revet_Limit_Down"),
|
||||
"info",
|
||||
@@ -170,7 +170,12 @@ export function computeCardCulvert(
|
||||
showToast(L("B06_Cross_Height_Floor"), "info");
|
||||
}
|
||||
}
|
||||
revetOffset.syncApplied(section.chainage_m, role, applied);
|
||||
// d = null(기본 자리)은 null로 되돌려 담는다 — 적용 좌표를 그대로 저장하면
|
||||
// 자동 자리가 "사용자 지정"으로 굳어 지형·높이 변경을 따라가지 못한다.
|
||||
revetOffset.syncApplied(section.chainage_m, role, {
|
||||
...applied,
|
||||
d: requested.d == null ? null : applied.d,
|
||||
});
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
|
||||
@@ -337,6 +337,38 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
);
|
||||
extraParts.controls.append(countMinus, countValue, countPlus);
|
||||
|
||||
// 키보드 방향키 = 십자 버튼(2026-08-23 사용자). 9키(집수정)도 **십자 축만** —
|
||||
// 모서리(크기) 키는 대상 아님. 셀렉트 포커스 중엔 드롭다운 탐색에 양보한다.
|
||||
// 창은 카드마다 새로 만들어지므로 문서 리스너는 show/hide에 맞춰 붙였다 떼고,
|
||||
// 카드가 통째로 사라진 경우(root 분리)엔 스스로 해제한다.
|
||||
const ARROW_SLOT: Record<string, string> = {
|
||||
ArrowUp: "up",
|
||||
ArrowDown: "down",
|
||||
ArrowLeft: "left",
|
||||
ArrowRight: "right",
|
||||
};
|
||||
const onArrowKey = (event: KeyboardEvent) => {
|
||||
if (!root.isConnected) {
|
||||
document.removeEventListener("keydown", onArrowKey);
|
||||
return;
|
||||
}
|
||||
const slot = ARROW_SLOT[event.key];
|
||||
if (!slot || root.classList.contains("is-hidden")) return;
|
||||
const focus = event.target as HTMLElement | null;
|
||||
if (focus && (focus.tagName === "SELECT" || focus.tagName === "INPUT")) return;
|
||||
for (const candidate of [basinRow, moveRow]) {
|
||||
if (candidate.row.classList.contains("is-hidden")) continue;
|
||||
const button = candidate.controls.querySelector<HTMLButtonElement>(
|
||||
`.b06-structure-panel__btn--${slot}`,
|
||||
);
|
||||
if (button) {
|
||||
event.preventDefault();
|
||||
button.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeButton = makeButton("✕", L("B06_Cross_Revet_Close"), () => deps.close());
|
||||
closeButton.classList.add("b06-structure-panel__close");
|
||||
|
||||
@@ -362,7 +394,9 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
show: (key) => {
|
||||
current = key;
|
||||
root.classList.toggle("is-hidden", key === null);
|
||||
document.removeEventListener("keydown", onArrowKey);
|
||||
if (!key) return;
|
||||
document.addEventListener("keydown", onArrowKey);
|
||||
const isExtra = key.startsWith("extra");
|
||||
const movable = deps.canNudge(key);
|
||||
const structure = deps.structureFor();
|
||||
@@ -431,11 +465,15 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
(Math.abs(adjust.x) > 1e-9
|
||||
? `${L(adjust.x > 0 ? "B06_Cross_Revet_Outward" : "B06_Cross_Revet_Inward")} ${Math.abs(adjust.x).toFixed(1)}m`
|
||||
: L("B06_Cross_Revet_Auto"));
|
||||
// d = null → 자동(기본 자리). 숫자는 성토선 0점(벽 상단=노견) 기준 절대값
|
||||
// (배관 벽 — 2026-08-23). 0 = 성토선 0 지점.
|
||||
slopeLine.textContent =
|
||||
`${L("B06_Cross_Slope_Label")} ` +
|
||||
(Math.abs(adjust.d) > 1e-9
|
||||
? `${L(adjust.d > 0 ? "B06_Cross_Revet_DirDown" : "B06_Cross_Revet_DirUp")} ${Math.abs(adjust.d).toFixed(1)}m`
|
||||
: L("B06_Cross_Revet_Auto"));
|
||||
(adjust.d == null
|
||||
? L("B06_Cross_Revet_Auto")
|
||||
: Math.abs(adjust.d) > 1e-9
|
||||
? `${L(adjust.d > 0 ? "B06_Cross_Revet_DirDown" : "B06_Cross_Revet_DirUp")} ${Math.abs(adjust.d).toFixed(1)}m`
|
||||
: "0m");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import type { CrossWidthActions, ZoomPanState } from "./B06_Section_UI_Cross_Vie
|
||||
import { toeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
||||
import { createCrossAxes, IDENTITY_VIEW } from "./B06_Section_UI_Cross_Axes";
|
||||
import type { RevetHighlightSetter, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
||||
import { REVET_EMBED_DEPTH_M } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import {
|
||||
@@ -164,8 +165,11 @@ export function createCrossSectionCard(
|
||||
let culvertExtraState = { canAdd: false, count: 0 };
|
||||
/** 집수정 계류측 다단 상태(2026-08-22) — 조정창의 집수정 단 수 행이 쓴다. */
|
||||
let culvertBasinExtraState = { canAdd: false, count: 0 };
|
||||
/** 마지막 계산의 벽 제원(높이·재질) — 조정창 표시·높이 조작 기준. */
|
||||
/** 마지막 계산의 벽 제원(높이·재질) — 조정창 표시·높이 조작 기준.
|
||||
* 배관(유입·유출) 벽 높이는 **순수 높이**(바닥~상단 — 2026-08-23 사용자). */
|
||||
let culvertWallSpecs = new Map<RevetKey, { height: number; material: RevetMaterial }>();
|
||||
/** 마지막 계산의 실제 적용 d(상하) — d 미지정(자동) 벽의 ▲▼ 시작값. */
|
||||
let culvertAppliedD = new Map<RevetKey, number>();
|
||||
/**
|
||||
* 기슭막이를 고른다. 구조물 선택과 절·성토 면적 강조는 **같은 레벨**이라 하나를
|
||||
* 고르면 다른 하나는 풀린다(2026-08-21 사용자). 또 구조물을 고르면 그 **측점 카드도
|
||||
@@ -417,10 +421,23 @@ export function createCrossSectionCard(
|
||||
};
|
||||
culvertWallSpecs = new Map();
|
||||
const specOf = (wall: (typeof culvertLayout.walls)[number], key: RevetKey) =>
|
||||
culvertWallSpecs.set(key, { height: wall.height, material: wall.material });
|
||||
culvertWallSpecs.set(key, {
|
||||
// 배관 벽은 순수 높이(바닥~상단 = 계산용 + 근입 0.5 — 2026-08-23 사용자).
|
||||
height: wall.role === "extra" ? wall.height : wall.height + REVET_EMBED_DEPTH_M,
|
||||
material: wall.material,
|
||||
});
|
||||
for (const wall of culvertLayout.walls) specOf(wall, wall.role as RevetKey);
|
||||
culvertLayout.extraWalls.forEach((wall, i) => specOf(wall, `extra${i}` as RevetKey));
|
||||
culvertLayout.basinExtras.forEach((wall, i) => specOf(wall, `bextra${i}` as RevetKey));
|
||||
culvertAppliedD = new Map();
|
||||
culvertAppliedD.set("inlet", culvertLayout.revetShift.inlet.d ?? 0);
|
||||
culvertAppliedD.set("outlet", culvertLayout.revetShift.outlet.d ?? 0);
|
||||
culvertLayout.revetShift.extras.forEach((applied, i) =>
|
||||
culvertAppliedD.set(`extra${i}` as RevetKey, applied.d ?? 0),
|
||||
);
|
||||
culvertLayout.revetShift.basinExtras.forEach((applied, i) =>
|
||||
culvertAppliedD.set(`bextra${i}` as RevetKey, applied.d ?? 0),
|
||||
);
|
||||
}
|
||||
if (culvertLayout) {
|
||||
setRevetActive = appendCulvertOverlay(
|
||||
@@ -534,15 +551,19 @@ export function createCrossSectionCard(
|
||||
const materialOfWall = (key: RevetKey): RevetMaterial =>
|
||||
culvertWallSpecs.get(key)?.material ?? "dry";
|
||||
const adjustOf = (key: RevetKey): WallAdjust =>
|
||||
revetOffset?.adjustFor(section, key) ?? { x: 0, d: 0, h: null, m: null };
|
||||
revetOffset?.adjustFor(section, key) ?? { x: 0, d: null, h: null, m: null };
|
||||
const panel = buildStructurePanel({
|
||||
adjustFor: adjustOf,
|
||||
nudge: (key, screenDeltaM) =>
|
||||
revetOffset?.update(section.chainage_m, key, {
|
||||
x: adjustOf(key).x + screenDeltaM * outwardOf(key),
|
||||
}),
|
||||
// d 미지정(자동)이면 실제 적용 자리에서부터 움직인다(2026-08-23 — d는 0점
|
||||
// 기준 절대값이라 시작값이 필요하다).
|
||||
nudgeSlope: (key, deltaM) =>
|
||||
revetOffset?.update(section.chainage_m, key, { d: adjustOf(key).d + deltaM }),
|
||||
revetOffset?.update(section.chainage_m, key, {
|
||||
d: (adjustOf(key).d ?? culvertAppliedD.get(key) ?? 0) + deltaM,
|
||||
}),
|
||||
nudgeHeight: (key, deltaM) => {
|
||||
const current = adjustOf(key);
|
||||
const base = current.h ?? heightOfWall(key);
|
||||
|
||||
@@ -135,7 +135,9 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
revetShifts.set(entry, { ...ZERO_ADJUST, x: value });
|
||||
} else if (value && typeof value === "object") {
|
||||
revetShifts.set(entry, { ...ZERO_ADJUST, ...value });
|
||||
// 구세션 호환: d=0은 옛 "자동 자리" 의미 — 새 체계(절대 0 = 성토선 0점)로
|
||||
// 읽으면 벽이 노견까지 튀므로 null(기본 자리)로 옮긴다.
|
||||
revetShifts.set(entry, { ...ZERO_ADJUST, ...value, d: value.d ? value.d : null });
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
@@ -157,15 +159,17 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
const round1 = (value: number): number => Math.round(value * 10) / 10;
|
||||
const clampMove = (value: number): number => Math.min(Math.max(round1(value), -10), 10);
|
||||
const isDefaultAdjust = (value: WallAdjust): boolean =>
|
||||
Math.abs(value.x) < 1e-9 && Math.abs(value.d) < 1e-9 && value.h == null && value.m == null;
|
||||
Math.abs(value.x) < 1e-9 && value.d == null && value.h == null && value.m == null;
|
||||
const storeAdjust = (key: string, value: WallAdjust): void => {
|
||||
if (isDefaultAdjust(value)) revetShifts.delete(key);
|
||||
else revetShifts.set(key, value);
|
||||
persistRevetShifts();
|
||||
};
|
||||
const axisEq = (a: number | null, b: number | null): boolean =>
|
||||
a == null || b == null ? a === b : Math.abs(a - b) < 1e-9;
|
||||
const sameAdjust = (a: WallAdjust, b: WallAdjust): boolean =>
|
||||
Math.abs(a.x - b.x) < 1e-9 &&
|
||||
Math.abs(a.d - b.d) < 1e-9 &&
|
||||
axisEq(a.d, b.d) &&
|
||||
(a.h ?? null) === (b.h ?? null) &&
|
||||
(a.m ?? null) === (b.m ?? null);
|
||||
|
||||
@@ -189,7 +193,8 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
const current = revetShifts.get(key) ?? { ...ZERO_ADJUST };
|
||||
const next: WallAdjust = {
|
||||
x: clampMove(patch.x ?? current.x),
|
||||
d: clampMove(patch.d ?? current.d),
|
||||
// d: null = 기본 자리(배관 벽 — 경사길이 4m 지점) 유지.
|
||||
d: patch.d === undefined ? current.d : patch.d === null ? null : clampMove(patch.d),
|
||||
// 높이는 0.1 눈금 반올림만 — 재질 한계 절삭은 기하가 하고 되받는다.
|
||||
h: patch.h === undefined ? current.h : patch.h === null ? null : round1(patch.h),
|
||||
m: patch.m === undefined ? current.m : patch.m,
|
||||
|
||||
Reference in New Issue
Block a user