Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_2
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user