Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_2

This commit is contained in:
2026-10-09 19:14:47 +09:00
13 changed files with 263 additions and 68 deletions
+8 -3
View File
@@ -214,7 +214,7 @@ def compute_cross_design(
높이만큼 낮게 앉으므로 계획고를 그만큼 내려 잡는다. 단면 전체가 평행 이동하므로
횡단경사·측구·사면 규칙은 그대로고 절·성토 면적만 따라 바뀐다(2026-08-30 사용자).
spoil_fill: 이 측점에 선 유용토운반작업장(구 사토장) — `{"side", "width_m", "slope_ratio_n"}`.
shoulder_extend_left_m · shoulder_extend_right_m: 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 ·
shoulder_extend_left_m · shoulder_extend_right_m: 설계선 쉬프트 = 쪽마다 사면 이동(m · ≥ 0 ·
139-6) — 사용자 값 · 저장분은 `stored_shoulder_extend`. TS 짝 `shoulderExtendLeftM` 등.
폭은 **노면 끝**(노견이 시작하는 자리)에서 재고, 그 바깥 성토는 **노선 몫이 아니라
사토장 몫**이라 `fill_area_m2` 에서 뺀다(2026-09-09 확정 ㉠ — 두 번 세지 않기).
@@ -444,8 +444,13 @@ def compute_cross_design(
"carriageway_width_m": round(geometry.half_road_left + geometry.half_road_right, 4),
# 규격 폭과 확폭을 따로 남긴다 — 횡단도 라벨·수량 산출이 둘을 나눠 쓴다.
"carriageway_standard_width_m": round(group["road_width_m"], 4),
"widening_left_m": round(geometry.half_road_left - geometry.half_road, 4),
"widening_right_m": round(geometry.half_road_right - geometry.half_road, 4),
# 확폭 = 곡선부 몫만 — 사면 이동(151-2)이 넓힌 차도는 뺀다(TS 짝 같음).
"widening_left_m": round(
geometry.half_road_left - geometry.half_road - geometry.shoulder_extend_left, 4
),
"widening_right_m": round(
geometry.half_road_right - geometry.half_road - geometry.shoulder_extend_right, 4
),
"cross_slope_pct": round(cross_slope_pct, 4),
"ditch": ditch_spec,
# 결과 — **실제로 섰나**. 읽는 쪽 여섯이 이 뜻으로 쓴다.
@@ -140,10 +140,13 @@ class _SectionGeometry:
#: 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다.
self.ditch_choice = choice
# 설계선 쉬프트 = 쪽마다 노견 확장(≥ 0 · 139-6) — 단면유형 · 측구 판정은 **옮기기 전**
# 노견에서(위) · 노체 · 측구 · 사면이 같이 바깥으로 · 차도는 그대로. ⚠ TS 짝 `SectionGeometry`.
# 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
# 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도.
# ⚠ TS 짝 `SectionGeometry`.
self.shoulder_extend_left = max(shoulder_extend_left_m, 0.0)
self.shoulder_extend_right = max(shoulder_extend_right_m, 0.0)
self.half_road_left += self.shoulder_extend_left
self.half_road_right += self.shoulder_extend_right
self.left_extent += self.shoulder_extend_left
self.right_extent += self.shoulder_extend_right
@@ -235,6 +235,8 @@ export function appendCrossDesignOverlay(
// 노폭 라벨(2026-09-06 사용자 지시) — 확폭이 걸린 측점인지 눈으로 바로 알게 한다.
// 확폭이 없으면 규격 폭만, 있으면 「4.5m (규격 3.0 + 확폭 1.5)」로 적는다.
const widened = (design.widening_left_m ?? 0) + (design.widening_right_m ?? 0);
// 사면 이동(151-2) — 노견째 바깥으로 옮겨 늘어난 차도 몫.
const moved = (design.shoulder_extend_left_m ?? 0) + (design.shoulder_extend_right_m ?? 0);
const standardWidth = design.carriageway_standard_width_m;
const shoulderLeft = roadEdges ? roadEdges.left.offset_m - edges.left.offset_m : null;
const shoulderRight = roadEdges ? edges.right.offset_m - roadEdges.right.offset_m : null;
@@ -245,9 +247,13 @@ export function appendCrossDesignOverlay(
);
label.setAttribute("y", String(labelAt?.y ?? toDisplayY(edges.left.elevation_m) - 6));
label.setAttribute("class", "b06-chart__carriageway-label");
const extras = [
widened > 0.001 ? ` + 확폭 ${widened.toFixed(2)}` : "",
moved > 0.001 ? ` + 이동 ${moved.toFixed(2)}` : "",
].join("");
const widthText =
widened > 0.001 && typeof standardWidth === "number"
? `노폭 ${design.carriageway_width_m.toFixed(2)}m (규격 ${standardWidth.toFixed(2)} + 확폭 ${widened.toFixed(2)})`
extras && typeof standardWidth === "number"
? `노폭 ${design.carriageway_width_m.toFixed(2)}m (규격 ${standardWidth.toFixed(2)}${extras})`
: `노폭 ${design.carriageway_width_m.toFixed(2)}m`;
// 노견은 좌우가 같으면 한 번만 적는다 — 라벨이 길어지면 옆 측점 라벨과 겹친다.
label.textContent =
+60 -28
View File
@@ -4,15 +4,22 @@
*
* 그려진 보라 선(`b06-chart__design-cross`)을 화면 좌표에서 m 로 되돌려 같은 방향 토막을
* 직선 조각 하나로 합치고, 벽 · 본체 폴리곤에 가린 부분을 뺀 **보이는 조각**마다 투명 판정
* 선을 구조물 판정면 아래에 깐다. 고르면 그 조각을 강조하고 [+ − ⤢] 왼쪽에 치수를 적는다.
* 선을 구조물 판정면 아래에 깐다. 고르면 그 조각을 강조하고 [+ − ⤢] 왼쪽에 치수를 두 줄로 적는다.
* 구역 판정(`crossZoneAt` · 도로 · 절토 · 성토)은 설계선 쉬프트(139-6)도 쓴다.
* 고름 상태는 페이지에 한 칸(측점 + 조각 번호) — 카드를 다시 그려도 되살아난다.
* 고른 조각의 쪽 노견을 [◀ ▶] 0.1 m 로 넓히고 좁힘(139-6 쉬프트 · 노폭 안쪽 금지 = 0 에서 멈춤).
* 고른 조각 쪽 사면을 노견째 이동 패널(◀ ↺ ▶ · 방향키 · 0.1 m)로 옮김(139-6 쉬프트 · 151-3 ·
* 노폭 안쪽 금지 = 0 에서 멈춤).
* ========================================================================== */
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import { clipSegment, type XY } from "../A00_Common/b_polygon_clip";
import { sideRole } from "@util/common_util_cross_design_geometry";
import {
bindArrowKeys,
buildPanelShell,
dpadButton,
makeRow,
} from "./B06_Section_UI_Cross_Panel_Base";
const SVG_NS = "http://www.w3.org/2000/svg";
const DESIGN_CLASS = "b06-chart__design-cross";
@@ -205,12 +212,13 @@ export function pieceMeasure(
return { lengthM: Math.hypot(horizontalM, heightM), horizontalM, heightM, grade };
}
/** 두 줄(151-1) — 1줄 = 길이 · 수평 · 2줄 = 높이 · 구배(줄바꿈 글자로 가름). */
export function pieceLabel(design: ZoneDesign, piece: DesignPiece): string {
const { zone } = pieceZone(design, piece);
const m = pieceMeasure(piece, zone);
return (
`${zone === "road" ? "길이" : "사면길이"} ${m.lengthM.toFixed(2)}m · ` +
`수평 ${m.horizontalM.toFixed(2)}m · 높이 ${m.heightM.toFixed(2)}m · ${m.grade}`
`수평 ${m.horizontalM.toFixed(2)}m\n높이 ${m.heightM.toFixed(2)}m · ${m.grade}`
);
}
@@ -239,7 +247,7 @@ const pointsOf = (element: Element): XY[] => {
.filter((p) => p.length === 2 && p.every(Number.isFinite));
};
/** 설계선 쉬프트(139-6) — 쪽마다 노견 확장(m · ≥ 0). */
/** 설계선 쉬프트(139-6) — 쪽마다 사면 이동(m · ≥ 0 · 노견째 · 151-2). */
export interface ShoulderShift {
valueOf: (side: CrossSide) => number;
set: (side: CrossSide, valueM: number) => void;
@@ -247,7 +255,7 @@ export interface ShoulderShift {
/** 쉬프트 걸음(m). */
export const SHIFT_STEP_M = 0.1;
/** 화면 ◀(−1) · ▶(+1) 한 걸음 → 그 쪽 노견 확장 새 값(0.1 m 눈금 · 0 아래 금지).
/** 화면 ◀(−1) · ▶(+1) 한 걸음 → 그 쪽 사면 이동 새 값(0.1 m 눈금 · 0 아래 금지).
* `kx` = 화면 x / offset 기울기(+offset = 좌 · 화면 왼쪽이면 kx < 0). */
export function shiftedExtend(now: number, side: CrossSide, screenDir: -1 | 1, kx: number): number {
const offsetDelta = screenDir * Math.sign(kx) * SHIFT_STEP_M;
@@ -259,6 +267,8 @@ export interface DesignLinePick {
pieces: DesignPiece[];
/** 치수 칸 — 카드가 [+ − ⤢] 앞에 붙임 · 고름 없으면 숨김. */
label: HTMLElement;
/** 사면 이동 패널(151-3) — 카드가 그림 칸에 붙임 · `shift` 없거나 고름 없으면 숨김. */
panel: HTMLElement;
setActive: (index: number | null) => void;
}
@@ -272,7 +282,7 @@ export function attachDesignLinePick(input: {
x: (offset: number) => number;
toDisplayY: (elevation: number) => number;
onPick: (index: number) => void;
/** 있으면 고른 조각 쪽 노견을 [◀ ▶] 로 옮김(139-6). */
/** 있으면 고른 조각 쪽 사면을 이동 패널로 옮김(139-6 · 151-3). */
shift?: ShoulderShift;
}): DesignLinePick {
const { layer, design, x, toDisplayY } = input;
@@ -319,40 +329,62 @@ export function attachDesignLinePick(input: {
const label = document.createElement("span");
label.className = "b06-cross-card__line-dims";
label.hidden = true;
const text = document.createElement("span");
label.append(text);
let side: CrossSide | null = null;
let active: number | null = null;
const { shift } = input;
if (shift) {
const button = (glyph: string, dir: -1 | 1, tip: string): HTMLButtonElement => {
const el = document.createElement("button");
el.type = "button";
el.className = "b06-cross-card__zoom-btn b06-cross-card__line-shift";
el.textContent = glyph;
el.title = tip;
el.addEventListener("click", (event) => {
event.stopPropagation();
if (side) shift.set(side, shiftedExtend(shift.valueOf(side), side, dir, kx));
});
return el;
};
label.append(
button("◀", -1, "고른 선 쪽 노견을 화면 왼쪽으로 0.1 m(노폭 안쪽은 못 감)"),
button("▶", 1, "고른 선 쪽 노견을 화면 오른쪽으로 0.1 m(노폭 안쪽은 못 감)"),
);
}
// 사면 이동 패널(151-3) — B06 공용 조정창 꼴(머리 · 「이동」 줄 십자 · 방향키 ←→).
// ✕ = 같은 조각 다시 고름(카드가 풂) · ↺ = 그 쪽 0.
const shell = buildPanelShell(() => {
if (active !== null) input.onPick(active);
});
const readout = document.createElement("div");
readout.className = "b06-structure-panel__value";
const move = makeRow("이동");
move.controls.classList.add("b06-structure-panel__buttons");
const step = (dir: -1 | 1) => () => {
if (side && shift) shift.set(side, shiftedExtend(shift.valueOf(side), side, dir, kx));
};
move.controls.append(
dpadButton(
"◀",
"left",
"고른 사면을 화면 왼쪽으로 0.1 m(노견째 · 노폭 안쪽은 못 감)",
step(-1),
),
dpadButton("↺", "reset", "이 쪽 이동 0", () => {
if (side && shift) shift.set(side, 0);
}),
dpadButton(
"▶",
"right",
"고른 사면을 화면 오른쪽으로 0.1 m(노견째 · 노폭 안쪽은 못 감)",
step(1),
),
);
shell.root.append(readout, move.row);
const arrows = bindArrowKeys(shell.root, () => [move.controls]);
return {
pieces,
label,
panel: shell.root,
setActive: (index) => {
const piece = index === null ? undefined : pieces[index];
hits.forEach((hit, i) => hit.classList.toggle("is-active", !!piece && i === index));
label.hidden = !piece;
active = piece ? index : null;
side = piece ? pieceZone(design, piece).side : null;
const extend = side && shift ? shift.valueOf(side) : 0;
text.textContent = piece
? pieceLabel(design, piece) + (extend > 0 ? ` · 노견 +${extend.toFixed(1)}m` : "")
label.textContent = piece
? pieceLabel(design, piece) + (extend > 0 ? ` · 이동 +${extend.toFixed(1)}m` : "")
: "";
const open = !!(piece && shift);
shell.root.classList.toggle("is-hidden", !open);
shell.title.textContent = side === "left" ? "사면 이동 (좌)" : "사면 이동 (우)";
readout.textContent = `이동 ${extend.toFixed(1)}m`;
arrows.detach();
if (open) arrows.attach();
},
};
}
+1
View File
@@ -662,6 +662,7 @@ export function createCrossSectionCard(
const zoomBar = buildZoomControls(zoomPan, widthActions);
if (linePick) zoomBar.prepend(linePick.label); // 치수 = [+ − ⤢] 왼쪽(137-2)
chartWrap.append(svg, readout.root, zoomBar, panel.root);
if (linePick) chartWrap.append(linePick.panel); // 사면 이동 패널(151-3)
for (const panel of bodies.panels) chartWrap.append(panel.root);
if (activeArea) {
setBandActive(activeArea);
@@ -223,14 +223,39 @@
background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent);
}
/* 고른 보라 선 치수(137-2) — [+ − ⤢] 왼쪽 같은 줄. */
/* 고른 보라 선 치수(137-2) — [+ − ⤢] 왼쪽 · 두 줄(151-1 · 길이 · 수평 / 높이 · 구배). */
.b06-cross-card__line-dims {
display: flex;
align-items: center;
padding: 0 var(--spacing-4);
align-self: center;
padding: 1px var(--spacing-4);
color: var(--color-royal-amethyst);
font-size: 0.75rem;
white-space: nowrap;
line-height: 1.25;
text-align: right;
white-space: pre-line;
}
/* 좁은 카드(3열 등)는 막대 옆에 두면 왼쪽 면적표를 덮음 — 표 아래 높이로 내려 오른쪽에 붙임(151-1). */
.b06-cross-card__chart-wrap {
container-type: inline-size;
}
@container (max-width: 620px) {
.b06-cross-card__zoom {
overflow: visible;
}
.b06-cross-card__line-dims {
position: absolute;
top: calc(var(--spacing-8) + var(--b06-areas-height, 3.6rem));
right: -1px;
border: 1px solid var(--color-border);
border-radius: var(--radius-inputs);
background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent);
}
.b06-cross-card__line-dims + .b06-cross-card__zoom-btn {
border-left: none;
}
}
.b06-cross-card__line-dims[hidden] {
+8 -3
View File
@@ -113,7 +113,7 @@ export interface CrossDesignOptions {
* 짝: 파이썬 `compute_cross_design(spoil_fill=…)`.
*/
spoilFill?: { side: "left" | "right"; widthM: number; slopeRatioN?: number | null } | null;
/** 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6). 짝: 파이썬 `shoulder_extend_*_m`. */
/** 설계선 쉬프트 = 쪽마다 사면 이동(m · ≥ 0 · 139-6 · 151-2 노견째). 짝: 파이썬 `shoulder_extend_*_m`. */
shoulderExtendLeftM?: number;
shoulderExtendRightM?: number;
}
@@ -518,8 +518,13 @@ export function computeCrossDesign(
carriageway_width_m: round4(geometry.halfRoadLeft + geometry.halfRoadRight),
// 규격 폭과 확폭을 따로 남긴다 — 횡단도 라벨·수량 산출이 둘을 나눠 쓴다(짝: 파이썬).
carriageway_standard_width_m: round4(group.road_width_m),
widening_left_m: round4(geometry.halfRoadLeft - geometry.halfRoad),
widening_right_m: round4(geometry.halfRoadRight - geometry.halfRoad),
// 확폭 = 곡선부 몫만 — 사면 이동(151-2)이 넓힌 차도는 뺀다(짝: 파이썬).
widening_left_m: round4(
geometry.halfRoadLeft - geometry.halfRoad - geometry.shoulderExtendLeft,
),
widening_right_m: round4(
geometry.halfRoadRight - geometry.halfRoad - geometry.shoulderExtendRight,
),
cross_slope_pct: round4(crossSlopePct),
ditch: ditchSpec,
ditch_enabled: geometry.hasDitch,
@@ -108,7 +108,7 @@ export class SectionGeometry {
hasDitch: boolean;
/** 사용자가 정한 선택(`null` = 자동). 결과(`hasDitch`)와 다른 값이다. */
ditchChoice: boolean | null;
/** 노견 확장(m · 139-6) — 쪽마다. */
/** 사면 이동(m · 139-6 · 151-2) — 쪽마다 · 차도 반폭에 들어 있음. */
shoulderExtendLeft = 0;
shoulderExtendRight = 0;
ditchPoints: Array<[number, number]> = [];
@@ -210,10 +210,12 @@ export class SectionGeometry {
/** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */
this.ditchChoice = choice;
// 설계선 쉬프트 = 쪽마다 노견 확장(≥ 0 · 139-6) — 단면유형 · 측구 판정은 **옮기기 전** 노견에서
// (위) · 노체 · 측구 · 사면이 같이 바깥으로 · 차도는 그대로. ⚠ 파이썬 짝 `_SectionGeometry`.
// 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
// 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. ⚠ 파이썬 짝 `_SectionGeometry`.
this.shoulderExtendLeft = Math.max(params.shoulderExtendLeftM ?? 0, 0);
this.shoulderExtendRight = Math.max(params.shoulderExtendRightM ?? 0, 0);
this.halfRoadLeft += this.shoulderExtendLeft;
this.halfRoadRight += this.shoulderExtendRight;
this.leftExtent += this.shoulderExtendLeft;
this.rightExtent += this.shoulderExtendRight;
@@ -112,6 +112,8 @@ class FakeNode {
globalThis.document = {
createElement: (tag) => new FakeNode(tag),
createElementNS: (_ns, tag) => new FakeNode(tag),
addEventListener: () => {}, // 조정창 공용 부품(사면 이동 패널 · 151-3)이 불러올 때 닮
removeEventListener: () => {},
};
function loadTs(file) {
+60 -12
View File
@@ -1,5 +1,5 @@
/* 139-6 고른 설계선 [◀ ▶] 노견 확장 헬퍼 — `_Cross_Design_Pick.attachDesignLinePick` 을 그 자리에서
* 트랜스파일해 흉내 DOM 으로 돌림. 짝 = `test_139_6_line_shift.py`.
/* 139-6 고른 설계선 사면 이동 헬퍼 — `_Cross_Design_Pick.attachDesignLinePick` 을 그 자리에서
* 트랜스파일해 흉내 DOM 으로 돌림 · 151-3 이동 패널(◀ ↺ ▶ · 방향키 ←→). 짝 = `test_139_6_line_shift.py`.
*/
const fs = require("fs");
const path = require("path");
@@ -15,6 +15,7 @@ class Node {
this.attrs = {};
this.listeners = {};
this.hidden = false;
this.isConnected = true;
this._text = "";
this._cls = new Set();
const self = this;
@@ -24,6 +25,16 @@ class Node {
contains: (c) => self._cls.has(c),
};
}
get descendants() {
return this.children.flatMap((c) => [c, ...c.descendants]);
}
/** `.이름` 한 개만 — 패널 공용 부품이 슬롯 단추를 찾는 꼴. */
querySelector(sel) {
return this.descendants.find((n) => n._cls.has(sel.slice(1))) ?? null;
}
getClientRects() {
return this._cls.has("is-hidden") ? [] : [{}];
}
set className(v) {
this._cls = new Set(String(v).split(/\s+/).filter(Boolean));
}
@@ -56,10 +67,19 @@ class Node {
(this.listeners.click ?? []).forEach((fn) => fn({ stopPropagation: () => undefined }));
}
}
const docListeners = {};
globalThis.document = {
createElement: (t) => new Node(t),
createElementNS: (_n, t) => new Node(t),
addEventListener: (t, fn) => (docListeners[t] ??= []).push(fn),
removeEventListener: (t, fn) => {
docListeners[t] = (docListeners[t] ?? []).filter((f) => f !== fn);
},
};
const key = (name) =>
[...(docListeners.keydown ?? [])].forEach((fn) =>
fn({ key: name, target: null, preventDefault: () => undefined }),
);
function loadTs(file) {
const full = path.resolve(file);
@@ -115,37 +135,65 @@ layer.append(
]),
);
const values = { left: 0, right: 0 };
const picks = [];
const handle = pick.attachDesignLinePick({
layer,
design,
x,
toDisplayY: y,
onPick: () => undefined,
onPick: (i) => picks.push(i),
shift: {
valueOf: (side) => values[side],
set: (side, v) => (values[side] = v),
},
});
const [left, right] = handle.label.children.filter((c) => c.tagName === "button");
const panel = handle.panel;
const slot = (name) => panel.querySelector(`.b06-structure-panel__btn--${name}`);
const close = panel.querySelector(".b06-structure-panel__close");
const pieceAt = (offset) =>
handle.pieces.findIndex(
(p) =>
Math.min(p.from.offset, p.to.offset) <= offset &&
offset <= Math.max(p.from.offset, p.to.offset),
);
const hiddenAtStart = panel.classList.contains("is-hidden");
const log = [];
handle.setActive(pieceAt(4)); // 좌 사면
left.click(); // 화면 왼쪽 = 좌측 바깥 → +0.1
left.click();
const titleLeft = panel.descendants.find((n) =>
n._cls.has("b06-structure-panel__title"),
).textContent;
slot("left").click(); // 화면 왼쪽 = 좌측 바깥 → +0.1
key("ArrowLeft"); // 방향키 = 십자
log.push({ ...values });
right.click(); // 안쪽 → 0.1
right.click();
right.click(); // 0 아래 금지
key("ArrowRight"); // 안쪽 → 0.1
slot("right").click();
key("ArrowRight"); // 0 아래 금지
log.push({ ...values });
handle.setActive(pieceAt(-4)); // 우 사면
left.click(); // 화면 왼쪽 = 우측 안쪽 → 0 에서 멈춤
right.click(); // 우측 바깥 → +0.1
key("ArrowLeft"); // 화면 왼쪽 = 우측 안쪽 → 0 에서 멈춤
slot("right").click(); // 우측 바깥 → +0.1
log.push({ ...values });
handle.setActive(pieceAt(-4));
const text = handle.label.textContent;
process.stdout.write(JSON.stringify({ log, text, buttons: [left.textContent, right.textContent] }));
const readout = panel.descendants.find((n) => n._cls.has("b06-structure-panel__value")).textContent;
slot("reset").click(); // ↺ = 그 쪽 0
const afterReset = { ...values };
close.click(); // ✕ = 같은 조각 다시 고름(카드가 풂)
handle.setActive(null);
key("ArrowRight"); // 숨은 패널은 방향키 안 먹음
process.stdout.write(
JSON.stringify({
log,
text,
readout,
titleLeft,
afterReset,
afterHidden: { ...values },
picks,
pieceRight: pieceAt(-4),
hiddenAtStart,
hiddenAfter: panel.classList.contains("is-hidden"),
labelButtons: handle.label.children.length,
slots: ["left", "reset", "right"].map((n) => slot(n).textContent),
}),
);
+14 -2
View File
@@ -57,8 +57,9 @@ def test_치수_사면길이_수평_높이_구배(out) -> None:
assert abs(fill["lengthM"] - 13**0.5) < 1e-9
assert out["measure"]["road"]["grade"] == "2.0%" # 노면 = %
assert out["measure"]["flatSlope"] == "0.0%"
assert out["label"]["fill"] == "사면길이 3.61m · 수평 3.00m · 높이 2.00m · 1:1.50"
assert out["label"]["road"] == "길이 2.50m · 수평 2.50m · 높이 0.05m · 2.0%"
# 두 줄(151-1) — 1줄 = 길이 · 수평 · 2줄 = 높이 · 구배
assert out["label"]["fill"] == "사면길이 3.61m · 수평 3.00m\n높이 2.00m · 1:1.50"
assert out["label"]["road"] == "길이 2.50m · 수평 2.50m\n높이 0.05m · 2.0%"
def test_구역_도로_절토_성토(out) -> None:
@@ -100,10 +101,21 @@ def test_카드_배선_하나만() -> None:
assert view.count("dropLine();") >= 4 # 본체 · 벽 · 카드 풂 · 면적
assert "setLineActive(pickedLineAt(section.station_id));" in view # 다시 그려도 되살림
assert "zoomBar.prepend(linePick.label);" in view # 치수 = [+ − ⤢] 왼쪽
assert "chartWrap.append(linePick.panel);" in view # 사면 이동 패널(151-3)
css = _src("B06_Section/B06_Section_UI_Style_Cross_Areas.css")
assert ".b06-chart__design-hit.is-active {" in css
def test_치수_두_줄_좁은_카드는_표_아래로() -> None:
"""151-1 — 줄바꿈 글자를 살리고 오른쪽 맞춤 · 좁은 카드(3열 등)는 표 높이 아래로 내려 표를 안 덮음."""
css = _src("B06_Section/B06_Section_UI_Style_Cross_Controls.css")
dims = css.split(".b06-cross-card__line-dims {", 1)[1].split("}", 1)[0]
assert "white-space: pre-line;" in dims and "text-align: right;" in dims
assert "container-type: inline-size;" in css
narrow = css.split("@container (max-width: 620px) {", 1)[1]
assert "top: calc(var(--spacing-8) + var(--b06-areas-height, 3.6rem));" in narrow
def test_폴리곤_자르기는_공용_한_벌() -> None:
b07 = _src("B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts")
assert "function clipSegment(" not in b07 and "function pointInPolygon(" not in b07
+19 -8
View File
@@ -1,4 +1,4 @@
"""139-6 고른 설계선 [◀ ▶] — 고른 조각 쪽 노견 확장 0.1 m 걸음 · 노폭 안쪽 금지(0 에서 멈춤).
"""139-6 · 151-3 고른 설계선 사면 이동 패널 — 고른 조각 쪽만 0.1 m 걸음 · 노폭 안쪽 금지(0 에서 멈춤).
화면 TS(`_Cross_Design_Pick`)를 흉내 DOM 으로 돌린 값(`helper_139_6_line_shift.cjs`).
"""
@@ -26,16 +26,27 @@ def out() -> dict:
return json.loads(done.stdout)
def test_화면_방향대로_그_쪽_노견만(out: dict) -> None:
def test_화면_방향대로_그_쪽만_단추와_방향키(out: dict) -> None:
first, back, right = out["log"]
assert first == {"left": 0.2, "right": 0} # 좌 사면 고름 · ◀ 두 번 = 좌 바깥 0.2
assert back == {"left": 0, "right": 0} # ▶ 세 번 = 0 에서 멈춤(노폭 안쪽 금지)
assert right == {"left": 0, "right": 0.1} # 우 사면 · ◀ = 안쪽 0 · ▶ = 바깥 0.1
assert first == {"left": 0.2, "right": 0} # 좌 사면 · ◀ 단추 + ← 키 = 좌 바깥 0.2
assert back == {"left": 0, "right": 0} # → 키 · ▶ · → 키 = 0 에서 멈춤(노폭 안쪽 금지)
assert right == {"left": 0, "right": 0.1} # 우 사면 · ← = 안쪽 0 · ▶ = 바깥 0.1
def test_치수_줄에_노견_확장_표기(out: dict) -> None:
assert "노견 +0.1m" in out["text"]
assert out["buttons"] == ["◀", "▶"]
def test_치수_줄과_패널에_이동_표기(out: dict) -> None:
assert "이동 +0.1m" in out["text"] and "노견 +" not in out["text"]
assert out["readout"] == "이동 0.1m"
assert out["titleLeft"] == "사면 이동 (좌)"
def test_단추는_패널로_옮김_재설정_닫기(out: dict) -> None:
"""151-3 — 치수 칸 [◀ ▶] 지움 · 패널 ◀ ↺ ▶ · ↺ = 그 쪽 0 · ✕ = 고름 풂 · 숨으면 방향키 안 먹음."""
assert out["labelButtons"] == 0
assert out["slots"] == ["◀", "↺", "▶"]
assert out["afterReset"] == {"left": 0, "right": 0}
assert out["picks"] == [out["pieceRight"]]
assert out["hiddenAtStart"] is True and out["hiddenAfter"] is True
assert out["afterHidden"] == {"left": 0, "right": 0}
def test_페이지가_같은_창구로_세션_재계산() -> None:
@@ -204,6 +204,20 @@ CASES = [
"shoulder_extend_left_m": 2.5,
},
},
{
# 151-2 — 곡선부 확폭과 사면 이동이 같은 쪽에 겹침(확폭 출력엔 이동 몫 안 섞임)
"name": "확폭 좌 + 사면 이동 좌 0.4",
"samples": _ground(0.28),
"design_elevation_m": 100.3,
"options": {
"ground_type": "soil",
"section_mode": "left_cut",
"ditch_side": "left",
"plan_radius_m": 18.0,
"curve_outer_side": "left",
"shoulder_extend_left_m": 0.4,
},
},
]
_TS_RUNNER = """
@@ -381,3 +395,32 @@ def test_시험이_빈_결과로_통과하지_않는다() -> None:
assert sum(1 for item in results if item["fill_area_m2"] > 0) >= 1
assert any(item.get("widening_left_m") or item.get("widening_right_m") for item in results)
assert any(item["cut_rock_area_m2"] > 0 for item in results)
def test_사면_이동은_노견째_평행이동_늘어난_폭은_차도() -> None:
"""151-2 — 노견 폭 그대로 · 차도 끝이 이동량만큼 바깥 · 확폭 출력엔 이동 몫 안 섞임."""
by_name = dict(zip((case["name"] for case in CASES), _python_results(), strict=True))
base = compute_cross_design(
_ground(0.35),
100.4,
standard=STANDARD_CROSS_SECTION,
ground_type="soil",
section_mode="left_cut",
ditch_side="left",
)
moved = by_name["노견 확장 좌 0.7 · 우 1.2"]
for side, sign, extend in (("left", 1, 0.7), ("right", -1, 1.2)):
road = moved["road_edges"][side]["offset_m"]
lane = moved["carriageway_edges"][side]["offset_m"]
base_shoulder = (
base["road_edges"][side]["offset_m"] - base["carriageway_edges"][side]["offset_m"]
)
assert abs((road - lane) - base_shoulder) < 1e-9 # 노견 폭 고정
assert abs(lane - (base["carriageway_edges"][side]["offset_m"] + sign * extend)) < 1e-9
assert abs(moved["carriageway_width_m"] - (base["carriageway_width_m"] + 1.9)) < 1e-9
assert moved["widening_left_m"] == 0.0 and moved["widening_right_m"] == 0.0
curve = by_name["곡선부 확폭 — 바깥쪽 좌"]
both = by_name["확폭 좌 + 사면 이동 좌 0.4"]
assert both["widening_left_m"] == curve["widening_left_m"] > 0
assert abs(both["carriageway_width_m"] - (curve["carriageway_width_m"] + 0.4)) < 1e-9