- 충돌 24 파일 = import 줄 다툼 — 518-2 새 경로(Cross_Culvert · Culvert_Geom · Extra · Wire · Types · Box_Geom · Ford_Geom · Ford_Pavement) + 518-3 새 경로(Cross_Body_Guard · Wall_Geom · Wall_Hatch_Geom · Excavation_Geom · Revetment_Geom · Structure_Layouts · Route_Carry_Patch)
- 518-2 logic 셋이 부르던 UI_Cross_Wall · Wall_Hatch · Body_Guard = 518-3 logic 경로로 · 518-3 새 logic 넷의 옛 Culvert_Types · Extra 경로 = 518-2 logic 으로
- test_472_3 GEOMETRY_PULLS 남은 3 줄(Design_Surface · Excavation · Wall_Hatch) 걷음 = 빈 목록
- Corridor_Structures prettier
- 확인 = 518-1 · 518-2 · 518-3 고친 시험 + golden_472 73 파일 444 통과 · typecheck 0 · design-calc · corridor 묶음 · 옛 판(1d5d63875) ↔ 합친 판 덧그림 대조 938 측점 같음
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
224 lines
9.7 KiB
TypeScript
224 lines
9.7 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_Cross_Wall_Hatch_Geom.ts
|
|
* 벽 안 무늬 · 물빼기 · 도수로 · 돌붙임 띠 **기하**(m 좌표) — 그리기는
|
|
* `../B06_Section_UI_Cross_Wall_Hatch`(472-3). 횡단 그림과 B05 3D 띠 솔리드가 같은 것을 쓴다.
|
|
* ========================================================================== */
|
|
|
|
import { REVET_THICKNESS_M } from "./B06_Section_UI_Cross_Culvert_Const";
|
|
import type { OffsetPoint, WallLayout } from "./B06_Section_Cross_Culvert_Types";
|
|
|
|
/** 형태 문자열 → 표현 갈래. 판정 순서는 좁은 말(돌망태·바자)부터. */
|
|
export type WallHatch = "dry" | "wet" | "concrete" | "gabion" | "log" | "wattle";
|
|
|
|
export function hatchOfForm(form: string | null | undefined): WallHatch {
|
|
if (!form) return "dry";
|
|
if (form.includes("돌망태")) return "gabion";
|
|
if (form.includes("통나무") || form.includes("목재")) return "log";
|
|
if (form.includes("바자")) return "wattle";
|
|
if (form.includes("콘크리트")) return "concrete";
|
|
if (form.includes("찰")) return "wet";
|
|
return "dry";
|
|
}
|
|
|
|
/** 켜 수 — 켜 높이가 목표치에 가장 가깝게, 최소 2켜. */
|
|
export function courses(spanM: number, targetM: number): number {
|
|
return Math.max(2, Math.round(spanM / targetM));
|
|
}
|
|
|
|
/** 벽 전면 기운 띠(이음선~전면, 두께 REVET_THICKNESS_M)의 중심축 — 바닥 · 상단 offset · 바닥 표고 · 길이. */
|
|
export function wallHatchBand(wall: WallLayout): {
|
|
centerBase: number;
|
|
centerTop: number;
|
|
embedBase: number;
|
|
spanM: number;
|
|
} {
|
|
// 하단선이 원지반을 따라 기울어 있으므로 띠 바닥도 하단선 위에서 잡는다.
|
|
const bottomSpan = wall.bottomFront.offset - wall.bottomBack.offset;
|
|
const bottomAtOffset = (offset: number): number =>
|
|
Math.abs(bottomSpan) > 1e-9
|
|
? wall.bottomBack.elevation +
|
|
(wall.bottomFront.elevation - wall.bottomBack.elevation) *
|
|
((offset - wall.bottomBack.offset) / bottomSpan)
|
|
: wall.bottomBack.elevation;
|
|
const centerBase = wall.outerOffset - wall.outward * (REVET_THICKNESS_M / 2);
|
|
const centerTop = wall.topJoint.offset + wall.outward * (REVET_THICKNESS_M / 2);
|
|
const embedBase = bottomAtOffset(centerBase);
|
|
const spanM = Math.max(0.3, wall.topBack.elevation - embedBase);
|
|
return { centerBase, centerTop, embedBase, spanM };
|
|
}
|
|
|
|
/** 물빼기 파이프 단면(73-2) — 보기용 고정값 · 지식DB 관경 · 간격과 별개. */
|
|
const DRAIN_BAND_M = 1.5;
|
|
const DRAIN_DIAMETER_M = 0.15;
|
|
/** 앞쪽이 낮게 — 벽 두께 1m 당 낙차(보기용). */
|
|
const DRAIN_FALL = 0.1;
|
|
|
|
/** 직선 a→b 가 볼록 벽 단면 안을 지나는 구간 — clip 이 없는 곳(B07 오프스크린 수확)도 벽 밖으로 안 샘. */
|
|
function insideRun(a: OffsetPoint, b: OffsetPoint, polygon: OffsetPoint[]): OffsetPoint[] | null {
|
|
const dx = b.offset - a.offset;
|
|
const dy = b.elevation - a.elevation;
|
|
const hits: number[] = [];
|
|
polygon.forEach((p, i) => {
|
|
const q = polygon[(i + 1) % polygon.length];
|
|
const ex = q.offset - p.offset;
|
|
const ey = q.elevation - p.elevation;
|
|
const denominator = dx * ey - dy * ex;
|
|
if (Math.abs(denominator) < 1e-12) return;
|
|
const t = ((p.offset - a.offset) * ey - (p.elevation - a.elevation) * ex) / denominator;
|
|
const u = ((p.offset - a.offset) * dy - (p.elevation - a.elevation) * dx) / denominator;
|
|
if (u >= -1e-9 && u <= 1 + 1e-9) hits.push(t);
|
|
});
|
|
if (hits.length < 2) return null;
|
|
const at = (t: number): OffsetPoint => ({
|
|
offset: a.offset + dx * t,
|
|
elevation: a.elevation + dy * t,
|
|
});
|
|
return [at(Math.min(...hits)), at(Math.max(...hits))];
|
|
}
|
|
|
|
/**
|
|
* 옹벽 물빼기 — 높이(바닥~상단)를 반올림(높이 / 1.5)개(최소 1) 띠로 고르게 나눠 띠마다
|
|
* 가운데 하나. 배면 → 전면으로 꿰는 두 줄 평행선(앞쪽 조금 낮게) · 벽 두께만큼.
|
|
* 물빼기마다 벽 안 구간 목록(0~2 줄).
|
|
*/
|
|
export function wallDrainRuns(wall: WallLayout): OffsetPoint[][][] {
|
|
const bottom = wall.bottomBack.elevation;
|
|
const height = wall.topBack.elevation - bottom;
|
|
const count = Math.max(1, Math.round(height / DRAIN_BAND_M));
|
|
const back = wall.backOffset;
|
|
const [, , topFront, bottomFront] = wall.points;
|
|
const drains: OffsetPoint[][][] = [];
|
|
for (let i = 0; i < count; i += 1) {
|
|
const center = bottom + (height * (i + 0.5)) / count;
|
|
// 그 높이의 전면 — 전면은 기울어 위로 갈수록 배면에 가까움.
|
|
const front =
|
|
bottomFront.offset + (topFront.offset - bottomFront.offset) * ((center - bottom) / height);
|
|
const fall = (DRAIN_FALL * Math.abs(front - back)) / 2;
|
|
const runs: OffsetPoint[][] = [];
|
|
for (const side of [-0.5, 0.5]) {
|
|
const level = center + side * DRAIN_DIAMETER_M;
|
|
const run = insideRun(
|
|
{ offset: back, elevation: level + fall },
|
|
{ offset: front, elevation: level - fall },
|
|
wall.points,
|
|
);
|
|
if (run) runs.push(run);
|
|
}
|
|
drains.push(runs);
|
|
}
|
|
return drains;
|
|
}
|
|
|
|
/**
|
|
* 도수로 · 돌붙임 띠 단면(90-2) — 윗면 = 사면선(겹친 점 뺌) · 변마다 아래쪽(사면 안쪽) 단위 법선 ·
|
|
* 둘레 = 윗면 → 두께만큼 오프셋한 아랫면(거꾸로). 횡단 그림과 B05 3D 띠 솔리드가 같은 것을 쓴다.
|
|
*/
|
|
export function chuteBandOutline(
|
|
raw: OffsetPoint[],
|
|
thickness: number,
|
|
): { top: OffsetPoint[]; normals: OffsetPoint[]; outline: OffsetPoint[] } | null {
|
|
if (!(thickness > 0)) return null;
|
|
const top = raw.filter(
|
|
(p, i) =>
|
|
i === 0 ||
|
|
Math.hypot(p.offset - raw[i - 1].offset, p.elevation - raw[i - 1].elevation) > 1e-6,
|
|
);
|
|
if (top.length < 2) return null;
|
|
// 변마다 아래쪽(사면 안쪽) 단위 법선
|
|
const normals = top.slice(1).map((p, i) => {
|
|
const dx = p.offset - top[i].offset;
|
|
const dz = p.elevation - top[i].elevation;
|
|
const length = Math.hypot(dx, dz);
|
|
const sign = dx / length > 0 ? -1 : 1;
|
|
return { offset: (sign * -dz) / length, elevation: (sign * dx) / length };
|
|
});
|
|
// 꺾임은 두 오프셋 선의 교점(마이터 · 너무 뾰족하면 두 배까지)
|
|
const bottom = top.map((p, i) => {
|
|
const a = normals[Math.max(i - 1, 0)];
|
|
const b = normals[Math.min(i, normals.length - 1)];
|
|
const sum = { offset: a.offset + b.offset, elevation: a.elevation + b.elevation };
|
|
const k = thickness / Math.max(sum.offset * a.offset + sum.elevation * a.elevation, 0.5);
|
|
return { offset: p.offset + sum.offset * k, elevation: p.elevation + sum.elevation * k };
|
|
});
|
|
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 {
|
|
if (segment.kind !== "cut") return true;
|
|
const { points } = segment;
|
|
return points.length > 1 && points[points.length - 1].elevation <= points[0].elevation + 1e-6;
|
|
}
|