Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1

This commit is contained in:
2026-10-12 21:58:59 +09:00
28 changed files with 331 additions and 63 deletions
+1
View File
@@ -166,6 +166,7 @@ export function appendBoxOverlay(
box.chute,
x,
toDisplayY,
layout.ground,
);
// ⑥ 끝 바깥 절 · 성토선 — 세월교 측벽과 같은 체계(79-7). 흙막이 쪽은 기하가 걷어 둠.
for (const side of layout.sides) {
@@ -16,6 +16,7 @@
* 부재 두께는 세월교 승계(측벽 0.2·상판 0.3·저판 0.3)이며 백엔드가 실어 보낸다.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import type { BoxSet, CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import type {
BasinLayout,
@@ -74,6 +75,8 @@ export interface BoxSideLayout {
export interface BoxLayout {
box: BoxSet;
/** 원지반 — 도수로 · 돌붙임 길이가 사면보다 길면 이어 그림(294-4). */
ground?: OffsetPoint[];
sides: BoxSideLayout[];
/** 상판 폴리곤(좌·우 표고가 다르면 기울어진다). */
topSlab: OffsetPoint[];
@@ -222,6 +225,7 @@ export function computeBoxLayout(
const apronLengthOf = (side: BoxSideLayout): number =>
side.apron.length ? Math.abs(side.apron[1].offset - side.apron[0].offset) : 0;
return {
ground: groundLine(groundSamples),
box,
sides: [left, right],
topSlab: quad(0, barrelTopDrop),
@@ -125,6 +125,7 @@ export function appendCulvertOverlay(
culvert.chute,
x,
toDisplayY,
layout.ground,
);
for (const segment of layout.outletFill.segments) {
// 묻힌 유출 끝 = 3도 수평 절토선(보라 · 면적에도 듦 · 139-4) — 3도 선이 원지반을
@@ -6,6 +6,7 @@
* (designTrim). 유입 = 상단측. 집수정은 `_Basin.ts`, 성토부·다단은 `_Extra.ts`.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
import {
revetWallSpec,
@@ -575,6 +576,7 @@ export function computeCulvertLayout(
const outletSlope = outletWallFinal ? slopeOf(outletWallFinal) : null;
return {
ground: groundLine(groundSamples),
inletOptions,
culvert,
pipe: { inlet: pipeStart, outlet, lengthM, slopePct },
@@ -201,6 +201,8 @@ export interface OutletFillSegment {
export interface CulvertLayout {
culvert: CulvertSet;
/** 원지반 — 도수로 · 돌붙임 길이가 사면보다 길면 이어 그림(294-4). */
ground?: OffsetPoint[];
pipe: { inlet: OffsetPoint; outlet: OffsetPoint; lengthM: number; slopePct: number };
/** 관 축·법선 단위벡터(그리기가 두께 방향으로 밀 때 쓴다). */
pipeAxis: { axis: OffsetPoint; normal: OffsetPoint };
+1
View File
@@ -144,6 +144,7 @@ export function appendFordOverlay(
ford.chute,
x,
toDisplayY,
layout.ground,
);
// ④ 계류측 마감선 — 구조물이 원지반 위면 성토부선, 안이면 절토선(집수정과 같은 체계).
for (const side of layout.sides) {
@@ -19,6 +19,7 @@
* 성토측이라도 기슭막이 세트(다단·재질)와는 연계하지 않는다.
* ========================================================================== */
import { groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import type { CrossSection, FordSet, SectionSample } from "./B06_Section_Api_Fetch";
import { GUARD_CUT_BERM_M, pipeWallThicknessM } from "./B06_Section_UI_Cross_Culvert_Const";
import type {
@@ -108,6 +109,8 @@ export interface FordSideLayout {
export interface FordLayout {
ford: FordSet;
/** 원지반 — 도수로 · 돌붙임 길이가 사면보다 길면 이어 그림(294-4). */
ground?: OffsetPoint[];
sides: FordSideLayout[];
/** 두 측벽 사이를 잇는 바닥판. */
slabBridge: OffsetPoint[];
@@ -329,6 +332,7 @@ export function computeFordLayout(
const countText = ford.pipe_count > 1 ? ` ${ford.pipe_count}련` : "";
return {
ground: groundLine(groundSamples),
ford,
sides,
slabBridge,
@@ -13,7 +13,7 @@
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import { appendPavementOverlay } from "./B06_Section_UI_Cross_Design_Surface";
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
import { appendChuteBand, chuteRun } from "./B06_Section_UI_Cross_Wall_Hatch";
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
export interface FordPavementSpec {
@@ -102,6 +102,8 @@ export function appendFordPavementOverlay(
design: CrossDesign,
x: (offset: number) => number,
y: (elevation: number) => number,
/** 원지반 — 도수로 띠가 원지반에 닿으면 끝 · 길이가 사면보다 길면 이어 그림(294-4). */
ground?: readonly OffsetPoint[],
): boolean {
const edges = fordEdges(design);
if (!spec || !edges) return false;
@@ -130,7 +132,7 @@ export function appendFordPavementOverlay(
),
);
if (spec.chute)
appendChuteBand(svg, [{ points: fordChuteLine(design, spec.chute) }], spec.chute, x, y);
appendChuteBand(svg, [{ points: fordChuteLine(design, spec.chute, ground) }], spec.chute, x, y);
return true;
}
@@ -143,18 +145,38 @@ export function fordEdges(design: CrossDesign): CrossDesign["road_edges"] | unde
return reach.side === "left" ? { ...edges, left: end } : { ...edges, right: end };
}
/** 원지반 꺾은선의 그 오프셋 표고(밖은 끝값). */
function groundZ(ground: readonly OffsetPoint[], offset: number): number | null {
if (!ground.length) return null;
if (offset <= ground[0].offset) return ground[0].elevation;
for (let i = 1; i < ground.length; i += 1) {
const [a, b] = [ground[i - 1], ground[i]];
if (offset > b.offset) continue;
const span = b.offset - a.offset;
return span > 1e-9
? a.elevation + ((b.elevation - a.elevation) * (offset - a.offset)) / span
: b.elevation;
}
return ground[ground.length - 1].elevation;
}
/**
* 유출측(`inflowLeft` 반대) 노견 바깥 성토사면 선(136-2) — 설계선을 노견에서 바깥으로 따라
* 내려가는 동안만(오르면 절토라 멈춤). 성토가 없으면 빈 선(띠 안 그림).
* 유출측(낮은 쪽) 성토사면 선(136-2 · 294-4) — 낮은 쪽 끝단(늘인 노면 끝 · 아니면 유출 쪽 노견 끝)에서
* 설계선을 바깥으로 따라 내려가는 동안만(오르면 절토라 멈춤) · 원지반(`ground`)에 닿으면 끝.
* 성토가 없으면(끝단이 이미 원지반 · 양측 절토 물넘이) 점 하나 = 띠 없음.
*/
export function outletFillLine(design: CrossDesign): OffsetPoint[] {
const edges = design.road_edges;
export function outletFillLine(
design: CrossDesign,
ground?: readonly OffsetPoint[],
): OffsetPoint[] {
const edges = fordEdges(design);
if (!edges || !design.design_line?.length) return [];
const outward = inflowLeft(design) ? -1 : 1; // +offset = 좌측 · 유입 반대
const edge =
outward > 0
? Math.max(edges.left.offset_m, edges.right.offset_m)
: Math.min(edges.left.offset_m, edges.right.offset_m);
const reach = design.tilt_reach;
const outLeft = reach ? reach.side === "left" : !inflowLeft(design); // +offset = 좌측
const outward = outLeft ? 1 : -1;
const edge = outLeft
? Math.max(edges.left.offset_m, edges.right.offset_m)
: Math.min(edges.left.offset_m, edges.right.offset_m);
const points = design.design_line
.filter((p) => (p.offset_m - edge) * outward >= -1e-6)
.sort((a, b) => (a.offset_m - b.offset_m) * outward);
@@ -163,31 +185,25 @@ export function outletFillLine(design: CrossDesign): OffsetPoint[] {
const last = line[line.length - 1];
if (last && p.elevation_m > last.elevation + 1e-6) break;
line.push({ offset: p.offset_m, elevation: p.elevation_m });
const z = ground ? groundZ(ground, p.offset_m) : null;
if (z !== null && Math.abs(z - p.elevation_m) < 1e-3) break; // 원지반에 닿음 = 끝
}
return line;
}
/**
* 물넘이 도수로 · 돌붙임 선(286-3) — 유출 성토사면 선을 노견에서 사면 따라 `length_m` 까지 자름 ·
* 비거나 사면보다 길면 원지반까지. 횡단 띠 · 3D 띠가 같이 씀(그림 전용).
* 물넘이 도수로 · 돌붙임 선(286-3 · 294-4) — 낮은 쪽 끝단에서 성토사면 따라 · 길이 비움 = 원지반 닿으면 끝 ·
* 길이 넣음 = 그 길이(사면보다 길면 원지반 따라 이어) · 양측 절토(늘인 노면 끝이 원지반) = 길이 없으면 안 그림.
* 횡단 띠 · 3D 띠가 같이 씀(그림 전용).
*/
export function fordChuteLine(design: CrossDesign, chute?: { length_m?: number }): OffsetPoint[] {
const line = outletFillLine(design);
let left = chute?.length_m ?? 0;
if (!(left > 0)) return line;
const out = line.slice(0, 1);
for (let i = 1; i < line.length && left > 1e-9; i += 1) {
const [a, b] = [line[i - 1], line[i]];
const step = Math.hypot(b.offset - a.offset, b.elevation - a.elevation);
if (step < 1e-9) continue;
const t = Math.min(left / step, 1);
out.push({
offset: a.offset + (b.offset - a.offset) * t,
elevation: a.elevation + (b.elevation - a.elevation) * t,
});
left -= step;
}
return out;
export function fordChuteLine(
design: CrossDesign,
chute?: { length_m?: number },
ground?: readonly OffsetPoint[],
): OffsetPoint[] {
return (
chuteRun([{ points: outletFillLine(design, ground) }], chute?.length_m, ground)[0]?.points ?? []
);
}
/**
+2 -1
View File
@@ -24,7 +24,7 @@ import { ownWallsDrawAt } from "./B06_Section_UI_Cross_Culvert_Wire";
import { attachShapePick, shapePickRoute } from "./B06_Section_UI_Cross_Shape_Pick";
import { appendUnderdrainOverlay } from "./B06_Section_UI_Cross_Underdrain";
import { raiseWalls } from "./B06_Section_UI_Cross_Wall";
import { attachChuteBandPick } from "./B06_Section_UI_Cross_Wall_Hatch";
import { attachChuteBandPick, groundLine } from "./B06_Section_UI_Cross_Wall_Hatch";
import { appendSpoilFillOverlay } from "./B06_Section_UI_Cross_SpoilFill";
import { appendRevetmentOverlay, computeRevetmentLayout } from "./B06_Section_UI_Cross_Revetment";
import {
@@ -454,6 +454,7 @@ export function createCrossSectionCard(
section.design,
x,
toDisplayY,
groundLine(section.samples ?? []),
);
// 월류 폭으로 걸친 옆 카드 = 흙막이 연동 꼴 · 주인 측점 글(294-6)
markLinkedFordPavement(plotLayer, fordFrom, section, stationInterval);
+78 -3
View File
@@ -349,6 +349,77 @@ export function chuteBandOutline(
return { top, normals, outline: [...top, ...bottom.reverse()] };
}
/** 꺾은선 앞에서부터 `lengthM` 까지(쓴 길이와 함께). */
function trimPolyline(
points: readonly OffsetPoint[],
lengthM: number,
): { points: OffsetPoint[]; used: number } {
const out = points.slice(0, 1);
let used = 0;
for (let i = 1; i < points.length && lengthM - used > 1e-9; i += 1) {
const [a, b] = [points[i - 1], points[i]];
const step = Math.hypot(b.offset - a.offset, b.elevation - a.elevation);
if (step < 1e-9) continue;
const t = Math.min((lengthM - used) / step, 1);
out.push({
offset: a.offset + (b.offset - a.offset) * t,
elevation: a.elevation + (b.elevation - a.elevation) * t,
});
used += step * t;
}
return { points: out, used };
}
/** 원지반 샘플 → 꺾은선(유효 점만 · 오프셋 차례). */
export function groundLine(
samples: ReadonlyArray<{
offset_m?: number | null;
elevation_m?: number | null;
valid?: boolean;
}>,
): OffsetPoint[] {
return samples
.filter((s) => s.valid !== false && s.offset_m != null && s.elevation_m != null)
.map((s) => ({ offset: s.offset_m as number, elevation: s.elevation_m as number }))
.sort((a, b) => a.offset - b.offset);
}
/**
* 도수로 · 돌붙임 선 길이(294-4) — 선들을 차례로 사면 따라 `lengthM` 까지 자름 · 사면보다 길면 마지막 끝에서
* 원지반(`ground`)을 바깥으로 따라 이어 그 길이까지 · 길이 없으면 그대로(원지반 닿으면 끝). 점 하나뿐인 선
* (사면 없음 · 끝이 이미 원지반)은 길이가 있으면 원지반 따라 · 없으면 안 그림. 그림 전용(수량 · 내역 무관).
*/
export function chuteRun<L extends { points: OffsetPoint[] }>(
lines: readonly L[],
lengthM?: number | null,
ground?: readonly OffsetPoint[],
): L[] {
const drawn = (list: L[]) => list.filter((line) => line.points.length > 1);
if (!(lengthM && lengthM > 0) || !lines.length) return drawn([...lines]);
let left = lengthM;
const out: L[] = [];
for (const line of lines) {
if (left <= 1e-9) break;
const cut = trimPolyline(line.points, left);
out.push({ ...line, points: cut.points });
left -= cut.used;
}
const last = out[out.length - 1];
if (left > 1e-9 && ground?.length && last) {
const first = lines[0].points[0];
const end = last.points[last.points.length - 1];
const outward = Math.sign(end.offset - first.offset) || Math.sign(end.offset) || 1;
const along = [
end,
...ground
.filter((p) => (p.offset - end.offset) * outward > 1e-6)
.sort((a, b) => (a.offset - b.offset) * outward),
];
out.push({ ...last, points: trimPolyline(along, left).points });
}
return drawn(out);
}
/** 띠가 서는 유출 조각(152-3) — 성토 조각 + 내림 · 수평 절토 조각(3도 절토선 · 끝 ≤ 시작).
* 3도 선이 못 만나 복합경사로 오르는 절토 조각은 뺌. B06 관 · 흙막이 띠와 3D 띠가 같은 거름. */
export function isChuteSegment(segment: { kind?: string; points: OffsetPoint[] }): boolean {
@@ -365,13 +436,16 @@ export function isChuteSegment(segment: { kind?: string; points: OffsetPoint[] }
*/
export function appendChuteBand(
layer: SVGElement,
lines: ReadonlyArray<{ points: OffsetPoint[]; kind?: string }>,
chute: { structure: string; thickness_m: number },
allLines: ReadonlyArray<{ points: OffsetPoint[]; kind?: string }>,
chute: { structure: string; thickness_m: number; length_m?: number },
x: (offset: number) => number,
toDisplayY: (elevation: number) => number,
/** 원지반 — 길이가 사면보다 길면 이어 그림(294-4). */
ground?: readonly OffsetPoint[],
): void {
const thickness = chute.thickness_m;
if (!(thickness > 0)) return;
const lines = chuteRun(allLines, chute.length_m, ground);
const pixelsPerMeter = Math.abs(x(1) - x(0)) || 1;
const px = (p: OffsetPoint): [number, number] => [x(p.offset), toDisplayY(p.elevation)];
for (const line of lines) {
@@ -383,7 +457,8 @@ export function appendChuteBand(
band.setAttribute("class", "b06-chart__culvert-pitching");
const title = document.createElementNS(SVG_NS, "title");
const along = line.kind === "cut" ? "3도 절토선" : "성토사면";
title.textContent = `유출부 ${chute.structure} — 두께 ${thickness.toFixed(2)}m · ${along} 따라 원지반까지`;
const reach = chute.length_m ? `길이 ${chute.length_m.toFixed(2)}m` : "원지반까지";
title.textContent = `유출부 ${chute.structure} — 두께 ${thickness.toFixed(2)}m · ${along} 따라 ${reach}`;
band.append(title);
clipSeq += 1;
const clipId = `b06-chute-clip-${clipSeq}`;