auto: 2026-08-29 08:54 (EOMSANGDON-HOME)
This commit is contained in:
@@ -163,9 +163,10 @@ export function appendCulvertOverlay(
|
||||
const revetShape = polygon(
|
||||
wall.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]),
|
||||
"b06-chart__culvert-revet",
|
||||
// 배관 벽 높이 표기 = 순수 높이(바닥~상단, 관 아래 0.5 포함 — 2026-08-23 사용자).
|
||||
// 높이 표기 = 순수 높이(바닥~상단, 근입 0.5 포함). 추가 기슭막이도 같은 기준으로
|
||||
// 통일했다(2026-08-23 배관 벽 → 2026-08-29 추가 벽).
|
||||
`${roleLabel(wall.role)} 기슭막이 ${wall.form ?? ""} H=${(
|
||||
wall.height + (wall.role === "extra" ? 0 : REVET_EMBED_DEPTH_M)
|
||||
wall.height + REVET_EMBED_DEPTH_M
|
||||
).toFixed(1)}m` +
|
||||
`(상단 = 사면선 접점, 전면 1:${REVET_LEAN_RATIO}` +
|
||||
`, 높이 한계 ${revetHeightLimit(wall.form).toFixed(1)}m — 교본 7-3)` +
|
||||
|
||||
@@ -92,9 +92,9 @@ export function revetHeightLimit(form: string | null | undefined): number {
|
||||
/* ── 기슭막이 재질·높이 조작(2026-08-22 사용자 확정) ─────────────────────
|
||||
* 높이 조작이 교본 형태별 한계와 부딪히면 **재질을 바꿔야** 더 올릴 수 있다:
|
||||
* 메쌓기 2.0 / 찰쌓기 3.0(돌쌓기.md §1) / 콘크리트 5.0(프로젝트 기본값,
|
||||
* 외부 기준 미확인). 배관 기슭막이의 높이 제어·표기·한계 기준은 **순수 높이**
|
||||
* (벽 바닥~상단, 관 아래 0.5m 포함 — 2026-08-23 사용자 확정). 일반(추가)
|
||||
* 기슭막이는 종전 계산용 높이(근입 0.5 위 기준선~상단) 기준을 유지한다. */
|
||||
* 외부 기준 미확인). 높이 제어·표기·한계 기준은 모두 **순수 높이**(벽 바닥~상단,
|
||||
* 근입 0.5m 포함) — 배관 기슭막이 2026-08-23, 추가·독립 기슭막이 2026-08-29
|
||||
* 사용자 확정으로 기준을 하나로 맞췄다. */
|
||||
export type RevetMaterial = "dry" | "wet" | "concrete";
|
||||
|
||||
export const REVET_MATERIALS: RevetMaterial[] = ["dry", "wet", "concrete"];
|
||||
|
||||
@@ -294,10 +294,18 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
const adjust = input.adjusts[i];
|
||||
const material = adjust.m ?? "dry";
|
||||
const limit = materialLimit(material);
|
||||
// 높이 기준 = **벽 바닥~상단**(근입 0.5 포함) — 배관 기준벽과 같은 기준으로
|
||||
// 통일했다(2026-08-29 사용자: 추가 기슭막이 높이가 하단~상단이 아니었다).
|
||||
const height = Math.min(
|
||||
Math.max(adjust.h ?? EXTRA_WALL_DEFAULT_HEIGHT_M, EXTRA_WALL_MIN_HEIGHT_M),
|
||||
Math.max(
|
||||
adjust.h ?? EXTRA_WALL_DEFAULT_HEIGHT_M,
|
||||
EXTRA_WALL_MIN_HEIGHT_M,
|
||||
REVET_EMBED_DEPTH_M + 0.1,
|
||||
),
|
||||
limit,
|
||||
);
|
||||
/** 기준선(근입 위)~상단 — 도형·자리 계산은 종전대로 이 값으로 한다. */
|
||||
const exposed = height - REVET_EMBED_DEPTH_M;
|
||||
|
||||
/** 자리 x(벽 하단 중점)에 지반 안착 + 상단이 성토선에 닿는 데 필요한 높이. */
|
||||
const heightAt = (x: number): number => {
|
||||
@@ -318,7 +326,7 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
let previousShort = 0; // 직전 스캔점의 h 부족량(heightAt < h) — 보간용
|
||||
for (let t = step; t <= Math.max(span, 0) + 1e-9; t += step) {
|
||||
const x = src.offset + outward * t;
|
||||
const gap = heightAt(x) - height;
|
||||
const gap = heightAt(x) - exposed;
|
||||
if (gap >= 0) {
|
||||
// 교차점 선형 보간 — 격자 대신 정확 자리(사면선·지반 동시 접점).
|
||||
const back = previousShort + gap > 1e-9 ? (gap / (previousShort + gap)) * step : 0;
|
||||
@@ -333,36 +341,41 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
}
|
||||
if (autoOffset === null) autoOffset = bestOffset;
|
||||
if (autoOffset === null) break; // 세울 만한 지형이 아니다 — 되는 만큼만.
|
||||
// 관통 금지: 상단 ≤ 윗단 하단 → 성토선 수평거리(경사부) ≥ 1.2×(시작−윗단하단).
|
||||
const autoJointRun = (autoOffset - src.offset) * outward - jointRunOf(height);
|
||||
const minJointRun = FILL_SLOPE_RATIO_MIN * Math.max(src.elevation - prevBottom, 0);
|
||||
// 등간격 배치: 1차는 남은 구간(끝 포함) 균등 분할 근사, 이후 라운드는 보정 d.
|
||||
const autoJointRun = (autoOffset - src.offset) * outward - jointRunOf(exposed);
|
||||
/** 자동 자리(선반 0·수직 0)의 벽 상단 표고 — 조작은 여기서부터 잰다. */
|
||||
const autoTop = src.elevation - autoJointRun / FILL_SLOPE_RATIO_MIN;
|
||||
// 관통 금지: 상단 ≤ 윗단 하단 → 그만큼은 반드시 내려간다.
|
||||
const minV = Math.ceil(Math.max(autoTop - prevBottom, 0) * 10) / 10;
|
||||
// 조작 축(2026-08-29 사용자): d = **순수 수직 하강(m)**, x = **순수 수평 이동(m)**.
|
||||
// 성토선 각도(1:1.2)는 그대로 두고, 윗단 앞 **수평 선반**이 길이를 흡수한다
|
||||
// 선반 = x − 1.2·d (≥ 0이어야 각도가 유지된다).
|
||||
// 등간격 배치: 1차는 남은 구간(끝 포함) 균등 분할 근사, 이후 라운드는 보정값.
|
||||
const requestedD =
|
||||
plan === "user"
|
||||
? (adjust.d ?? 0)
|
||||
: plan === "greedy"
|
||||
? (trailing.lengthM / (input.adjusts.length - i + 1)) *
|
||||
? ((trailing.lengthM / (input.adjusts.length - i + 1)) *
|
||||
(FILL_SLOPE_RATIO_MIN / Math.hypot(1, FILL_SLOPE_RATIO_MIN)) -
|
||||
autoJointRun
|
||||
autoJointRun) /
|
||||
FILL_SLOPE_RATIO_MIN
|
||||
: (plan[i] ?? 0);
|
||||
const requestedX = plan === "user" ? adjust.x : 0;
|
||||
const minD = Math.ceil((minJointRun - autoJointRun) * 10) / 10;
|
||||
/** 후보 자리의 바닥 — 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 이상 매몰 → 성토 불필요 자리).
|
||||
const placeable = (xShift: number, dShift: number): boolean => {
|
||||
const topE = src.elevation - (autoJointRun + dShift) / FILL_SLOPE_RATIO_MIN;
|
||||
const aX = autoOffset + outward * (xShift + dShift);
|
||||
const jointX = aX - outward * jointRunOf(height);
|
||||
const placeable = (xShift: number, vShift: number): boolean => {
|
||||
const topE = autoTop - vShift;
|
||||
const aX = autoOffset + outward * xShift;
|
||||
const jointX = aX - outward * jointRunOf(exposed);
|
||||
if (
|
||||
Math.max(groundAt(jointX), groundAt(jointX + outward * REVET_THICKNESS_M)) >=
|
||||
topE - 0.01
|
||||
)
|
||||
return false;
|
||||
const baseE = topE - height;
|
||||
const baseE = topE - exposed;
|
||||
const bottom = bottomAt(aX, baseE);
|
||||
const startE = bottom + REVET_EMBED_DEPTH_M;
|
||||
const topFrontX = jointX + outward * REVET_THICKNESS_M;
|
||||
@@ -379,13 +392,18 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
) !== null
|
||||
);
|
||||
};
|
||||
let appliedD = Math.max(requestedD, minD);
|
||||
let appliedX = Math.max(requestedX, 0);
|
||||
let appliedD = Math.max(requestedD, minV);
|
||||
/** 선반 길이가 음수가 되면 각도가 깨진다 — x가 최소 1.2·d까지 따라 나간다. */
|
||||
const shelfFloor = (v: number): number => FILL_SLOPE_RATIO_MIN * v;
|
||||
let appliedX = Math.max(requestedX, 0, shelfFloor(appliedD));
|
||||
for (let pass = 0; pass < 400 && !placeable(appliedX, appliedD); pass += 1) {
|
||||
// 매몰 자리 — 요청을 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우).
|
||||
if (appliedD > minD + 1e-9) appliedD = Math.max(minD, appliedD - 0.05);
|
||||
else if (appliedX > 1e-9) appliedX = Math.max(0, appliedX - 0.05);
|
||||
else break;
|
||||
if (appliedD > minV + 1e-9) {
|
||||
appliedD = Math.max(minV, appliedD - 0.05);
|
||||
appliedX = Math.max(appliedX, shelfFloor(appliedD));
|
||||
} else if (appliedX > shelfFloor(appliedD) + 1e-9) {
|
||||
appliedX = Math.max(shelfFloor(appliedD), appliedX - 0.05);
|
||||
} else break;
|
||||
}
|
||||
if (!placeable(appliedX, appliedD)) break; // 자동 자리조차 매몰 — 이 단은 불가.
|
||||
appliedD = Math.round(appliedD * 10) / 10;
|
||||
@@ -397,23 +415,23 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
|
||||
m: adjust.m,
|
||||
});
|
||||
|
||||
const slopeRun = autoJointRun + appliedD;
|
||||
const topElevation = src.elevation - slopeRun / FILL_SLOPE_RATIO_MIN;
|
||||
const anchorX = autoOffset + outward * (appliedX + appliedD);
|
||||
const base = topElevation - height; // 근입 0.5 위 기준선
|
||||
const topElevation = autoTop - appliedD;
|
||||
const anchorX = autoOffset + outward * appliedX;
|
||||
const base = topElevation - exposed; // 근입 0.5 위 기준선
|
||||
const wall = buildExtraWall(
|
||||
i,
|
||||
{ offset: anchorX, elevation: base },
|
||||
outward,
|
||||
height,
|
||||
exposed,
|
||||
material,
|
||||
Math.max(0, base - groundAt(anchorX)),
|
||||
);
|
||||
walls.push(wall);
|
||||
// 성토선: src → (수평 선반 x>0이면 선반 끝) → 이음선 상단점. 사면길이 = 경사부.
|
||||
const shelfRun = Math.max(0, appliedX - FILL_SLOPE_RATIO_MIN * appliedD);
|
||||
const shelf: OffsetPoint | null =
|
||||
appliedX > 1e-9
|
||||
? { offset: src.offset + outward * appliedX, elevation: src.elevation }
|
||||
shelfRun > 1e-9
|
||||
? { offset: src.offset + outward * shelfRun, elevation: src.elevation }
|
||||
: null;
|
||||
const slopeFrom = shelf ?? src;
|
||||
const run = Math.abs(wall.topJoint.offset - slopeFrom.offset);
|
||||
@@ -474,7 +492,9 @@ 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 ?? 0) + (mean - (lengths[i] ?? mean)) * horizontalPerSlope,
|
||||
(applied, i) =>
|
||||
(applied.d ?? 0) +
|
||||
((mean - (lengths[i] ?? mean)) * horizontalPerSlope) / FILL_SLOPE_RATIO_MIN,
|
||||
);
|
||||
result = cascadeOnce(dNext);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* 배관만 없을 뿐, 벽 자리·이동·한계는 배관 유출 벽과 한 몸으로 푼다(2026-08-28 사용자:
|
||||
* "배관용에서 배관만 빼면 대부분 동일해야").
|
||||
*
|
||||
* · 자리: `placePipeWall`(배관 공용 4축 배치) — d=0이면 벽 상단이 노견, d로 성토 사면을
|
||||
* 1:1.2 타고 내려간다. base(=invert)는 **성토선을 따라** 정해지고(지반밀착 아님),
|
||||
* 지반보다 뜨면 `floatGap`으로 잡는다. x·d 한계(노견 안쪽 금지·매몰 금지)도 여기서.
|
||||
* · 자리(2026-08-29 사용자 개정): 조작은 **순수 좌우(x)·순수 상하(d)** 0.1m다.
|
||||
* x = 자동 자리에서의 수평 이동, d = 노견에서 잰 수직 하강. 성토선 각도는 유지하고
|
||||
* **노견 확장 구간**이 길이를 흡수한다. 한계는 노견 안쪽 금지·확장 0(각도 유지)·매몰 금지.
|
||||
* 배관 기준벽(`placePipeWall`)의 이동은 손대지 않는다.
|
||||
* · 실제 적용값(한계 절삭 후)은 `appliedAdjust`로 돌려주고, 화면이 조작 저장소에 되받는다
|
||||
* — 눌러도 안 움직이는데 숫자만 커지는 것 방지(배관과 같은 syncApplied 규칙).
|
||||
* · 다단: 배관과 **같은 함수** `buildExtrasAt`. 그리기(폴리곤·이음선·돌·성토부선)는 공용
|
||||
@@ -18,6 +19,8 @@
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
PIPE_CONNECT_GRADE,
|
||||
PIPE_WALL_DEFAULT_RUN_M,
|
||||
REVET_EMBED_DEPTH_M,
|
||||
REVET_LEAN_RATIO,
|
||||
@@ -25,12 +28,8 @@ import {
|
||||
revetHeightLimit,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import {
|
||||
fillWallBaseWidth,
|
||||
groundInterpolator,
|
||||
placePipeWall,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
import { fillWallBaseWidth, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||||
import { buildExtrasAt, slopedCrossing } from "./B06_Section_UI_Cross_Culvert_Extra";
|
||||
import {
|
||||
ZERO_ADJUST,
|
||||
type CulvertDesignTrim,
|
||||
@@ -80,7 +79,7 @@ export interface RevetmentTier {
|
||||
|
||||
export interface RevetmentLayout {
|
||||
side: "left" | "right";
|
||||
/** 1단 벽 높이(m) — 조정창 높이 표시·조작의 기준값. */
|
||||
/** 1단 벽 높이(m) — **바닥~상단**. 조정창 높이 표시·조작의 기준값. */
|
||||
heightM: number;
|
||||
/** 이 단면의 누가거리(m) — 부족 안내를 측점 단위로 센다(재렌더 중복 방지). */
|
||||
chainageM: number;
|
||||
@@ -119,6 +118,29 @@ function sampleLimit(section: CrossSection, outward: number): number | null {
|
||||
return outward > 0 ? Math.max(...offsets) : Math.min(...offsets);
|
||||
}
|
||||
|
||||
/**
|
||||
* 노면 횡단물매를 **바깥 방향 1m당 표고 변화**로 돌려준다(하향이면 음수).
|
||||
* 노면은 측구 쪽으로 내려가는 단일 평면이라(엔진 `road_z`), 노견을 밖으로 연장하면
|
||||
* 그 평면을 그대로 타고 가야 각도가 맞는다(2026-08-29 사용자).
|
||||
* 1순위는 **실제 그려진 노견 구간**(차도 끝 → 노면 끝)의 기울기 — 부호를 추측하지
|
||||
* 않는다. 노견 폭이 0이면 `cross_slope_pct`를 측구 방향 부호로 쓴다.
|
||||
*/
|
||||
function roadSlopePerOutward(
|
||||
design: NonNullable<CrossSection["design"]>,
|
||||
side: "left" | "right",
|
||||
outward: number,
|
||||
): number {
|
||||
const edge = design.road_edges?.[side];
|
||||
const inner = design.carriageway_edges?.[side];
|
||||
if (edge && inner) {
|
||||
const run = (edge.offset_m - inner.offset_m) * outward;
|
||||
if (run > 1e-6) return (edge.elevation_m - inner.elevation_m) / run;
|
||||
}
|
||||
const pct = Number(design.cross_slope_pct);
|
||||
if (!Number.isFinite(pct)) return 0;
|
||||
return (pct / 100) * (design.ditch_side === side ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 독립 기슭막이 단면 — 제원·높이·설계선이 없거나 자리를 못 찾으면 null.
|
||||
* 1단 벽 = 배관 유출 벽과 같은 `placePipeWall`+`buildRevetWallGeometry`,
|
||||
@@ -141,31 +163,65 @@ export function computeRevetmentLayout(
|
||||
const limit = sampleLimit(section, outward);
|
||||
if (!edge || !groundAt || limit === null) return null;
|
||||
|
||||
// 재질 한계로 높이를 자른다(배관 벽과 같은 규칙).
|
||||
const height = Math.min(requestedHeight, revetHeightLimit(spec.form));
|
||||
// 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준,
|
||||
// 2026-08-29 사용자). 재질(형태) 한계도 이 기준으로 자른다.
|
||||
const pureHeight = Math.min(
|
||||
Math.max(requestedHeight, REVET_EMBED_DEPTH_M + 0.1),
|
||||
revetHeightLimit(spec.form),
|
||||
);
|
||||
/** 기준선(근입 위)~상단 — 도형 계산은 종전대로 이 값으로 한다. */
|
||||
const height = pureHeight - REVET_EMBED_DEPTH_M;
|
||||
|
||||
// 배관 유출 벽과 **같은 4축 배치**. 기준(d=0) = 벽 상단이 노견인 자리.
|
||||
const autoOffset = edge.offset_m + outward * (fillWallBaseWidth(height) / 2 - REVET_THICKNESS_M / 2);
|
||||
const placed = placePipeWall({
|
||||
autoOffset,
|
||||
outward,
|
||||
height,
|
||||
baseElevation0: edge.elevation_m - height, // d=0에서 벽 상단 = 노견
|
||||
edgeElevation: edge.elevation_m,
|
||||
invertCap: Number.POSITIVE_INFINITY, // 관 없음 — 위 한계는 d≥0(노견)만
|
||||
adjust: { x: adjust?.x ?? 0, d: adjust?.d ?? null, h: null, m: null },
|
||||
defaultD: PIPE_WALL_DEFAULT_RUN_M,
|
||||
groundAt,
|
||||
limitOffset: limit,
|
||||
requireCrossing: true, // 유출 벽처럼 매몰(원지반 아래 daylight 없음) 금지
|
||||
});
|
||||
const baseElevation = placed.invert;
|
||||
const floatGapM = Math.max(0, baseElevation - groundAt(placed.anchorOffset));
|
||||
const roadSlope = roadSlopePerOutward(design, side, outward);
|
||||
const autoOffset =
|
||||
edge.offset_m + outward * (fillWallBaseWidth(height) / 2 - REVET_THICKNESS_M / 2);
|
||||
// 자동 자리 = 성토사면(1:1.2) 위 기본 지점 — 배관 유출 벽과 같은 기준값.
|
||||
const autoRun = PIPE_WALL_DEFAULT_RUN_M;
|
||||
/**
|
||||
* 조작 축(2026-08-29 사용자): x = **순수 좌우(m)**, d = **노견에서 잰 순수 수직
|
||||
* 하강(m)**. 성토선 각도는 그대로 두고 **노견 확장 구간**이 길이를 흡수한다
|
||||
* (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다).
|
||||
*/
|
||||
const defaultDrop = autoRun / FILL_SLOPE_RATIO_MIN;
|
||||
/** 그 자리에 벽을 세울 수 있나 — 매몰(원지반 아래 daylight 없음) 금지. */
|
||||
const placeable = (xShift: number, drop: number): boolean => {
|
||||
const anchor = autoOffset + outward * (autoRun + xShift);
|
||||
const invert = edge.elevation_m - drop - height;
|
||||
return (
|
||||
invert >= groundAt(anchor) - 0.01 ||
|
||||
slopedCrossing(
|
||||
{ offset: anchor, elevation: invert },
|
||||
outward,
|
||||
-PIPE_CONNECT_GRADE,
|
||||
groundAt,
|
||||
limit,
|
||||
) !== null
|
||||
);
|
||||
};
|
||||
const requestedX = Math.max(adjust?.x ?? 0, 0);
|
||||
/** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리. */
|
||||
const xFloor = (drop: number): number => Math.max(0, FILL_SLOPE_RATIO_MIN * drop - autoRun);
|
||||
let appliedDrop = Math.max(adjust?.d ?? defaultDrop, 0);
|
||||
let appliedX = Math.max(requestedX, xFloor(appliedDrop));
|
||||
for (let pass = 0; pass < 400 && !placeable(appliedX, appliedDrop); pass += 1) {
|
||||
// 매몰 자리 — 요청을 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우).
|
||||
if (appliedDrop > 1e-9) {
|
||||
appliedDrop = Math.max(0, appliedDrop - 0.05);
|
||||
appliedX = Math.max(Math.min(appliedX, requestedX), xFloor(appliedDrop));
|
||||
} else if (appliedX > 1e-9) appliedX = Math.max(0, appliedX - 0.05);
|
||||
else break;
|
||||
}
|
||||
appliedX = Math.round(appliedX * 10) / 10;
|
||||
appliedDrop = Math.round(appliedDrop * 10) / 10;
|
||||
|
||||
const anchorOffset = autoOffset + outward * (autoRun + appliedX);
|
||||
const baseElevation = edge.elevation_m - appliedDrop - height;
|
||||
const floatGapM = Math.max(0, baseElevation - groundAt(anchorOffset));
|
||||
|
||||
const material = materialOfForm(spec.form);
|
||||
const lengthM = Number.isFinite(spec.end_m - spec.start_m) ? spec.end_m - spec.start_m : null;
|
||||
const tier1 = buildRevetWallGeometry({
|
||||
anchor: { offset: placed.anchorOffset, elevation: baseElevation },
|
||||
anchor: { offset: anchorOffset, elevation: baseElevation },
|
||||
outward,
|
||||
height,
|
||||
material,
|
||||
@@ -191,8 +247,31 @@ export function computeRevetmentLayout(
|
||||
|
||||
const walls: WallLayout[] = [tier1, ...extras.walls];
|
||||
const jt = tier1.topJoint;
|
||||
// 노견 → 벽 이음선 상단점 성토선(1:1.2, 벽이 밖으로 밀리면 그만큼 노견이 연장된다).
|
||||
const slope = { points: [{ offset: edge.offset_m, elevation: edge.elevation_m }, jt] };
|
||||
// 노견 → (물매를 탄 노견 연장) → 벽 이음선 상단점. 연장 구간은 노면 물매 그대로 가고,
|
||||
// 꺾임점에서 성토 물매 1:1.2로 벽 상단에 닿는다(2026-08-29 사용자 확정).
|
||||
// D = 노견에서 벽 상단까지 바깥 방향 거리, R0 = 노견과 벽 상단의 낙차
|
||||
// (D − t) = 1.2 × (R0 + roadSlope × t) → t = (D − 1.2·R0) / (1 + 1.2·roadSlope)
|
||||
const edgePoint = { offset: edge.offset_m, elevation: edge.elevation_m };
|
||||
const D = (jt.offset - edge.offset_m) * outward;
|
||||
const R0 = edge.elevation_m - jt.elevation;
|
||||
const denominator = 1 + FILL_SLOPE_RATIO_MIN * roadSlope;
|
||||
const breakRun =
|
||||
R0 > 1e-6 && D > 1e-6 && Math.abs(denominator) > 1e-9
|
||||
? Math.min(D, Math.max(0, (D - FILL_SLOPE_RATIO_MIN * R0) / denominator))
|
||||
: 0;
|
||||
const slope = {
|
||||
points:
|
||||
breakRun > 1e-6
|
||||
? [
|
||||
edgePoint,
|
||||
{
|
||||
offset: edge.offset_m + outward * breakRun,
|
||||
elevation: edge.elevation_m + roadSlope * breakRun,
|
||||
},
|
||||
jt,
|
||||
]
|
||||
: [edgePoint, jt],
|
||||
};
|
||||
const designTrim: CulvertDesignTrim =
|
||||
outward > 0
|
||||
? { minOffset: -Infinity, maxOffset: jt.offset, maxElevation: jt.elevation, maxSlope: slope }
|
||||
@@ -200,14 +279,14 @@ export function computeRevetmentLayout(
|
||||
|
||||
return {
|
||||
side,
|
||||
heightM: height,
|
||||
heightM: pureHeight,
|
||||
chainageM: Number(section.chainage_m) || 0,
|
||||
requestedTiers,
|
||||
top: jt,
|
||||
walls,
|
||||
fillSegments: extras.segments,
|
||||
designTrim,
|
||||
appliedAdjust: { x: placed.x, d: placed.d, h: adjust?.h ?? null, m: null },
|
||||
appliedAdjust: { x: appliedX, d: appliedDrop, h: adjust?.h ?? null, m: null },
|
||||
tiers: walls.map((wall) => ({ polygon: wall.points })),
|
||||
span: {
|
||||
beforeM: Math.max(spec.anchor_m - spec.start_m, 0),
|
||||
|
||||
@@ -86,6 +86,13 @@ export interface StructurePanelHandle {
|
||||
* 높이는 0.1m 눈금(2026-08-22 사용자 확정 2~3m·0.5~3m 구간 0.1 단위 제어).
|
||||
*/
|
||||
const STEP_M = 1.0;
|
||||
/**
|
||||
* 독립·추가 기슭막이의 좌우·상하 한 걸음(m) — **0.1m**(2026-08-29 사용자).
|
||||
* 배관 기준벽(유입·유출)만 관 길이에 맞춘 1m 걸음을 그대로 쓴다.
|
||||
*/
|
||||
const OWN_STEP_M = 0.1;
|
||||
const moveStepOf = (key: RevetKey): number =>
|
||||
key === "inlet" || key === "outlet" ? STEP_M : OWN_STEP_M;
|
||||
const HEIGHT_STEP_M = 0.1;
|
||||
const BASIN_STEP_M = 0.1;
|
||||
/** 전/후 한 걸음(m) — 집수정 기본이 2m·1/1이라 1m 눈금은 너무 거칠다. */
|
||||
@@ -162,13 +169,13 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
"▲",
|
||||
"up",
|
||||
L("B06_Cross_Revet_Up"),
|
||||
act((key) => deps.nudgeSlope(key, -STEP_M)),
|
||||
act((key) => deps.nudgeSlope(key, -moveStepOf(key))),
|
||||
),
|
||||
dpad(
|
||||
"◀",
|
||||
"left",
|
||||
L("B06_Cross_Revet_Left"),
|
||||
act((key) => deps.nudge(key, STEP_M)),
|
||||
act((key) => deps.nudge(key, moveStepOf(key))),
|
||||
),
|
||||
dpad(
|
||||
"↺",
|
||||
@@ -180,13 +187,13 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
"▶",
|
||||
"right",
|
||||
L("B06_Cross_Revet_Right"),
|
||||
act((key) => deps.nudge(key, -STEP_M)),
|
||||
act((key) => deps.nudge(key, -moveStepOf(key))),
|
||||
),
|
||||
dpad(
|
||||
"▼",
|
||||
"down",
|
||||
L("B06_Cross_Revet_Down"),
|
||||
act((key) => deps.nudgeSlope(key, STEP_M)),
|
||||
act((key) => deps.nudgeSlope(key, moveStepOf(key))),
|
||||
),
|
||||
dpad("≡", "equal", L("B06_Cross_Extra_Equalize"), () => deps.equalizeExtras()),
|
||||
);
|
||||
|
||||
@@ -195,8 +195,9 @@ export function culvertCardState(layout: CulvertLayout): {
|
||||
const wallSpecs = new Map<RevetKey, { height: number; material: RevetMaterial }>();
|
||||
const specOf = (wall: CulvertLayout["walls"][number], key: RevetKey): void => {
|
||||
wallSpecs.set(key, {
|
||||
// 배관 벽은 순수 높이(바닥~상단 = 계산용 + 근입 0.5 — 2026-08-23 사용자).
|
||||
height: wall.role === "extra" ? wall.height : wall.height + REVET_EMBED_DEPTH_M,
|
||||
// 조정창 높이 = 순수 높이(바닥~상단 = 계산용 + 근입 0.5). 추가 기슭막이도
|
||||
// 같은 기준이다(2026-08-23 배관 벽 → 2026-08-29 추가 벽).
|
||||
height: wall.height + REVET_EMBED_DEPTH_M,
|
||||
material: wall.material,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user