Merge remote-tracking branch 'origin/sub_laptop_7' into sub_laptop_2
This commit is contained in:
@@ -48,6 +48,7 @@ import {
|
||||
import { computeRevetmentLayout } from "../B06_Section/B06_Section_UI_Cross_Revetment";
|
||||
import { withoutGuardedFill } from "../B06_Section/B06_Section_UI_Cross_Body_Guard";
|
||||
import { chuteBands, fordPavementChute } from "./B05_Profile_UI_Corridor_Structures_Chute";
|
||||
import { isChuteSegment } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch";
|
||||
|
||||
/** 스윕 프레임 하나 — 노선 위 한 지점의 중심 XY·좌향 단위벡터와 종단 표고 오프셋. */
|
||||
export interface StructureFrame {
|
||||
@@ -588,8 +589,8 @@ export function buildCorridorStructures(
|
||||
wall.role,
|
||||
);
|
||||
}
|
||||
// 도수로 · 돌붙임 띠 — 유출 성토 조각(단 사이 포함) · 유출 벽 길이(135-3).
|
||||
chuteBands(layout.outletFill.segments, section.culvert?.chute).forEach((band) =>
|
||||
// 도수로 · 돌붙임 띠 — 유출 성토 조각(단 사이 포함) · 묻힌 끝 3도 절토 조각(152-3) · 유출 벽 길이(135-3).
|
||||
chuteBands(layout.outletFill.segments, section.culvert?.chute, isChuteSegment).forEach((band) =>
|
||||
pushSwept("chute", band, outletSpan.beforeM, outletSpan.afterM, null, "chute"),
|
||||
);
|
||||
// 다단은 **단별 구간값**을 따른다(2026-08-29 사용자 — 단마다 연장이 다르다).
|
||||
|
||||
@@ -19,14 +19,16 @@ interface FillSegment {
|
||||
points: OffsetPoint[];
|
||||
}
|
||||
|
||||
/** 유출 성토 조각들 → 띠 단면 폴리곤들. 상자를 안 골랐거나 두께가 없으면 빈 목록. */
|
||||
/** 유출 성토 조각들 → 띠 단면 폴리곤들. 상자를 안 골랐거나 두께가 없으면 빈 목록.
|
||||
* `keep` = 띠가 서는 조각 — 기본 성토만(박스 · 세월교 본체 끝) · 관 · 흙막이는 `isChuteSegment`(152-3). */
|
||||
export function chuteBands(
|
||||
segments: ReadonlyArray<FillSegment>,
|
||||
chute: ChuteSpec | null | undefined,
|
||||
keep: (segment: FillSegment) => boolean = (segment) => segment.kind !== "cut",
|
||||
): OffsetPoint[][] {
|
||||
if (!chute) return [];
|
||||
return segments.flatMap((segment) => {
|
||||
if (segment.kind === "cut") return [];
|
||||
if (!keep(segment)) return [];
|
||||
const band = chuteBandOutline(segment.points, chute.thickness_m);
|
||||
return band ? [band.outline] : [];
|
||||
});
|
||||
|
||||
@@ -47,9 +47,17 @@ const EXTRA_ROWS = [
|
||||
".b06-structure-panel__struct--span",
|
||||
].join(", ");
|
||||
|
||||
/** 도수로 · 돌붙임 상자 — 배관 · 박스 · 세월교 chute_add · 물넘이 chute(152-4). */
|
||||
const CHUTE_BOX =
|
||||
'fieldset[data-spec-container="chute_add"], fieldset[data-spec-container="chute"]';
|
||||
|
||||
/** 연동을 푼 옆 카드의 「이 측점 높이」 줄 — 어느 칸이든 받음(152-2). */
|
||||
const OWN_HEIGHT_ROW = ".b06-structure-panel__struct--own-height";
|
||||
|
||||
/** 유입구·유출구 칸은 폼에 없는 줄만 받는다 — 읽기(좌우 · 상하 · 구체 길이 · 표고) ·
|
||||
* 단 수 · 옵션 · 집수정 내공(24-4a). */
|
||||
const SIDE_ROWS = [
|
||||
OWN_HEIGHT_ROW,
|
||||
".b06-structure-panel__struct--readout",
|
||||
".b06-structure-panel__struct--tier",
|
||||
".b06-structure-panel__struct--options",
|
||||
@@ -59,15 +67,16 @@ const SIDE_ROWS = [
|
||||
/** 배관 칸 = 폼에 자리 없는 집수정 내공 · 유입 단 수만 — 목록에서 골라도 같은 글(54-29).
|
||||
* 유출 단 수는 폼 「추가 흙막이(단)」 칸(54-27) · 읽기 · 옵션 줄은 안 받음. */
|
||||
const PIPE_INLET_ROWS = [
|
||||
OWN_HEIGHT_ROW,
|
||||
".b06-structure-panel__struct--tier",
|
||||
".b06-structure-panel__struct--basin-size",
|
||||
].join(", ");
|
||||
const PIPE_OUTLET_ROWS = ".b06-structure-panel__struct--basin-size";
|
||||
const PIPE_OUTLET_ROWS = `${OWN_HEIGHT_ROW}, .b06-structure-panel__struct--basin-size`;
|
||||
|
||||
/** 받을 칸에 맞는 줄 — 배관 유입 · 유출 칸이 아니면 옛 줄 그대로 · 세월교 · 박스 유입 · 유출 칸은
|
||||
* 받는 줄 없음(측벽 높이 줄 지움 · 107-3 · 읽기는 칸에 안 둠 · 65-1). */
|
||||
function rowsFor(slot: HTMLElement): string | null {
|
||||
if (slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return null;
|
||||
if (slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return OWN_HEIGHT_ROW;
|
||||
if (slot === sideSlots?.outlet) return PIPE_OUTLET_ROWS;
|
||||
return slot === sideSlots?.inlet ? PIPE_INLET_ROWS : SIDE_ROWS;
|
||||
}
|
||||
@@ -155,14 +164,20 @@ function relocateSideRows(panelRoot: HTMLElement, scroll = true): void {
|
||||
const side = panelRoot.dataset.side;
|
||||
// 세월교 · BOX 본체 = 옮길 줄 없이 그 명세의 본체 상자만 강조(66-1) — 날개벽 칸이 든 명세 폼에서 찾음
|
||||
// 관 = 배관 명세 폼의 「관」 상자만 강조(118-1)
|
||||
if (side === "body" || side === "pipe") {
|
||||
const body = (side === "pipe" ? sideSlots?.inlet : sideSlots?.wingInlet)
|
||||
?.closest(".b05-structure__spec")
|
||||
?.querySelector<HTMLElement>(
|
||||
side === "pipe"
|
||||
? 'fieldset[data-spec-container="pipe"]'
|
||||
: 'fieldset[data-spec-container="ford"], fieldset[data-spec-container="body"]',
|
||||
);
|
||||
// 도수로 띠 = 보이는 명세의 「도수로/돌붙임」 상자(배관 · 박스 · 세월교 chute_add · 물넘이 chute · 152-4)
|
||||
if (side === "body" || side === "pipe" || side === "chute") {
|
||||
const body =
|
||||
side === "chute"
|
||||
? [...document.querySelectorAll<HTMLElement>(CHUTE_BOX)].find(
|
||||
(box) => !box.closest("[hidden]"),
|
||||
)
|
||||
: (side === "pipe" ? sideSlots?.inlet : sideSlots?.wingInlet)
|
||||
?.closest(".b05-structure__spec")
|
||||
?.querySelector<HTMLElement>(
|
||||
side === "pipe"
|
||||
? 'fieldset[data-spec-container="pipe"]'
|
||||
: 'fieldset[data-spec-container="ford"], fieldset[data-spec-container="body"]',
|
||||
);
|
||||
if (!body || body.closest("[hidden]")) return;
|
||||
setGroupLabel(body, "");
|
||||
body.classList.add("is-active");
|
||||
|
||||
@@ -162,7 +162,7 @@ export function appendBoxOverlay(
|
||||
layer,
|
||||
layout.sides
|
||||
.filter((side) => side.wingRole === "outlet")
|
||||
.flatMap((side) => side.fillSegments.filter((s) => s.kind !== "cut").map((s) => s.points)),
|
||||
.flatMap((side) => side.fillSegments.filter((s) => s.kind !== "cut")),
|
||||
box.chute,
|
||||
x,
|
||||
toDisplayY,
|
||||
|
||||
@@ -22,7 +22,11 @@ import type {
|
||||
WallLayout,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Geom";
|
||||
import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation";
|
||||
import { appendChuteBand, appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
import {
|
||||
appendChuteBand,
|
||||
appendWallHatch,
|
||||
isChuteSegment,
|
||||
} from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
|
||||
// 기하 계산 진입점과 공개 상수·타입은 여기서 재수출한다 — B05(최소 토피)와 횡단 뷰가
|
||||
// 이 모듈 경로로 이미 참조하고 있어 분리 후에도 import 경로를 바꾸지 않는다.
|
||||
@@ -56,8 +60,8 @@ function polygon(
|
||||
// "own" = 배관과 무관한 **독립 기슭막이**(구조물 정본 D군). 조정창·조작값 저장은
|
||||
// 배관 벽과 같은 체계를 그대로 쓴다(2026-08-28 사용자: 배관측점처럼 이동).
|
||||
export type RevetKey = "inlet" | "outlet" | "own" | `extra${number}` | `bextra${number}`;
|
||||
/** 고름 전용 키 — 벽 키 + 관(118-1 · 조정창 없음 · 강조만). 조정 함수에는 `RevetKey` 만. */
|
||||
export type RevetPick = RevetKey | "pipe";
|
||||
/** 고름 전용 키 — 벽 키 + 관(118-1) · 도수로 띠(152-4) — 조정창 없음 · 강조만. 조정 함수에는 `RevetKey` 만. */
|
||||
export type RevetPick = RevetKey | "pipe" | "chute";
|
||||
|
||||
/** 벽 강조를 카드 재생성 없이 갈아 끼우는 setter. */
|
||||
export type RevetHighlightSetter = (key: RevetPick | null) => void;
|
||||
@@ -110,11 +114,12 @@ export function appendCulvertOverlay(
|
||||
: `${name} 절토선 — 3도 선이 원지반을 못 만나 절토 경사로 원지반까지`;
|
||||
};
|
||||
const drawOutletFill = (): void => {
|
||||
// 도수로 · 돌붙임 띠(90-2) — 유출 끝 ~ 원지반 성토 조각(단 사이 포함) · 연동 카드엔 안 그림
|
||||
// 도수로 · 돌붙임 띠(90-2) — 유출 끝 ~ 원지반 성토 조각(단 사이 포함) · 묻힌 끝 3도 절토 조각(152-3)
|
||||
// · 연동 카드엔 안 그림
|
||||
if (culvert.chute && !linked)
|
||||
appendChuteBand(
|
||||
layer,
|
||||
layout.outletFill.segments.filter((s) => s.kind !== "cut").map((s) => s.points),
|
||||
layout.outletFill.segments.filter(isChuteSegment),
|
||||
culvert.chute,
|
||||
x,
|
||||
toDisplayY,
|
||||
|
||||
@@ -680,6 +680,7 @@ export function computeCulvertLayout(
|
||||
},
|
||||
bodyX:
|
||||
inletBody || outletBody ? { inlet: inletBody?.x ?? 0, outlet: outletBody?.x ?? 0 } : null,
|
||||
pureHeights: { inlet: inletWallSpec.pureHeight, outlet: outletWallSpec.pureHeight },
|
||||
designTrim: body && ownTrim ? mergeGuardTrim(ownTrim, body.trim, walls) : ownTrim,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,6 +221,8 @@ export interface CulvertLayout {
|
||||
inletOptions: { revetAllowed: boolean; basinLUAllowed: boolean };
|
||||
/** 세월교 · 박스 흙막이(73-4) — 본체 끝 자리의 배관 4축 x(적용 x 의 0점). 본체 없으면 없음. */
|
||||
bodyX?: { inlet: number; outlet: number } | null;
|
||||
/** 유입 · 유출 벽의 순수 높이(m) — 높이 빈 칸인 연동 옆 카드가 물려받음(152-2). */
|
||||
pureHeights?: { inlet: number; outlet: number };
|
||||
}
|
||||
/** 유입측 구조물 사용자 선택 — auto = 규칙 기반 자동 선택. */
|
||||
export type InletStructureChoice = "auto" | "revet" | "I" | "L" | "U";
|
||||
|
||||
@@ -606,7 +606,16 @@ export function ownerAppliedAdjusts(
|
||||
owner.culvert?.hidden_pipe ? "revet" : inletStructure?.valueFor(owner),
|
||||
inletStructure?.adjustFor(owner),
|
||||
);
|
||||
return ownerLayout ? bodyAbsolute(ownerLayout) : ownerInput;
|
||||
if (!ownerLayout) return ownerInput;
|
||||
// 높이 빈 칸 = 소유 측점이 그린 순수 높이 — 옆 측점엔 본체가 없어 근입 0.5 만 서던 것(152-2).
|
||||
const shift = bodyAbsolute(ownerLayout);
|
||||
const heights = ownerLayout.pureHeights;
|
||||
if (!heights) return shift;
|
||||
return {
|
||||
...shift,
|
||||
inlet: { ...shift.inlet, h: shift.inlet.h ?? heights.inlet },
|
||||
outlet: { ...shift.outlet, h: shift.outlet.h ?? heights.outlet },
|
||||
};
|
||||
}
|
||||
|
||||
function adjustsInput(
|
||||
|
||||
@@ -136,7 +136,7 @@ export function appendFordOverlay(
|
||||
layer,
|
||||
layout.sides
|
||||
.filter((side) => side.role === "outlet")
|
||||
.flatMap((side) => side.fillSegments.filter((s) => s.kind !== "cut").map((s) => s.points)),
|
||||
.flatMap((side) => side.fillSegments.filter((s) => s.kind !== "cut")),
|
||||
ford.chute,
|
||||
x,
|
||||
toDisplayY,
|
||||
|
||||
@@ -182,7 +182,7 @@ export function appendFordPavementOverlay(
|
||||
"b06-chart__ford-deck",
|
||||
),
|
||||
);
|
||||
if (spec.chute) appendChuteBand(svg, [outletFillLine(design)], spec.chute, x, y);
|
||||
if (spec.chute) appendChuteBand(svg, [{ points: outletFillLine(design) }], spec.chute, x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ export function buildStructurePanel(
|
||||
const heightRow = heightParts.row;
|
||||
// 다단 벽에서는 이 행이 [추가 기슭막이] 그룹으로 옮겨 간다(2026-08-30 지시 4).
|
||||
heightRow.classList.add("b06-structure-panel__struct--height");
|
||||
const heightLabel = heightRow.querySelector?.<HTMLElement>(".b06-structure-panel__label");
|
||||
const heightValue = document.createElement("span");
|
||||
heightValue.className = "b06-structure-panel__hval";
|
||||
const heightMinus = makeButton(
|
||||
@@ -495,11 +496,11 @@ export function buildStructurePanel(
|
||||
return {
|
||||
root,
|
||||
show: (pick) => {
|
||||
// 관 = 조정창 없음 · 좌측 「관」 상자만 강조(118-1 · 세월교 본체 꼴)
|
||||
const key = pick === "pipe" ? null : pick;
|
||||
// 관 · 도수로 띠 = 조정창 없음 · 좌측 「관」 · 「도수로/돌붙임」 상자만 강조(118-1 · 152-4)
|
||||
const key = pick === "pipe" || pick === "chute" ? null : pick;
|
||||
current = key;
|
||||
root.classList.toggle("is-hidden", key === null);
|
||||
root.dataset.side = pick === "pipe" ? "pipe" : "";
|
||||
root.dataset.side = pick === "pipe" || pick === "chute" ? pick : "";
|
||||
document.removeEventListener("keydown", onArrowKey);
|
||||
drag.detach();
|
||||
if (!key) return;
|
||||
@@ -550,11 +551,14 @@ export function buildStructurePanel(
|
||||
// 형태 · 높이는 좌측 폼 칸이 정본이다(유입구 · 유출구 · 독립 좌우 · 구조물 옵션) — 폼에
|
||||
// 자기 칸이 없는 **다단 벽**만 이 줄을 「추가 기슭막이」 칸에 세운다(24-4a).
|
||||
materialRow.classList.toggle("is-hidden", !isExtra);
|
||||
heightRow.classList.toggle("is-hidden", !isExtra);
|
||||
if (isExtra) {
|
||||
heightValue.textContent = `${deps.heightFor(key).toFixed(1)}m`;
|
||||
materialSelect.value = deps.formFor(key);
|
||||
}
|
||||
// 연동을 푼 옆 카드의 흙막이 = 「이 측점 높이」 줄(폼 칸은 본 흙막이 · 152-2)
|
||||
const ownHeight = deps.linkState()?.linked === false && guardKey && !isBasin;
|
||||
heightRow.classList.toggle("is-hidden", !isExtra && !ownHeight);
|
||||
heightRow.classList.toggle("b06-structure-panel__struct--own-height", ownHeight);
|
||||
if (heightLabel)
|
||||
heightLabel.textContent = ownHeight ? "이 측점 높이" : L("B06_Cross_Height_Label");
|
||||
if (isExtra || ownHeight) heightValue.textContent = `${deps.heightFor(key).toFixed(1)}m`;
|
||||
if (isExtra) materialSelect.value = deps.formFor(key);
|
||||
// 단 수 행 — 벽 종류를 가리지 않고 **높이 행 바로 아래** 한 자리에 온다.
|
||||
// 유출 벽은 자기 채널(extra), 유입 벽·집수정 계류측은 basin 채널(bextra)을 쓴다.
|
||||
// 유출 벽과 독립 기슭막이 유입 벽에서는 **항상** 보인다(2026-08-22 사용자 ⑤ —
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import { culvertWallsStandAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { raiseWalls } from "./B06_Section_UI_Cross_Wall";
|
||||
import { attachChuteBandPick } 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 {
|
||||
@@ -572,6 +573,19 @@ export function createCrossSectionCard(
|
||||
if (highlightRevet) setRevetActive(highlightRevet);
|
||||
}
|
||||
raiseWalls(plotLayer); // 흙막이 · 옹벽 안을 지나는 다른 선 가림(66-5)
|
||||
// 도수로 · 돌붙임 띠 고름 — 관 고름 꼴(152-4)
|
||||
const setChuteActive = attachChuteBandPick(
|
||||
plotLayer,
|
||||
revetOffset && (() => toggleRevet("chute")),
|
||||
);
|
||||
if (setChuteActive) {
|
||||
const baseSetActive = setRevetActive;
|
||||
setRevetActive = (key) => {
|
||||
baseSetActive(key);
|
||||
setChuteActive(key === "chute");
|
||||
};
|
||||
setChuteActive(highlightRevet === "chute");
|
||||
}
|
||||
// 보라 선 고름 — 가림 · 판정면 차례가 정해진 뒤(137-2).
|
||||
const lineDesign = section.design;
|
||||
linePick = attachDesignLinePick({
|
||||
|
||||
@@ -349,14 +349,23 @@ export function chuteBandOutline(
|
||||
return { top, normals, outline: [...top, ...bottom.reverse()] };
|
||||
}
|
||||
|
||||
/** 띠가 서는 유출 조각(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 도수로 · 돌붙임 띠(90-2) — 유출 성토사면 선(윗면 = 사면선)을 사면 안쪽(아래)으로 두께만큼 오프셋한
|
||||
* 띠 · 안에 돌(흙막이 돌과 같은 그리기 · 두께 인자). 선마다 한 띠 · 그림만(면적 · 터파기 무관).
|
||||
* 도수로 · 돌붙임 형상 같음 · 배관 · 박스 · 세월교 오버레이가 부름(B07 은 오버레이를 따라옴).
|
||||
* 절토 조각(`kind: "cut"` · 152-3)은 3도 절토선 따라.
|
||||
*/
|
||||
export function appendChuteBand(
|
||||
layer: SVGElement,
|
||||
lines: OffsetPoint[][],
|
||||
lines: ReadonlyArray<{ points: OffsetPoint[]; kind?: string }>,
|
||||
chute: { structure: string; thickness_m: number },
|
||||
x: (offset: number) => number,
|
||||
toDisplayY: (elevation: number) => number,
|
||||
@@ -365,15 +374,16 @@ export function appendChuteBand(
|
||||
if (!(thickness > 0)) return;
|
||||
const pixelsPerMeter = Math.abs(x(1) - x(0)) || 1;
|
||||
const px = (p: OffsetPoint): [number, number] => [x(p.offset), toDisplayY(p.elevation)];
|
||||
for (const raw of lines) {
|
||||
const shape = chuteBandOutline(raw, thickness);
|
||||
for (const line of lines) {
|
||||
const shape = chuteBandOutline(line.points, thickness);
|
||||
if (!shape) continue;
|
||||
const { top, normals, outline } = shape;
|
||||
const band = document.createElementNS(SVG_NS, "polygon");
|
||||
band.setAttribute("points", outline.map((p) => px(p).join(",")).join(" "));
|
||||
band.setAttribute("class", "b06-chart__culvert-pitching");
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
title.textContent = `유출부 ${chute.structure} — 두께 ${thickness.toFixed(2)}m · 성토사면 따라 원지반까지`;
|
||||
const along = line.kind === "cut" ? "3도 절토선" : "성토사면";
|
||||
title.textContent = `유출부 ${chute.structure} — 두께 ${thickness.toFixed(2)}m · ${along} 따라 원지반까지`;
|
||||
band.append(title);
|
||||
clipSeq += 1;
|
||||
const clipId = `b06-chute-clip-${clipSeq}`;
|
||||
@@ -414,3 +424,31 @@ export function appendChuteBand(
|
||||
layer.append(band, clip, group);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 도수로 · 돌붙임 띠 고름(152-4 · 118-1 관 고름 꼴) — 그린 띠(관 · 박스 · 세월교 · 물넘이)마다 투명 판정면.
|
||||
* 벽 판정면 아래 · 보라 선 판정 위(판정면 클래스를 같이 씀). 띠가 없으면 null · 강조 setter 반환.
|
||||
*/
|
||||
export function attachChuteBandPick(
|
||||
layer: SVGElement,
|
||||
onPick?: () => void,
|
||||
): ((active: boolean) => void) | null {
|
||||
const kids = [...layer.children];
|
||||
const bands = kids.filter((el) => el.classList.contains("b06-chart__culvert-pitching"));
|
||||
if (!bands.length) return null;
|
||||
const firstHit = kids.find((el) => el.classList.contains("b06-chart__culvert-revet-hit"));
|
||||
for (const band of onPick ? bands : []) {
|
||||
const hit = document.createElementNS(SVG_NS, "polygon");
|
||||
hit.setAttribute("points", band.getAttribute("points") ?? "");
|
||||
hit.setAttribute("class", "b06-chart__culvert-revet-hit b06-chart__chute-hit");
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
title.textContent = "도수로 · 돌붙임 선택";
|
||||
hit.append(title);
|
||||
hit.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
onPick?.();
|
||||
});
|
||||
layer.insertBefore(hit, firstHit ?? null);
|
||||
}
|
||||
return (active) => bands.forEach((band) => band.classList.toggle("is-active", active));
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
type SelectableStructure,
|
||||
} from "../A00_Common/design/design_selection";
|
||||
|
||||
/** 벽 고름이 받는 부재키 — 관은 강조만(118-1) · 집수정·본체는 여기서 빠진다. */
|
||||
const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+|pipe)$/;
|
||||
/** 벽 고름이 받는 부재키 — 관 · 도수로 띠는 강조만(118-1 · 152-4) · 집수정·본체는 여기서 빠진다. */
|
||||
const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+|pipe|chute)$/;
|
||||
|
||||
interface PickTargets {
|
||||
focusStation: (stationId: string) => void;
|
||||
|
||||
@@ -73,10 +73,12 @@ export function wireStructureSelection(
|
||||
// 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1)
|
||||
if (key) markWallSelecting();
|
||||
origRevet(chainageM, key);
|
||||
if (key) remember?.(ownerChainageOf(chainageM), key);
|
||||
if (key) panel.showAtChainage(ownerChainageOf(chainageM));
|
||||
// 도수로 띠는 자기 측점 시설 — 이웃 관 연장 안이어도 그 관 폼을 안 엶(152-4)
|
||||
const owner = key === "chute" ? chainageM : ownerChainageOf(chainageM);
|
||||
if (key) remember?.(owner, key);
|
||||
if (key) panel.showAtChainage(owner);
|
||||
else if (!nested) {
|
||||
remember?.(ownerChainageOf(chainageM), ""); // 푼 부재가 B05 ↔ B06 오갈 때 다시 안 열림
|
||||
remember?.(owner, ""); // 푼 부재가 B05 ↔ B06 오갈 때 다시 안 열림
|
||||
panel.releasePipe();
|
||||
}
|
||||
if (key) syncInletStructure();
|
||||
@@ -88,14 +90,20 @@ export function wireStructureSelection(
|
||||
const keyOf = (box: HTMLElement): RevetPick | null => {
|
||||
const id = box.dataset.specContainer;
|
||||
if (id === "inlet" || id === "outlet" || id === "pipe") return id;
|
||||
if (id === "chute_add" || id === "chute") return "chute"; // 도수로 띠(152-4)
|
||||
return id === "soil_add" ? `extra${Number(box.dataset.specIndex)}` : null;
|
||||
};
|
||||
const pickBox = (box: HTMLElement): void => {
|
||||
const chainageM = panel.currentChainage();
|
||||
const target = chainageM === null ? undefined : sectionAt(chainageM);
|
||||
const key = keyOf(box);
|
||||
if (!key || !target?.culvert || stationControls.revetOffset.highlightFor(target) === key)
|
||||
return;
|
||||
const chute =
|
||||
target?.culvert?.chute ??
|
||||
target?.box?.chute ??
|
||||
target?.ford?.chute ??
|
||||
target?.ford_pavement?.chute;
|
||||
if (!key || !target || !(key === "chute" ? chute : target.culvert)) return;
|
||||
if (stationControls.revetOffset.highlightFor(target) === key) return;
|
||||
stationControls.revetOffset.select(target.chainage_m, key);
|
||||
refreshCard?.(target.chainage_m);
|
||||
};
|
||||
|
||||
@@ -391,6 +391,18 @@
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* 도수로 · 돌붙임 띠 고름(152-4) — 관 고름과 같은 강조 · 판정면은 굵은 투명 테두리로 여유 */
|
||||
.b06-chart__culvert-pitching.is-active {
|
||||
fill: color-mix(in srgb, var(--color-danger) 30%, transparent);
|
||||
stroke: var(--color-danger);
|
||||
stroke-width: 2.2;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
.b06-chart__culvert-revet-hit.b06-chart__chute-hit {
|
||||
stroke: transparent;
|
||||
stroke-width: 8;
|
||||
}
|
||||
|
||||
/* 암 경계선 상/하/리셋 제어 (B05 측점 선 제어 ▲/▼/↺ 버튼 패턴 재활용) */
|
||||
.b06-design__rockb-btn {
|
||||
width: 22px;
|
||||
|
||||
@@ -49,7 +49,7 @@ const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
|
||||
const { buildCorridorStructures } = loadTs(B05("B05_Profile_UI_Corridor_Structures"));
|
||||
const { structureHashParts } = loadTs(B05("B05_Profile_UI_Corridor_Structures_Hash"));
|
||||
const { structureTopLoops } = loadTs(B05("B05_Profile_UI_Corridor_Tin_Design"));
|
||||
const { chuteBandOutline } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
|
||||
const { chuteBandOutline, isChuteSegment } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
|
||||
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom"));
|
||||
const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford"));
|
||||
const { outletFillLine } = loadTs(B06("B06_Section_UI_Cross_Ford_Pavement"));
|
||||
@@ -181,7 +181,7 @@ function check(name, section, expectedBands, expectedLength, strip) {
|
||||
const { culvertLayoutOf } = loadTs(B05("B05_Profile_UI_Corridor_Structures"));
|
||||
const layout = culvertLayoutOf(s);
|
||||
const bands = (layout?.outletFill.segments ?? [])
|
||||
.filter((g) => g.kind !== "cut")
|
||||
.filter(isChuteSegment) // 묻힌 끝 3도 절토 조각도(152-3)
|
||||
.map((g) => chuteBandOutline(g.points, 0.3)?.outline)
|
||||
.filter(Boolean);
|
||||
const spec = s.culvert.outlet;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/* 152-2 연동 흙막이 높이 헬퍼 — 높이 칸 빈 박스 흙막이(73-4 픽스처 352.14)의 연동 옆 측점(340 · 360)이
|
||||
* 소유 측점이 그린 순수 높이를 물려받는지 · 연동을 푼 카드만 자기 높이로 서는지. 결과 = 표준출력 JSON.
|
||||
* 짝 = `test_152_2_link_height.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
// 화면 전용 모듈(토스트 · 글) — 기하 계산엔 안 쓰임
|
||||
const STUBS = {
|
||||
"@ui/ui_template_toast": { showToast: () => undefined },
|
||||
};
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) {
|
||||
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
cache.set(full, data);
|
||||
return data;
|
||||
}
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", f);
|
||||
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
|
||||
|
||||
const fixture = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
|
||||
);
|
||||
// 높이 칸 비움(= 자동 · 세월교 · 박스 흙막이 기본) — 원본은 3.5 를 넣은 상태
|
||||
const sections = fixture.cross_sections.map((s) => {
|
||||
if (!s.culvert?.body) return s;
|
||||
const copy = JSON.parse(JSON.stringify(s));
|
||||
copy.culvert.inlet.revet_height_m = null;
|
||||
copy.culvert.outlet.revet_height_m = null;
|
||||
return copy;
|
||||
});
|
||||
const at = (list, c) => list.find((s) => Math.abs(s.chainage_m - c) < 0.6);
|
||||
const round = (v) => Math.round(v * 1000) / 1000;
|
||||
const heightsOf = (list, c) => {
|
||||
const culvert = computeStoredLayouts(at(list, c), list)?.culvert;
|
||||
return {
|
||||
pure: culvert?.pureHeights
|
||||
? { inlet: round(culvert.pureHeights.inlet), outlet: round(culvert.pureHeights.outlet) }
|
||||
: null,
|
||||
walls: (culvert?.walls ?? []).map((w) => ({ role: w.role, height: round(w.height) })),
|
||||
shift: culvert?.revetShift ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
const out = { owner: heightsOf(sections, 352.14), linked: {} };
|
||||
for (const c of [340, 360]) out.linked[c] = heightsOf(sections, c);
|
||||
|
||||
// 340 연동 풂 + 유출 흙막이만 +0.1 m(좌측 「이 측점 높이」 줄이 그 측점 값을 고친 것)
|
||||
const shift = out.linked[340].shift;
|
||||
const detached = sections.map((s) => {
|
||||
if (Math.abs(s.chainage_m - 340) >= 0.6) return s;
|
||||
const copy = JSON.parse(JSON.stringify(s));
|
||||
copy.design.revet_link_detached = true;
|
||||
copy.design.revet_adjust = {
|
||||
...(copy.design.revet_adjust ?? {}),
|
||||
inlet: { ...shift.inlet },
|
||||
outlet: { ...shift.outlet, h: round(shift.outlet.h + 0.1) },
|
||||
};
|
||||
return copy;
|
||||
});
|
||||
out.detached = {
|
||||
340: heightsOf(detached, 340),
|
||||
360: heightsOf(detached, 360),
|
||||
owner: heightsOf(detached, 352.14),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,112 @@
|
||||
/* 152-3 도수로 띠 절토 조각 헬퍼 — 묻힌 유출 끝 3도 절토선에도 띠 · 오르막 복합경사 절토엔 띠 없음.
|
||||
* 화면 TS(`_Cross_Wall_Hatch` 거름 · 단면 · B05 3D `chuteBands`)를 그 자리에서 돌림 · 짝 = `test_152_3_chute_cut.py`.
|
||||
* 입력 = 139-4 와 같은 픽스처(박스 352.14 · 배관 85.052 원지반 고친 사본).
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
const STUBS = { "@ui/ui_template_toast": { showToast: () => undefined } };
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
|
||||
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
|
||||
const { isChuteSegment, chuteBandOutline } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
|
||||
const { chuteBands } = loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Corridor_Structures_Chute.ts"),
|
||||
);
|
||||
const fixture = (name) =>
|
||||
JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", name), "utf8")).cross_sections;
|
||||
const clone = (v) => JSON.parse(JSON.stringify(v));
|
||||
const r3 = (v) => Math.round(v * 1000) / 1000;
|
||||
const CHUTE = { structure: "돌붙임", thickness_m: 0.3 };
|
||||
|
||||
/** 유출 조각마다 — 종류 · 오르막 · 띠 섬 · 띠 윗면이 그 선 · 아랫면이 선 아래(두께). */
|
||||
function study(section, list) {
|
||||
const culvert = computeStoredLayouts(section, list).culvert;
|
||||
const segments = culvert.outletFill.segments;
|
||||
const kept = segments.filter(isChuteSegment);
|
||||
return {
|
||||
segments: segments.map((g) => {
|
||||
const [a, b] = [g.points[0], g.points[g.points.length - 1]];
|
||||
const band = isChuteSegment(g) ? chuteBandOutline(g.points, CHUTE.thickness_m) : null;
|
||||
const top = band?.top ?? [];
|
||||
const bottom = band ? band.outline.slice(top.length) : [];
|
||||
return {
|
||||
kind: g.kind ?? "fill",
|
||||
rises: b.elevation > a.elevation + 1e-6,
|
||||
band: !!band,
|
||||
topIsLine: !!band && top.length === g.points.length,
|
||||
below: band
|
||||
? r3(
|
||||
Math.min(...top.map((p, i) => p.elevation - bottom[bottom.length - 1 - i].elevation)),
|
||||
)
|
||||
: null,
|
||||
};
|
||||
}),
|
||||
// 3D 띠(관 · 흙막이 = isChuteSegment) · 기본 거름(박스 · 세월교 본체 끝 = 성토만)
|
||||
bands3d: chuteBands(segments, CHUTE, isChuteSegment).length,
|
||||
bands3dFillOnly: chuteBands(segments, CHUTE).length,
|
||||
kept: kept.length,
|
||||
};
|
||||
}
|
||||
|
||||
const out = {};
|
||||
const bodies = fixture("b06_73_4_body_guard_sections.json");
|
||||
out.box = study(
|
||||
bodies.find((s) => Math.abs(s.chainage_m - 352.14) < 0.6),
|
||||
bodies,
|
||||
);
|
||||
|
||||
const pipes = fixture("b07_cross_overlay_sections.json");
|
||||
const pipe = pipes.find((s) => s.culvert && !s.culvert.hidden_pipe);
|
||||
const base = computeStoredLayouts(pipe, pipes).culvert;
|
||||
const tail = base.outletFill.segments[base.outletFill.segments.length - 1];
|
||||
const start = tail.points[0];
|
||||
const outward = Math.sign(tail.points[1].offset - start.offset) || 1;
|
||||
function reshaped(rise, slope) {
|
||||
const s = clone(pipe);
|
||||
for (const p of s.samples) {
|
||||
const t = ((p.offset_m ?? 0) - start.offset) * outward;
|
||||
if (t >= 0 && typeof p.elevation_m === "number")
|
||||
p.elevation_m = start.elevation + rise + slope * t;
|
||||
}
|
||||
return study(
|
||||
s,
|
||||
pipes.map((x) => (x === pipe ? s : x)),
|
||||
);
|
||||
}
|
||||
out.pipeBase = study(pipe, pipes);
|
||||
out.pipeDown = reshaped(0.3, -0.15); // 3도 선이 원지반을 만남
|
||||
out.pipeUp = reshaped(0.3, 0.1); // 오르막 → 복합경사 절토
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,278 @@
|
||||
/* PLAN 152-4 단위검증 헬퍼 — B06 도수로 · 돌붙임 띠 고름 · 강조(118-1 관 고름 꼴) · 조정창 숨김 · 3D chute 키.
|
||||
* 화면 TS 를 그 자리에서 트랜스파일해 흉내 DOM 에서 돌림 · 결과 = 표준출력 JSON 한 벌.
|
||||
* 입력 = `fixtures/b07_cross_overlay_sections.json` 배관 측점(도수로 상자를 붙인 사본). 짝 = `test_152_4_chute_pick.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
let STUBS = {
|
||||
"@ui/ui_template_toast": { showToast: () => undefined },
|
||||
"@ui/ui_template_elements": { showToast: () => undefined },
|
||||
};
|
||||
|
||||
// ── 화면 그릇 흉내 — 오버레이 · 조정창이 쓰는 만큼만 ─────────────────────────
|
||||
class FakeNode {
|
||||
constructor(tag) {
|
||||
this.tagName = String(tag).toUpperCase();
|
||||
this.children = [];
|
||||
this.attrs = {};
|
||||
this.dataset = {};
|
||||
this.listeners = {};
|
||||
this._text = "";
|
||||
this._classes = new Set();
|
||||
const self = this;
|
||||
this.classList = {
|
||||
add: (...names) => names.forEach((n) => self._classes.add(n)),
|
||||
remove: (...names) => names.forEach((n) => self._classes.delete(n)),
|
||||
toggle: (name, force) => {
|
||||
const on = force === undefined ? !self._classes.has(name) : !!force;
|
||||
on ? self._classes.add(name) : self._classes.delete(name);
|
||||
return on;
|
||||
},
|
||||
contains: (name) => self._classes.has(name),
|
||||
};
|
||||
this.style = { setProperty: () => {} };
|
||||
}
|
||||
set className(value) {
|
||||
this._classes = new Set(String(value).split(/\s+/).filter(Boolean));
|
||||
}
|
||||
get className() {
|
||||
return [...this._classes].join(" ");
|
||||
}
|
||||
set textContent(value) {
|
||||
this._text = String(value);
|
||||
this.children = [];
|
||||
}
|
||||
get textContent() {
|
||||
return this._text + this.children.map((c) => c.textContent).join("");
|
||||
}
|
||||
append(...kids) {
|
||||
kids.forEach((k) => this.children.push(k));
|
||||
}
|
||||
insertBefore(kid, ref) {
|
||||
const at = ref ? this.children.indexOf(ref) : -1;
|
||||
at < 0 ? this.children.push(kid) : this.children.splice(at, 0, kid);
|
||||
return kid;
|
||||
}
|
||||
appendChild(kid) {
|
||||
this.children.push(kid);
|
||||
return kid;
|
||||
}
|
||||
replaceChildren(...kids) {
|
||||
this.children = kids;
|
||||
}
|
||||
setAttribute(name, value) {
|
||||
this.attrs[name] = String(value);
|
||||
if (name === "class") this.className = value;
|
||||
}
|
||||
getAttribute(name) {
|
||||
return name === "class" ? this.className : (this.attrs[name] ?? null);
|
||||
}
|
||||
addEventListener(type, fn) {
|
||||
(this.listeners[type] ??= []).push(fn);
|
||||
}
|
||||
removeEventListener() {}
|
||||
click() {
|
||||
let stopped = false;
|
||||
const event = { stopPropagation: () => (stopped = true), preventDefault: () => {} };
|
||||
(this.listeners.click ?? []).forEach((fn) => fn(event));
|
||||
return stopped;
|
||||
}
|
||||
querySelectorAll(selector) {
|
||||
const found = [];
|
||||
const walk = (node) =>
|
||||
node.children.forEach((kid) => {
|
||||
if (kid.classList && kid.classList.contains(selector.slice(1))) found.push(kid);
|
||||
walk(kid);
|
||||
});
|
||||
walk(this);
|
||||
return found;
|
||||
}
|
||||
querySelector(selector) {
|
||||
return this.querySelectorAll(selector)[0] ?? null;
|
||||
}
|
||||
getClientRects() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
globalThis.document = {
|
||||
createElement: (tag) => new FakeNode(tag),
|
||||
createElementNS: (_ns, tag) => new FakeNode(tag),
|
||||
createComment: () => new FakeNode("#comment"),
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
};
|
||||
globalThis.window = { addEventListener: () => {}, removeEventListener: () => {} };
|
||||
globalThis.requestAnimationFrame = (fn) => fn();
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) {
|
||||
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
cache.set(full, data);
|
||||
return data;
|
||||
}
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
|
||||
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
|
||||
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
|
||||
const { attachChuteBandPick } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
|
||||
const { applyStructurePick } = loadTs(B06("B06_Section_UI_Page_Structure_Pick"));
|
||||
|
||||
const fixture = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b07_cross_overlay_sections.json"), "utf8"),
|
||||
);
|
||||
const sections = fixture.cross_sections;
|
||||
const plain = sections.find((s) => s.culvert && !s.culvert.hidden_pipe);
|
||||
const pipeSection = JSON.parse(JSON.stringify(plain));
|
||||
pipeSection.culvert.chute = { structure: "돌붙임", thickness_m: 0.3 };
|
||||
const listWith = sections.map((s) => (s === plain ? pipeSection : s));
|
||||
const out = {};
|
||||
const cls = (n) => n.className;
|
||||
|
||||
// ① 오버레이 + 띠 고름 — 판정면 차례 · 누름 · 강조
|
||||
function draw(section, list, onPick) {
|
||||
const layer = new FakeNode("g");
|
||||
const layout = computeStoredLayouts(section, list).culvert;
|
||||
const setter = appendCulvertOverlay(
|
||||
layer,
|
||||
layout,
|
||||
(o) => -o * 10,
|
||||
(e) => -e * 10,
|
||||
() => {},
|
||||
false,
|
||||
);
|
||||
const picked = [];
|
||||
const setChute = attachChuteBandPick(layer, onPick ? () => picked.push("chute") : undefined);
|
||||
return { layer, setter, setChute, picked };
|
||||
}
|
||||
const drawn = draw(pipeSection, listWith, true);
|
||||
const kids = drawn.layer.children;
|
||||
const bands = kids.filter((n) => n.classList.contains("b06-chart__culvert-pitching"));
|
||||
const hits = kids.filter((n) => n.classList.contains("b06-chart__culvert-revet-hit"));
|
||||
const chuteHits = hits.filter((n) => n.classList.contains("b06-chart__chute-hit"));
|
||||
const stopped = chuteHits[0].click();
|
||||
drawn.setChute(true);
|
||||
const on = bands.map((b) => b.classList.contains("is-active"));
|
||||
drawn.setChute(false);
|
||||
const off = bands.map((b) => b.classList.contains("is-active"));
|
||||
out.overlay = {
|
||||
bands: bands.length,
|
||||
chuteHits: chuteHits.length,
|
||||
// 판정면 차례 — 띠 판정면이 관 · 벽 판정면보다 먼저(아래)
|
||||
hitOrder: hits.map((n) => (n.classList.contains("b06-chart__chute-hit") ? "chute" : "other")),
|
||||
sameShape: chuteHits.every((h, i) => h.attrs.points === bands[i].attrs.points),
|
||||
picked: drawn.picked,
|
||||
stopped,
|
||||
on,
|
||||
off,
|
||||
};
|
||||
// 고름 배선 없음(조작 없는 카드) — 판정면 없이 강조만 · 띠 없으면 null
|
||||
const bare = draw(pipeSection, listWith, false);
|
||||
out.bare = {
|
||||
hits: bare.layer.children.filter((n) => n.classList.contains("b06-chart__chute-hit")).length,
|
||||
setter: typeof bare.setChute,
|
||||
};
|
||||
out.noChute = draw(plain, sections, true).setChute === null;
|
||||
|
||||
// ② B05 3D 띠 고름(chute 키) → B06 고름 길로
|
||||
const calls = [];
|
||||
applyStructurePick(
|
||||
{ at: pipeSection.chainage_m, key: "chute" },
|
||||
{ cross_sections: [pipeSection] },
|
||||
{
|
||||
focusStation: () => {},
|
||||
refreshCard: () => {},
|
||||
revetSelect: (c, k) => calls.push(["revet", k]),
|
||||
fordSelect: (c, k) => calls.push(["ford", k]),
|
||||
boxSelect: (c, k) => calls.push(["box", k]),
|
||||
},
|
||||
);
|
||||
out.pick = calls;
|
||||
|
||||
// ③ 조정창 — show("chute") = 숨김 · side "chute"
|
||||
const base = loadTs(B06("B06_Section_UI_Cross_Panel_Base"));
|
||||
STUBS = {
|
||||
...STUBS,
|
||||
"./B06_Section_UI_Section_Common": { L: (key) => key },
|
||||
"./B06_Section_UI_Cross_Panel_Base": {
|
||||
...base,
|
||||
bindDragHandle: () => ({ attach: () => {}, detach: () => {} }),
|
||||
},
|
||||
};
|
||||
const { buildStructurePanel } = loadTs(B06("B06_Section_UI_Cross_Structure_Panel"));
|
||||
const touched = [];
|
||||
const spy = (name, value) => () => {
|
||||
touched.push(name);
|
||||
return value;
|
||||
};
|
||||
const panel = buildStructurePanel({
|
||||
scrollKey: "t152",
|
||||
adjustFor: spy("adjustFor", { x: 0, d: null, h: null, m: null }),
|
||||
nudge: spy("nudge"),
|
||||
nudgeSlope: spy("nudgeSlope"),
|
||||
nudgeHeight: spy("nudgeHeight"),
|
||||
heightFor: spy("heightFor", 2),
|
||||
formFor: spy("formFor", "돌쌓기(메)"),
|
||||
setForm: spy("setForm"),
|
||||
reset: spy("reset"),
|
||||
close: () => {},
|
||||
structureFor: () => "revet",
|
||||
basinAdjustFor: () => ({ innerWidthM: 1, innerHeightM: 1.2, lateralM: 0, slopeM: 0 }),
|
||||
updateBasin: () => {},
|
||||
nudgeBasinLateral: () => {},
|
||||
resetBasin: () => {},
|
||||
canNudge: spy("canNudge", true),
|
||||
fineMove: spy("fineMove", false),
|
||||
dragAxes: spy("dragAxes", null),
|
||||
extraState: () => ({ canAdd: false, count: 0 }),
|
||||
isLinkedCard: () => false,
|
||||
basinExtraState: () => ({ canAdd: false, count: 0 }),
|
||||
hiddenPipe: () => false,
|
||||
bodyGuard: () => false,
|
||||
setExtraCount: () => {},
|
||||
setBasinExtraCount: () => {},
|
||||
equalizeExtras: () => {},
|
||||
spanFor: spy("spanFor", null),
|
||||
setSpan: () => {},
|
||||
linkState: () => null,
|
||||
setLinked: () => {},
|
||||
followGrade: () => true,
|
||||
setFollowGrade: () => {},
|
||||
});
|
||||
panel.show("chute");
|
||||
out.panel = {
|
||||
hidden: panel.root.classList.contains("is-hidden"),
|
||||
side: panel.root.dataset.side,
|
||||
touched,
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -49,7 +49,7 @@ const run = (line) => {
|
||||
const layer = new Node("g");
|
||||
appendChuteBand(
|
||||
layer,
|
||||
[line],
|
||||
[{ points: line }],
|
||||
{ structure: "돌붙임", thickness_m: 0.3 },
|
||||
(o) => o * SCALE,
|
||||
(e) => -e * SCALE,
|
||||
|
||||
@@ -92,7 +92,7 @@ def test_관은_조정창_없음(out) -> None:
|
||||
|
||||
def test_좌측_관_상자_강조_스크롤() -> None:
|
||||
dock = _src("B06_Section/B06_Section_UI_Adjust_Dock.ts")
|
||||
assert 'if (side === "body" || side === "pipe") {' in dock
|
||||
assert 'if (side === "body" || side === "pipe" || side === "chute") {' in dock # 152-4 도수로
|
||||
assert "'fieldset[data-spec-container=\"pipe\"]'" in dock
|
||||
assert 'body.classList.add("is-active");' in dock
|
||||
assert 'if (scroll) body.scrollIntoView({ block: "nearest" });' in dock
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""152-2 연동 흙막이 높이 — 본 흙막이가 그린 순수 높이를 옆 카드가 물려받음 · 풀면 그 측점만 따로.
|
||||
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_152_2_link_height.cjs`) · 픽스처 = 73-4(박스 352.14 · 높이 칸 비움).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).parent / "helper_152_2_link_height.cjs"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def screen() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def _src(relative: str) -> str:
|
||||
return (ROOT / relative).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_연동_옆_카드_높이_본_흙막이와_같음(screen: dict) -> None:
|
||||
owner = screen["owner"]["pure"]
|
||||
assert owner == {"inlet": 2.0, "outlet": 2.0} # 본체 끝 높이(근입 0.5 아님)
|
||||
for chainage in ("340", "360"):
|
||||
assert screen["linked"][chainage]["pure"] == owner
|
||||
assert screen["linked"][chainage]["walls"] == screen["owner"]["walls"]
|
||||
|
||||
|
||||
def test_연동_풀린_카드만_따로(screen: dict) -> None:
|
||||
detached = screen["detached"]
|
||||
assert detached["340"]["pure"] == {"inlet": 2.0, "outlet": 2.1}
|
||||
assert detached["360"]["pure"] == {"inlet": 2.0, "outlet": 2.0}
|
||||
assert detached["owner"]["pure"] == {"inlet": 2.0, "outlet": 2.0}
|
||||
|
||||
|
||||
def test_풀린_카드_이_측점_높이_줄() -> None:
|
||||
panel = _src("B06_Section/B06_Section_UI_Cross_Structure_Panel.ts")
|
||||
assert "deps.linkState()?.linked === false && guardKey && !isBasin" in panel
|
||||
assert '"이 측점 높이"' in panel
|
||||
dock = _src("B06_Section/B06_Section_UI_Adjust_Dock.ts")
|
||||
assert 'const OWN_HEIGHT_ROW = ".b06-structure-panel__struct--own-height";' in dock
|
||||
assert "return OWN_HEIGHT_ROW;" in dock # 세월교 · 박스 날개벽 칸도 받음
|
||||
wire = _src("B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts")
|
||||
assert "h: shift.inlet.h ?? heights.inlet" in wire
|
||||
@@ -0,0 +1,65 @@
|
||||
"""152-3 도수로 · 돌붙임 띠 — 묻힌 유출 끝 3도 절토선(내림 · 수평)에도 띠 · 오르막 복합경사 절토엔 없음.
|
||||
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_152_3_chute_cut.cjs`) · B06 관 · 흙막이 띠와 3D 띠가 같은 거름.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).parent / "helper_152_3_chute_cut.cjs"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def _src(relative: str) -> str:
|
||||
return (ROOT / relative).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_묻힌_흙막이_유출_3도_선에_띠(out: dict) -> None:
|
||||
(cut,) = out["box"]["segments"]
|
||||
assert cut["kind"] == "cut" and not cut["rises"]
|
||||
assert cut["band"] and cut["topIsLine"] # 윗면 = 3도 선
|
||||
assert cut["below"] == pytest.approx(0.3, abs=0.01) # 선 아래 두께
|
||||
assert out["box"]["bands3d"] == 1 and out["box"]["bands3dFillOnly"] == 0
|
||||
|
||||
|
||||
def test_배관_묻힌_끝_내림이면_띠_오르막이면_없음(out: dict) -> None:
|
||||
down = out["pipeDown"]["segments"]
|
||||
assert [s["kind"] for s in down] == ["fill", "cut"]
|
||||
assert all(s["band"] for s in down)
|
||||
up = out["pipeUp"]["segments"]
|
||||
assert up[-1]["kind"] == "cut" and up[-1]["rises"] and not up[-1]["band"]
|
||||
assert out["pipeDown"]["bands3d"] == 2 and out["pipeUp"]["bands3d"] == 1
|
||||
|
||||
|
||||
def test_성토만인_측점은_그대로(out: dict) -> None:
|
||||
base = out["pipeBase"]
|
||||
assert base["bands3d"] == base["bands3dFillOnly"] == 2
|
||||
|
||||
|
||||
def test_거름_한_벌_배선() -> None:
|
||||
culvert = _src("B06_Section/B06_Section_UI_Cross_Culvert.ts")
|
||||
assert "layout.outletFill.segments.filter(isChuteSegment)," in culvert
|
||||
corridor = _src("B05_Profile/B05_Profile_UI_Corridor_Structures.ts")
|
||||
assert (
|
||||
"chuteBands(layout.outletFill.segments, section.culvert?.chute, isChuteSegment)" in corridor
|
||||
)
|
||||
hatch = _src("B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts")
|
||||
assert '"3도 절토선"' in hatch
|
||||
# 박스 · 세월교 본체 끝은 성토만(3도 규칙 안 넓힘)
|
||||
for name in ("B06_Section_UI_Cross_Box.ts", "B06_Section_UI_Cross_Ford.ts"):
|
||||
assert 'side.fillSegments.filter((s) => s.kind !== "cut")' in _src(f"B06_Section/{name}")
|
||||
@@ -0,0 +1,66 @@
|
||||
"""152-4 도수로 · 돌붙임 띠 고름 · 강조 — 118-1 관 고름 꼴(판정면 · setter · 좌측 상자 · 3D 키).
|
||||
|
||||
화면 TS 를 흉내 DOM 에서 돌린 값(`helper_152_4_chute_pick.cjs`) + 배선 문자열.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).parent / "helper_152_4_chute_pick.cjs"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def _src(relative: str) -> str:
|
||||
return (ROOT / relative).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_띠마다_판정면_벽_관_아래(out: dict) -> None:
|
||||
overlay = out["overlay"]
|
||||
assert overlay["bands"] == overlay["chuteHits"] == 2
|
||||
assert overlay["sameShape"]
|
||||
n = overlay["chuteHits"]
|
||||
assert overlay["hitOrder"][:n] == ["chute"] * n # 벽 · 관 판정면이 위(겹치면 이김)
|
||||
assert "chute" not in overlay["hitOrder"][n:]
|
||||
|
||||
|
||||
def test_띠_누름_고름_강조(out: dict) -> None:
|
||||
overlay = out["overlay"]
|
||||
assert overlay["picked"] == ["chute"] and overlay["stopped"]
|
||||
assert overlay["on"] == [True, True] and overlay["off"] == [False, False]
|
||||
assert out["bare"] == {"hits": 0, "setter": "function"}
|
||||
assert out["noChute"]
|
||||
|
||||
|
||||
def test_조정창_없음_3D_키(out: dict) -> None:
|
||||
assert out["panel"] == {"hidden": True, "side": "chute", "touched": []}
|
||||
assert out["pick"] == [["revet", "chute"]]
|
||||
|
||||
|
||||
def test_좌측_상자_강조_배선() -> None:
|
||||
view = _src("B06_Section/B06_Section_UI_Cross_View.ts")
|
||||
assert "attachChuteBandPick(" in view and 'toggleRevet("chute")' in view
|
||||
assert 'setChuteActive(key === "chute");' in view
|
||||
dock = _src("B06_Section/B06_Section_UI_Adjust_Dock.ts")
|
||||
assert (
|
||||
'fieldset[data-spec-container="chute_add"], fieldset[data-spec-container="chute"]' in dock
|
||||
)
|
||||
selection = _src("B06_Section/B06_Section_UI_Page_Structures_Selection.ts")
|
||||
assert 'if (id === "chute_add" || id === "chute") return "chute";' in selection
|
||||
assert 'const owner = key === "chute" ? chainageM : ownerChainageOf(chainageM);' in selection
|
||||
css = _src("B06_Section/B06_Section_UI_Style_Cross_Areas.css")
|
||||
assert ".b06-chart__culvert-pitching.is-active {" in css
|
||||
@@ -88,5 +88,8 @@ def test_자동_자리_읽기_글이_없음() -> None:
|
||||
|
||||
def test_배관_유출_칸은_옛_단_수_줄을_안_받음() -> None:
|
||||
dock = _src("B06_Section/B06_Section_UI_Adjust_Dock.ts")
|
||||
# 54-29 — 배관 유출 칸 = 집수정 내공 줄만(단 수 · 읽기 · 옵션 줄 안 받음)
|
||||
assert 'const PIPE_OUTLET_ROWS = ".b06-structure-panel__struct--basin-size";' in dock
|
||||
# 54-29 — 배관 유출 칸 = 집수정 내공 줄만(단 수 · 읽기 · 옵션 줄 안 받음) · 152-2 풀린 카드 높이 줄
|
||||
assert (
|
||||
"const PIPE_OUTLET_ROWS = `${OWN_HEIGHT_ROW}, .b06-structure-panel__struct--basin-size`;"
|
||||
in dock
|
||||
)
|
||||
|
||||
@@ -25,7 +25,11 @@ def test_배관_칸은_옵션_읽기_줄을_안_받음() -> None:
|
||||
assert inlet
|
||||
assert "struct--options" not in inlet.group(1)
|
||||
assert "struct--readout" not in inlet.group(1)
|
||||
assert 'const PIPE_OUTLET_ROWS = ".b06-structure-panel__struct--basin-size";' in dock
|
||||
# 152-2 — 풀린 연동 카드 「이 측점 높이」 줄만 더 받음
|
||||
assert (
|
||||
"const PIPE_OUTLET_ROWS = `${OWN_HEIGHT_ROW}, .b06-structure-panel__struct--basin-size`;"
|
||||
in dock
|
||||
)
|
||||
|
||||
|
||||
def test_배관_칸_이름표는_그대로() -> None:
|
||||
|
||||
@@ -168,8 +168,11 @@ def test_집계표_세월교_박스_흙막이_상자는_유출_단_수만큼():
|
||||
|
||||
|
||||
def test_B06_세월교_박스_칸은_받는_줄_없음():
|
||||
"""107-3 — 측벽 높이 줄 지움."""
|
||||
"""107-3 — 측벽 높이 줄 지움 · 152-2 연동 풀린 카드 「이 측점 높이」 줄만 받음."""
|
||||
dock = (ROOT / "B06_Section/B06_Section_UI_Adjust_Dock.ts").read_text(encoding="utf-8")
|
||||
assert "slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return null" in dock
|
||||
assert (
|
||||
"slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return OWN_HEIGHT_ROW"
|
||||
in dock
|
||||
)
|
||||
lists = (ROOT / "B05_Profile/B05_Profile_UI_Structures_List.ts").read_text(encoding="utf-8")
|
||||
assert "return isNone(c, state[c.id][0])" in lists
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_세월교_바닥_손_값은_원지반을_바꿔도_그대로(fit: dict)
|
||||
def test_세월교_박스_칸은_측벽_높이_줄_없음() -> None:
|
||||
"""107-3 — 「측벽 높이(자동)」 줄 지움(유입 · 유출)."""
|
||||
dock = (ROOT / "B06_Section/B06_Section_UI_Adjust_Dock.ts").read_text(encoding="utf-8")
|
||||
assert "sideSlots?.wingOutlet) return null" in dock
|
||||
assert "sideSlots?.wingOutlet) return OWN_HEIGHT_ROW" in dock # 152-2 풀린 카드 높이 줄만
|
||||
assert "wall-height" not in dock
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user