feat(B05): 노면공 확폭 구간 단추에 확폭 면적 표시(138-2)
- wideningArea — 측점 사이 사다리꼴 합 · 구간 앞뒤 확폭 0 측점까지 · 노면공 범위로 자름 - 단추 오른쪽 끝 「n.nn ㎡」 · 끈 구간 회색 · 수량엔 안 넣음 - test_87 면적 경우 셋 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
This commit is contained in:
@@ -63,6 +63,35 @@ export function wideningRuns(
|
||||
return runs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 확폭 면적(㎡ · 138-2 · 표시만) — 확폭(좌 + 우) 측점 사이 사다리꼴 합 Σ(wᵢ + wᵢ₊₁)/2 × Δ.
|
||||
* 구간 앞뒤 이웃 측점(확폭 0)까지 이어 붙이고 · 노면공 범위 `[start, end]` 밖은 잘라 냄(자른 끝은 직선 보간).
|
||||
*/
|
||||
export function wideningArea(
|
||||
stations: readonly WideningStation[],
|
||||
run: WideningRun,
|
||||
range: { start: number; end: number },
|
||||
): number {
|
||||
const sorted = [...stations].sort((a, b) => a.chainage_m - b.chainage_m);
|
||||
const first = sorted.findIndex((s) => Math.abs(s.chainage_m - run.start_m) <= WIDENING_TOL_M);
|
||||
const last = sorted.findIndex((s) => Math.abs(s.chainage_m - run.end_m) <= WIDENING_TOL_M);
|
||||
if (first < 0 || last < 0) return 0;
|
||||
const points = sorted
|
||||
.slice(Math.max(first - 1, 0), last + 2)
|
||||
.map((s) => [s.chainage_m, wideningOf(s)] as const);
|
||||
let area = 0;
|
||||
for (let i = 0; i + 1 < points.length; i += 1) {
|
||||
const [x0, w0] = points[i];
|
||||
const [x1, w1] = points[i + 1];
|
||||
const a = Math.max(x0, range.start);
|
||||
const b = Math.min(x1, range.end);
|
||||
if (b <= a || x1 <= x0) continue;
|
||||
const at = (x: number): number => w0 + ((w1 - w0) * (x - x0)) / (x1 - x0);
|
||||
area += ((at(a) + at(b)) / 2) * (b - a);
|
||||
}
|
||||
return area;
|
||||
}
|
||||
|
||||
const round = (value: number): number => Number(value.toFixed(2));
|
||||
|
||||
/** 저장 글 → 끈 구간 쌍. 틀린 조각은 버림. */
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isRunOff,
|
||||
offForRuns,
|
||||
toggleRun,
|
||||
wideningArea,
|
||||
wideningRuns,
|
||||
type WideningRun,
|
||||
} from "../A00_Common/design/design_structure_widening";
|
||||
@@ -37,18 +38,24 @@ export function createWideningBox(hooks: {
|
||||
let off = "";
|
||||
|
||||
/** 지금 범위의 구간들 — 횡단 자료 · 범위가 없으면 null(+ 안내 글). */
|
||||
const runs = (): { list: WideningRun[] | null; note: string } => {
|
||||
const runs = (): {
|
||||
list: WideningRun[] | null;
|
||||
note: string;
|
||||
area?: (run: WideningRun) => number;
|
||||
} => {
|
||||
const sections = pipeSections();
|
||||
if (!sections?.length) return { list: null, note: "횡단 자료 없음" };
|
||||
const { start, end } = hooks.range();
|
||||
if (start === null || end === null || end <= start)
|
||||
return { list: null, note: "시작 · 종료 측점을 넣으면 보임" };
|
||||
const list = wideningRuns(sections, start, end);
|
||||
return { list, note: list.length ? "" : "범위 안 확폭 없음" };
|
||||
// 확폭 면적(138-2) — 표시만 · 수량엔 안 넣음
|
||||
const area = (run: WideningRun) => wideningArea(sections, run, { start, end });
|
||||
return { list, note: list.length ? "" : "범위 안 확폭 없음", area };
|
||||
};
|
||||
|
||||
const paintBody = (): void => {
|
||||
const { list, note } = runs();
|
||||
const { list, note, area } = runs();
|
||||
if (!list?.length) {
|
||||
const text = document.createElement("span");
|
||||
text.className = "b05-structure__widening-note";
|
||||
@@ -68,6 +75,12 @@ export function createWideningBox(hooks: {
|
||||
const from = stationLabel(run.start_m, step);
|
||||
const to = stationLabel(run.end_m, step);
|
||||
button.textContent = `${from === to ? from : `${from} ~ ${to}`} · 확폭 ${Number(run.widening_m.toFixed(2))} m`;
|
||||
const size = area?.(run);
|
||||
const areaText = document.createElement("span");
|
||||
areaText.className = "b05-structure__widening-area";
|
||||
areaText.textContent =
|
||||
size !== undefined && Number.isFinite(size) ? `${size.toFixed(2)} ㎡` : "";
|
||||
button.append(areaText);
|
||||
button.title = on ? "확폭 포함 — 누르면 규격 폭만" : "규격 폭만 — 누르면 확폭 포함";
|
||||
button.addEventListener("click", () => {
|
||||
off = toggleRun(list, off, run);
|
||||
|
||||
@@ -606,3 +606,20 @@
|
||||
.b05-structure__widening [data-widening-run][aria-pressed="true"]::before {
|
||||
content: "✓ ";
|
||||
}
|
||||
|
||||
/* 확폭 면적(138-2) — 단추 안 오른쪽 끝 · 끈 구간 회색 */
|
||||
.b05-structure__widening [data-widening-run] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
}
|
||||
|
||||
.b05-structure__widening-area {
|
||||
margin-left: auto;
|
||||
padding-left: var(--spacing-8, 8px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b05-structure__widening [aria-pressed="false"] .b05-structure__widening-area {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@@ -50,5 +50,18 @@ const out = {
|
||||
toggledOn: w.toggleRun(runs, off, runs[0]),
|
||||
stale: w.offForRuns(runs, "20-60,0-5,100-130"),
|
||||
empty: w.offForRuns([], "20-60"),
|
||||
// 확폭 면적(138-2) — 앞뒤 확폭 0 측점까지 사다리꼴 · 범위로 자름
|
||||
area: runs.map((r) => w.wideningArea(stations, r, { start: 0, end: 140 })),
|
||||
areaClipped: w.wideningArea(stations, runs[0], { start: 30, end: 140 }),
|
||||
areaSingle: w.wideningArea(
|
||||
[at(0), at(20, 1, 0), at(40)],
|
||||
{ start_m: 20, end_m: 20, widening_m: 1 },
|
||||
{ start: 0, end: 40 },
|
||||
),
|
||||
areaMissing: w.wideningArea(
|
||||
stations,
|
||||
{ start_m: 999, end_m: 999, widening_m: 1 },
|
||||
{ start: 0, end: 140 },
|
||||
),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -58,3 +58,19 @@ def test_끈_구간_글_왕복(out):
|
||||
def test_안_맞는_끈_값은_무시_저장_때_빠짐(out):
|
||||
assert out["stale"] == "20-60"
|
||||
assert out["empty"] == ""
|
||||
|
||||
|
||||
def test_확폭_면적_사다리꼴_앞뒤_0_측점까지(out):
|
||||
# 0~20: (0+0.3)/2×20=3 · 20~40: 10.5 · 40~60: 11.5 · 60~80: 4 → 29
|
||||
# 80~100: 5 · 100~120: 11 · 120~140: 6 → 22
|
||||
assert [round(a, 6) for a in out["area"]] == [29.0, 22.0]
|
||||
|
||||
|
||||
def test_확폭_면적_노면공_범위로_자름(out):
|
||||
# 30 에서 자름: w(30)=0.525 → (0.525+0.75)/2×10=6.375 + 11.5 + 4 = 21.875
|
||||
assert round(out["areaClipped"], 6) == 21.875
|
||||
|
||||
|
||||
def test_한_측점_구간도_면적_자료_없으면_0(out):
|
||||
assert round(out["areaSingle"], 6) == 20.0 # (0+1)/2×20 × 2
|
||||
assert out["areaMissing"] == 0
|
||||
|
||||
Reference in New Issue
Block a user