Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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 ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user