fix(B06): 조정 단위를 관 길이 1m로·벽 형상 불변·사면 역전 차단
지적 3건: - 우측(유출) 기슭막이만 좌우로 밀 때 단면이 변했다. 0.1m씩 미니 관 길이 정수의 끝수를 벽 폭이 흡수해 0.45→0.90까지 뚱뚱해졌다. 조정 단위를 **관 길이 1m**로 바꾸고, 조정창으로 옮긴 벽은 폭을 절대 건드리지 않게 했다. 자리만 옮기면 정수 길이가 성립하므로 형상이 그대로다(검증: 두께 0.450 고정, L 1m씩 증감). - 성토 사면이 역전되는 자리가 있었다. 벽 상단이 노견보다 높아지면 사면선이 위로 올라간다 — 최소 성토고 0.3m(FILL_MIN_RISE_M)를 두고 그 앞에서 멈춘다. - 이동이 노견을 넘어 도로 반대편까지 갔다. 벽은 제 노견 바깥에만 서게 막았다. 조정창은 "관 길이 8m · 바깥 2.0m"처럼 관 길이를 같이 적는다. 700줄 유지를 위해 clampWallOffset·cutLength·outletSlopeFactory를 _Solve로 옮겼다. 검증: 이동 ±5m 20케이스 역전 0건·두께 고정, 275.71 유출 이동 11↔7m 정상, 계획고 스윕 24케이스 회귀 문제 0. tsc 통과, pytest 148 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,3 +105,12 @@ export const FILL_SLOPE_MAX_LENGTH_M = 5.0;
|
||||
* 끝이 아니라 **사면길이 5m 지점**으로 끌어올리는 판정에 쓴다(2026-08-21 사용자 지시).
|
||||
*/
|
||||
export const FILL_STRUCTURE_HEIGHT_M = 3.0;
|
||||
|
||||
/**
|
||||
* 성토 사면이 성립하는 **최소 성토고**(m) — 노견 − 벽 이음선 상단.
|
||||
*
|
||||
* 이보다 낮은 자리에 벽을 세우면 사면선이 노견에서 벽 상단으로 **올라가** 역전된다
|
||||
* (2026-08-21 사용자 지적). 자동 배치는 물매 조건이 이미 막지만, 조정창으로 손수
|
||||
* 미는 경우는 이 하한으로 막는다.
|
||||
*/
|
||||
export const FILL_MIN_RISE_M = 0.3;
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Cross_Culvert_Geom.ts
|
||||
* 배수관 세트(배관·기슭막이·보호공) **기하 계산** — 그리기(`_Cross_Culvert.ts`)와 분리.
|
||||
* 백엔드가 만든 `section.culvert` 제원을 좌표로 옮긴다 — 치수를 새로 정하지 않는다.
|
||||
*
|
||||
* 백엔드(`B06_Section_Engine_Culvert`)가 `pipe_points.json` 정본 + 레지스트리 기본값으로
|
||||
* 만든 `section.culvert` 제원을 좌표로 옮기기만 한다 — 여기서 치수를 새로 정하지 않는다.
|
||||
*
|
||||
* 배치 규칙 — **실무 횡단도 4장 기준으로 재정립**(2026-08-20 사용자 제공, 울진 계열
|
||||
* `관매설 Φ800~1000 · 기슭막이 유입 찰/유출 메 H=1.5 · 돌붙임(메붙임) ㎡` 주기):
|
||||
* · 기슭막이 2기 — **도로면 양 가장자리**(유입 = 절토측 측구부, 유출 = 성토측 노견
|
||||
* 바깥 = 성토사면 최상단)에 선다. 형상은 사면 쪽으로 **기운 평행사변형**(전면 1:0.3,
|
||||
* 두께 0.45m), 상단 ≈ 노면고.
|
||||
* · 배관 — 두 벽 사이(도로 하부)만 직선으로 지난다. 단면 항상 직사각형(축 법선 두께),
|
||||
* 끝단면은 벽 전면과 평행하게 마감. 수평 가능. 길이 표기는 m 단위 올림.
|
||||
* · 보호공(돌붙임) — **관 하단 꼭짓점**에서 시작해 성토사면 경사(1:n)를 따라 원지반까지,
|
||||
* 최소 보호공 길이(낙차고×2)를 채울 때까지 지반을 따라 더 간다.
|
||||
* · 유입측 — 수평 보호공 없음. 측구 접속은 설계선(표준단면 측구)이 이미 표현한다.
|
||||
* 배치 규칙 — 실무 횡단도 4장 기준(2026-08-20 사용자 제공, 울진 계열 주기):
|
||||
* · 기슭막이 2기 — 유입/유출측 성토사면 안. 사면 쪽으로 기운 평행사변형(전면 1:0.3,
|
||||
* 두께 0.45m). 자리는 사면선이 이음선 상단점을 지나도록 풀어서 정한다.
|
||||
* · 배관 — 두 벽 사이(도로 하부)만 직선. 끝단면은 벽 전면과 평행하게 마감(수평 가능).
|
||||
* · 보호공(돌붙임) — 관 하단 꼭짓점에서 성토사면 경사를 따라 원지반까지, 최소 길이
|
||||
* (낙차고×2)를 채울 때까지 지반을 따라 더 간다. 유입측은 수평 보호공 없음.
|
||||
* · 성토 경사 — 벽 전면과 교차된 이후는 끊는다(designTrim — 벽·보호공이 대신한다).
|
||||
*
|
||||
* 유입 = 상단측(uphill_side), 유출 = 하단측. 유입구가 "집수정"이면 그쪽 기슭막이는
|
||||
@@ -23,6 +17,7 @@
|
||||
|
||||
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
FILL_MIN_RISE_M,
|
||||
FILL_SLOPE_MAX_LENGTH_M,
|
||||
FILL_STRUCTURE_HEIGHT_M,
|
||||
FILL_SLOPE_RATIO_MAX,
|
||||
@@ -50,6 +45,9 @@ export * from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
export * from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
import {
|
||||
clampWallOffset,
|
||||
cutLength,
|
||||
outletSlopeFactory,
|
||||
fillSlopeOf,
|
||||
groundInterpolator,
|
||||
designInterpolator,
|
||||
@@ -117,8 +115,7 @@ export function computeCulvertLayout(
|
||||
: null;
|
||||
|
||||
// 성토측 유입 기슭막이는 노견에 붙여 두지 않는다 — 사면선이 벽 이음선 상단점을 지나야
|
||||
// 하므로 자리를 풀어서 정한다(2026-08-21 사용자 ②). 집수정(절토·막힘)은 측구부 자리
|
||||
// 그대로 둔다. 벽 높이는 관경 + 여유고(0.1m 눈금 올림).
|
||||
// 하므로 자리를 풀어서 정한다(집수정은 측구부 자리 그대로).
|
||||
const wallHeightFor = (spec: CulvertSideSpec): number =>
|
||||
Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form));
|
||||
let inletPlacement: FillWallPlacement | null = null;
|
||||
@@ -134,8 +131,17 @@ export function computeCulvertLayout(
|
||||
slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit),
|
||||
);
|
||||
if (inletPlacement) {
|
||||
// 사용자가 화살표로 민 만큼 자동 자리에서 더 옮긴다(2026-08-21 사용자 ①).
|
||||
const shifted = inletPlacement.offset + inletInfo.outward * (revetShift?.inlet ?? 0);
|
||||
// 사용자가 조정창으로 민 만큼 자동 자리에서 더 옮긴다 — 사면 역전 구간은 막는다.
|
||||
const inletBaseAt = (offset: number): number =>
|
||||
Math.min(groundAt(offset), invertCap(inletInfo.edge));
|
||||
const shifted = clampWallOffset(
|
||||
inletPlacement.offset,
|
||||
inletPlacement.offset + inletInfo.outward * (revetShift?.inlet ?? 0),
|
||||
wallHeightFor(culvert.inlet),
|
||||
inletInfo.edge,
|
||||
inletBaseAt,
|
||||
inletInfo.outward,
|
||||
);
|
||||
inlet.offset = shifted;
|
||||
inlet.elevation = Math.min(groundAt(shifted), invertCap(inletInfo.edge));
|
||||
}
|
||||
@@ -167,8 +173,7 @@ export function computeCulvertLayout(
|
||||
const rise0 = outletAnchor.elevation - inlet.elevation;
|
||||
const length0 = Math.hypot(run0, rise0);
|
||||
if (!(length0 > 0.5)) return null;
|
||||
// 관은 m 단위 설치 — 올림 연장분은 유출 쪽으로(기슭막이를 조금 벗어나도 무관,
|
||||
// 2026-08-20 사용자 확정). 실무 표기 L=10~12m대가 여기서 나온다.
|
||||
// 관은 m 단위 설치 — 올림 연장분은 유출 쪽으로(2026-08-20 사용자 확정).
|
||||
let lengthM = Math.ceil(length0 - 1e-6);
|
||||
const scale = lengthM / length0;
|
||||
const outlet: OffsetPoint = {
|
||||
@@ -346,9 +351,8 @@ export function computeCulvertLayout(
|
||||
};
|
||||
return null;
|
||||
}
|
||||
// 형상(사용자 스케치 확정 — 좌측 벽 기준, 우측은 반전): **배면(도로측) 수직**,
|
||||
// **전면(계류측)은 1:0.3 기운 평행사변형 띠**. 띠 안쪽 평행선과 수직 배면 사이가
|
||||
// 사다리꼴 — 합집합의 내부 대각 이음선은 배면 상단에서 바닥으로 내려온다.
|
||||
// 형상(사용자 스케치 확정 — 좌측 벽 기준, 우측은 반전): 배면(도로측) 수직, 전면
|
||||
// (계류측)은 1:0.3 기운 평행사변형 띠. 띠 안쪽 평행선과 수직 배면 사이가 사다리꼴로,
|
||||
// 바닥이 넓고 상단이 좁다(상단 폭 = 띠 두께 0.45).
|
||||
// 높이: **관경 + 여유고**를 0.1m 눈금으로 올린 값(2026-08-21 사용자 확정). 관이
|
||||
// 물매를 두어 좌·우 invert가 달라도 **관 상단 위 여유고는 같아야** 한다. 사면선은
|
||||
@@ -429,7 +433,14 @@ export function computeCulvertLayout(
|
||||
outletAnchorOffset,
|
||||
);
|
||||
if (outletPlacement) {
|
||||
const shifted = outletPlacement.offset + outletInfo.outward * (revetShift?.outlet ?? 0);
|
||||
const shifted = clampWallOffset(
|
||||
outletPlacement.offset,
|
||||
outletPlacement.offset + outletInfo.outward * (revetShift?.outlet ?? 0),
|
||||
wallHeightFor(culvert.outlet),
|
||||
outletInfo.edge,
|
||||
invertAt,
|
||||
outletInfo.outward,
|
||||
);
|
||||
outletWallAnchor = { offset: shifted, elevation: invertAt(shifted) };
|
||||
}
|
||||
}
|
||||
@@ -490,27 +501,7 @@ export function computeCulvertLayout(
|
||||
// 길이가 다르다** — 긴 변을 기준으로 올림해 정수 m로 잡고, 모자란 만큼 **유출 벽 자리**
|
||||
// 를 관 축 방향 바깥으로 민다(벽 두께는 0.45 고정 — 사용자 확정). 벽 상단이 사면선과
|
||||
// 벌어지는 문제는 설계선 트림이 벽 상단을 따라오므로 생기지 않는다(사용자 ②).
|
||||
const cutLength = (corners: { inlet: PipeEnd; outlet: PipeEnd }): number =>
|
||||
Math.max(
|
||||
Math.hypot(
|
||||
corners.outlet.bottom.offset - corners.inlet.bottom.offset,
|
||||
corners.outlet.bottom.elevation - corners.inlet.bottom.elevation,
|
||||
),
|
||||
Math.hypot(
|
||||
corners.outlet.top.offset - corners.inlet.top.offset,
|
||||
corners.outlet.top.elevation - corners.inlet.top.elevation,
|
||||
),
|
||||
);
|
||||
/** 그 자리에 벽을 세웠을 때의 성토 사면 — 물매(1:n)와 사면길이(노견 → 이음선 상단점). */
|
||||
const outletSlopeAt = (offset: number, thickness: number): { ratio: number; lengthM: number } => {
|
||||
const joint = offset + outletInfo.outward * (thickness / 2);
|
||||
const rise = outletInfo.edge.elevation_m - (invertAt(offset) + wallHeightFor(culvert.outlet));
|
||||
const run = Math.abs(joint - outletInfo.edge.offset_m);
|
||||
return {
|
||||
ratio: rise > 1e-6 ? run / rise : Number.POSITIVE_INFINITY,
|
||||
lengthM: Math.hypot(run, Math.max(rise, 0)),
|
||||
};
|
||||
};
|
||||
const outletSlopeAt = outletSlopeFactory(outletInfo, invertAt, wallHeightFor(culvert.outlet));
|
||||
/** 유출 벽 두께 — 자리 이동만으로 관 길이를 정수로 못 맞출 때만 0.45에서 벗어난다. */
|
||||
let outletThickness = REVET_THICKNESS_M;
|
||||
if (outletWall) {
|
||||
@@ -535,15 +526,25 @@ export function computeCulvertLayout(
|
||||
const gap = target - cutLength(pipeCorners);
|
||||
if (Math.abs(gap) < 0.005) break;
|
||||
// ① 벽 자리를 관 축 방향으로 gap 만큼 옮긴다(두께 불변 — 사용자 확정 우선순위).
|
||||
// 단 사용자가 화살표로 자리를 지정했으면 그 자리를 지키고 폭으로만 흡수한다.
|
||||
const pinned = Math.abs(revetShift?.outlet ?? 0) > 1e-9;
|
||||
const moved = outletWallAnchor.offset + axis.offset * gap;
|
||||
const movedSlope = outletSlopeAt(moved, outletThickness);
|
||||
// 사면 역전·노견 안쪽으로는 옮기지 않는다 — 길이 맞춤보다 도면 성립이 먼저다.
|
||||
if (
|
||||
!pinned &&
|
||||
movedSlope.ratio >= FILL_SLOPE_RATIO_MIN &&
|
||||
movedSlope.ratio <= FILL_SLOPE_RATIO_MAX &&
|
||||
movedSlope.lengthM <= FILL_SLOPE_MAX_LENGTH_M + 1e-6
|
||||
(moved - outletInfo.edge.offset_m) * outletInfo.outward < 0 ||
|
||||
outletInfo.edge.elevation_m - (invertAt(moved) + wallHeightFor(culvert.outlet)) <
|
||||
FILL_MIN_RISE_M
|
||||
) {
|
||||
break;
|
||||
}
|
||||
// 조정창으로 옮긴 벽은 **폭을 건드리지 않는다** — 폭으로 흡수하면 미는 동안 벽
|
||||
// 단면이 변한다(2026-08-21 사용자 지적: 유출 벽만 0.45→0.90). 조정이 관 길이 1m
|
||||
// 단위라 자리만 맞추면 정수가 성립한다. 물매·5m 한계는 자동 배치용 제약이라
|
||||
// 사용자가 지정한 자리에는 걸지 않는다(경고로 알린다).
|
||||
if (
|
||||
Math.abs(revetShift?.outlet ?? 0) > 1e-9 ||
|
||||
(movedSlope.ratio >= FILL_SLOPE_RATIO_MIN &&
|
||||
movedSlope.ratio <= FILL_SLOPE_RATIO_MAX &&
|
||||
movedSlope.lengthM <= FILL_SLOPE_MAX_LENGTH_M + 1e-6)
|
||||
) {
|
||||
outletWallAnchor = { offset: moved, elevation: invertAt(moved) };
|
||||
rebuild();
|
||||
@@ -633,8 +634,8 @@ export function computeCulvertLayout(
|
||||
}
|
||||
}
|
||||
|
||||
// ── 성토 사면 구간 확정. 벽 자리가 다 굳은 뒤에 물매를 역산해야 관 길이 맞춤(벽 이동)
|
||||
// 이 접점을 다시 깨뜨리지 않는다 — 종전 어긋남의 직접 원인이 이 순서였다.
|
||||
// ── 성토 사면 구간 확정. 벽 자리가 굳은 뒤에 물매를 역산해야 관 길이 맞춤(벽 이동)이
|
||||
// 접점을 다시 깨뜨리지 않는다 — 종전 어긋남의 직접 원인이 이 순서였다.
|
||||
const slopeOf = (wall: WallLayout): FillSlopeSegment =>
|
||||
fillSlopeOf(wall, wall.role === "inlet" ? inletInfo.edge : outletInfo.edge);
|
||||
for (const wall of walls) {
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
|
||||
import type { SectionSample } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
FILL_MIN_RISE_M,
|
||||
FILL_SLOPE_MAX_LENGTH_M,
|
||||
FILL_SLOPE_RATIO_MAX,
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
FILL_STRUCTURE_HEIGHT_M,
|
||||
REVET_TRAP_TOP_M,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { OffsetPoint, PipeEnd } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
|
||||
/** 유효 지반 샘플 → offset 오름차순 보간기. 범위 밖은 끝값 클램프, 샘플 없으면 null. */
|
||||
export function groundInterpolator(samples: SectionSample[]): ((offset: number) => number) | null {
|
||||
@@ -219,3 +220,65 @@ export function fillSlopeOf(
|
||||
to,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 손으로 민 벽 자리를 **사면이 역전되지 않는 범위**로 되돌린다(2026-08-21 사용자 지적).
|
||||
* 노견보다 벽 상단이 높아지면 사면선이 위로 올라가 버린다 — 자동 자리에서 목표 자리로
|
||||
* 훑으며 성토고가 하한 아래로 떨어지기 직전까지만 간다.
|
||||
*/
|
||||
export function clampWallOffset(
|
||||
autoOffset: number,
|
||||
wantedOffset: number,
|
||||
height: number,
|
||||
edge: { offset_m: number; elevation_m: number },
|
||||
baseAt: (offset: number) => number,
|
||||
outward: number,
|
||||
): number {
|
||||
const steps = 40;
|
||||
let allowed = autoOffset;
|
||||
for (let i = 1; i <= steps; i += 1) {
|
||||
const candidate = autoOffset + ((wantedOffset - autoOffset) * i) / steps;
|
||||
// 벽은 제 노견 바깥에 선다 — 안쪽으로 넘기면 도로 밑을 파고들어 반대편까지 간다.
|
||||
if ((candidate - edge.offset_m) * outward < 0) break;
|
||||
if (edge.elevation_m - (baseAt(candidate) + height) < FILL_MIN_RISE_M) break;
|
||||
allowed = candidate;
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관 절단 길이(m) — 끝이 기운 벽 전면으로 잘려 **상단·하단 길이가 다르다**.
|
||||
* 표기·정수 맞춤은 **긴 변** 기준이다(2026-08-21 사용자 확정).
|
||||
*/
|
||||
export function cutLength(corners: { inlet: PipeEnd; outlet: PipeEnd }): number {
|
||||
return Math.max(
|
||||
Math.hypot(
|
||||
corners.outlet.bottom.offset - corners.inlet.bottom.offset,
|
||||
corners.outlet.bottom.elevation - corners.inlet.bottom.elevation,
|
||||
),
|
||||
Math.hypot(
|
||||
corners.outlet.top.offset - corners.inlet.top.offset,
|
||||
corners.outlet.top.elevation - corners.inlet.top.elevation,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 유출 벽을 그 자리에 세웠을 때의 성토 사면(물매 1:n·사면길이)을 재는 함수를 만든다.
|
||||
* 관 길이 정수 맞춤이 벽을 옮겨도 되는 자리인지 판정하는 데 쓴다.
|
||||
*/
|
||||
export function outletSlopeFactory(
|
||||
info: { edge: { offset_m: number; elevation_m: number }; outward: number },
|
||||
invertAt: (offset: number) => number,
|
||||
wallHeight: number,
|
||||
): (offset: number, thickness: number) => { ratio: number; lengthM: number } {
|
||||
return (offset, thickness) => {
|
||||
const joint = offset + info.outward * (thickness / 2);
|
||||
const rise = info.edge.elevation_m - (invertAt(offset) + wallHeight);
|
||||
const run = Math.abs(joint - info.edge.offset_m);
|
||||
return {
|
||||
ratio: rise > 1e-6 ? run / rise : Number.POSITIVE_INFINITY,
|
||||
lengthM: Math.hypot(run, Math.max(rise, 0)),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface StructurePanelDeps {
|
||||
nudge: (key: RevetKey, screenDeltaM: number) => void;
|
||||
/** 자동 자리로 되돌린다. */
|
||||
reset: (key: RevetKey) => void;
|
||||
/** 지금 관 길이(m) — 조정 단위가 관 길이 1m이라 창에 같이 적는다. */
|
||||
pipeLengthM: () => number | null;
|
||||
/** 창을 닫는다 = 구조물 선택 해제. */
|
||||
close: () => void;
|
||||
}
|
||||
@@ -31,8 +33,12 @@ export interface StructurePanelHandle {
|
||||
show: (key: RevetKey | null) => void;
|
||||
}
|
||||
|
||||
/** 한 걸음 이동량(m) — 벽 높이 눈금(0.1m)과 같은 단위로 맞춘다. */
|
||||
const STEP_M = 0.1;
|
||||
/**
|
||||
* 한 걸음 이동량(m) — **관 길이 1m**에 맞춘다(2026-08-21 사용자 확정).
|
||||
* 관은 m 단위로 설치하므로 0.1m씩 밀면 그 끝수를 벽 폭이 흡수하게 되어 미는 동안
|
||||
* 벽 단면이 변한다. 한 걸음을 1m로 두면 벽은 자리만 옮기고 형상은 그대로다.
|
||||
*/
|
||||
const STEP_M = 1.0;
|
||||
|
||||
function makeButton(label: string, title: string, onClick: () => void): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
@@ -105,10 +111,13 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
|
||||
// 자동 자리 기준 이동량 — 0이면 "자동"으로 적어 손을 안 댔음을 바로 알린다.
|
||||
// 부호(+/−)만 적으면 좌·우 벽에서 어느 쪽인지 읽히지 않아 **안/바깥**으로 적는다.
|
||||
const direction = L(shift > 0 ? "B06_Cross_Revet_Outward" : "B06_Cross_Revet_Inward");
|
||||
value.textContent =
|
||||
const moved =
|
||||
Math.abs(shift) < 1e-9
|
||||
? L("B06_Cross_Revet_Auto")
|
||||
: `${direction} ${Math.abs(shift).toFixed(1)}m`;
|
||||
const pipeLength = deps.pipeLengthM();
|
||||
value.textContent =
|
||||
pipeLength === null ? moved : `${L("B06_Cross_Revet_Pipe")} ${pipeLength}m · ${moved}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,6 +212,8 @@ export function createCrossSectionCard(
|
||||
let activeRevet: RevetKey | null = revetOffset?.selectedFor(section) ?? null;
|
||||
let setRevetActive: RevetHighlightSetter = () => undefined;
|
||||
let showRevetControl: (visible: boolean) => void = () => undefined;
|
||||
/** 지금 그린 관 길이(m) — 조정창이 "관 길이 8m"으로 적는다. 배수관 측점이 아니면 null. */
|
||||
let culvertPipeLengthM: number | null = null;
|
||||
/**
|
||||
* 기슭막이를 고른다. 구조물 선택과 절·성토 면적 강조는 **같은 레벨**이라 하나를
|
||||
* 고르면 다른 하나는 풀린다(2026-08-21 사용자). 또 구조물을 고르면 그 **측점 카드도
|
||||
@@ -450,7 +452,7 @@ export function createCrossSectionCard(
|
||||
toggleArea,
|
||||
);
|
||||
// 배수관 세트 기하를 먼저 계산한다 — 설계선이 기슭막이 밖 성토 경사를 끊는 데 쓴다.
|
||||
const culvertLayout = computeCulvertLayout(
|
||||
const culvertLayout: ReturnType<typeof computeCulvertLayout> = computeCulvertLayout(
|
||||
section,
|
||||
sourceSamples,
|
||||
revetOffset
|
||||
@@ -481,6 +483,7 @@ export function createCrossSectionCard(
|
||||
);
|
||||
}
|
||||
// 배수관 측점 세트(배관·기슭막이·보호공) — 기존 도형 위에 추가만 한다(2026-08-19).
|
||||
culvertPipeLengthM = culvertLayout?.pipe.lengthM ?? null;
|
||||
if (culvertLayout) {
|
||||
setRevetActive = appendCulvertOverlay(
|
||||
plotLayer,
|
||||
@@ -571,6 +574,7 @@ export function createCrossSectionCard(
|
||||
nudge: (key, screenDeltaM) =>
|
||||
revetOffset?.adjust(section.chainage_m, key, screenDeltaM * outwardOf(key)),
|
||||
reset: (key) => revetOffset?.reset(section.chainage_m, key),
|
||||
pipeLengthM: () => culvertPipeLengthM,
|
||||
close: () => {
|
||||
if (activeRevet) toggleRevet(activeRevet);
|
||||
},
|
||||
|
||||
@@ -135,9 +135,9 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
}
|
||||
}
|
||||
|
||||
/** 손으로 미는 범위 한계(m) — 벽이 노면 밑이나 사면 밖으로 달아나지 않게 묶는다. */
|
||||
/** 손으로 미는 범위 한계(m) — 조정 단위가 관 길이 1m이라 ±10m(=관 10m분)까지 둔다. */
|
||||
const clampRevetShift = (value: number): number =>
|
||||
Math.min(Math.max(Math.round(value * 10) / 10, -3), 3);
|
||||
Math.min(Math.max(Math.round(value * 10) / 10, -10), 10);
|
||||
|
||||
const revetOffsetControl: RevetOffsetControl = {
|
||||
shiftFor: (section, role) => revetShifts.get(revetKey(section.chainage_m, role)) ?? 0,
|
||||
|
||||
Reference in New Issue
Block a user