Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_6' and 'origin/sub_laptop_7' into sub_laptop_8
This commit is contained in:
@@ -103,6 +103,8 @@ export const STATE_REGISTRY = {
|
||||
"cross-columns": { bucket: "pref", scope: "global" },
|
||||
/** B05 · B06 종단 누가토량 알약 자리(309-2) — 그래프 영역 안 비율 `{"x":0~1,"y":0~1}` · 없으면 처음 자리. */
|
||||
"mass-haul-badge-pos": { bucket: "pref", scope: "global" },
|
||||
/** B05 3D 영역 투영(316-2) `perspective · ortho` · 없으면 원근. */
|
||||
"b05-3d-projection": { bucket: "pref", scope: "global" },
|
||||
"section-panel-height": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
|
||||
@@ -170,7 +170,7 @@ async def sync_uphill_overrides_into_designs(
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=stored_standard,
|
||||
rock_boundary_offset_m=design.get("rock_boundary_offset_m"),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
ditch_choice=design.get("ditch_choice"),
|
||||
surface_drop_m=ford_drop_at(float(chainage), ford_drops),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 좌측 패널 칸이 아니라 뷰어에 붙는다 — `B05_Profile_UI_Panel.ts` 에서 떼어냈다(계획서 24-2b).
|
||||
* ========================================================================== */
|
||||
|
||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import { L, button, toggleButton } from "./B05_Profile_UI_Panel_Fields";
|
||||
|
||||
/** 뷰 조작 줄이 부르는 것 — 좌측 패널 콜백의 일부다. */
|
||||
@@ -33,7 +34,8 @@ export function createViewControls(callbacks: ViewControlCallbacks): {
|
||||
(["iso", "top", "front", "side"] as const).forEach((preset) =>
|
||||
viewButtons.append(button(preset.toUpperCase(), () => callbacks.onView(preset), "glass")),
|
||||
);
|
||||
// 원근·직교 두 단추 — 지금 켜진 쪽을 강조한다. 기본은 직교(카메라 기본과 같음).
|
||||
// 원근·직교 두 단추 — 지금 켜진 쪽을 강조한다. 기본은 원근 · 고른 값은 화면 설정에 기억.
|
||||
const initialProjection = readStateRaw("b05-3d-projection") === "ortho" ? "ortho" : "perspective";
|
||||
const projectionButtons = (
|
||||
[
|
||||
["perspective", "원근"],
|
||||
@@ -47,15 +49,17 @@ export function createViewControls(callbacks: ViewControlCallbacks): {
|
||||
el.classList.toggle("is-active", other === kind);
|
||||
el.setAttribute("aria-pressed", String(other === kind));
|
||||
});
|
||||
writeStateRaw("b05-3d-projection", kind);
|
||||
callbacks.onProjection(kind);
|
||||
},
|
||||
"glass",
|
||||
);
|
||||
element.classList.toggle("is-active", kind === "ortho");
|
||||
element.setAttribute("aria-pressed", String(kind === "ortho"));
|
||||
element.classList.toggle("is-active", kind === initialProjection);
|
||||
element.setAttribute("aria-pressed", String(kind === initialProjection));
|
||||
return { kind, element };
|
||||
});
|
||||
viewButtons.append(...projectionButtons.map(({ element }) => element));
|
||||
if (initialProjection === "ortho") callbacks.onProjection("ortho");
|
||||
const visibilityButtons = document.createElement("div");
|
||||
visibilityButtons.className = "b05-route__view-group";
|
||||
visibilityButtons.append(
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* B05_Profile_UI_Viewer_Camera.ts
|
||||
* B05 뷰어의 **원근/직교 두 카메라**와 그 사이 갈아 끼우기.
|
||||
*
|
||||
* 기본은 직교 — 원근(시야각 45°)은 단추로 고른다. 갈아 끼울 때 위치·시선·근평면·
|
||||
* 먼평면과 **보이는 크기**를 그대로 옮기므로 화면이 튀지 않는다.
|
||||
* 기본은 원근(시야각 45°) — 직교는 단추로 고른다(고른 값은 보기 줄이 화면 설정에 기억).
|
||||
* 갈아 끼울 때 위치·시선·근평면·먼평면과 **보이는 크기**를 그대로 옮기므로 화면이 튀지 않는다.
|
||||
*
|
||||
* 카메라 객체가 바뀌므로 쓰는 쪽은 붙잡아 두지 말고 `camera()`로 그때그때 읽을 것.
|
||||
* ========================================================================== */
|
||||
@@ -21,7 +21,7 @@ export function createCameraRig() {
|
||||
const perspective = new THREE.PerspectiveCamera(FOV, 1, 0.1, 100000);
|
||||
perspective.position.set(100, 120, 100);
|
||||
const ortho = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 100000);
|
||||
let kind: ProjectionKind = "ortho";
|
||||
let kind: ProjectionKind = "perspective";
|
||||
let aspect = 1;
|
||||
let halfHeight = 100;
|
||||
|
||||
|
||||
@@ -366,9 +366,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
berm: bermAt(section.chainage_m, design),
|
||||
rockBoundaryOffsetM,
|
||||
cutSlopeRatio: cutSlopeAt(section.chainage_m, design),
|
||||
twoStageSlope:
|
||||
choice?.two_stage_slope ??
|
||||
(design.two_stage_slope === undefined ? true : Boolean(design.two_stage_slope)),
|
||||
twoStageSlope: choice?.two_stage_slope ?? Boolean(design.two_stage_slope),
|
||||
// ⚠ **선택과 결과를 갈라 넘긴다**(2026-09-09). 선택이 있으면 그것을 따르고,
|
||||
// 없으면 옛 저장분의 결과를 **자동값과 다를 때만** 선택으로 살린다.
|
||||
ditchChoice: choice
|
||||
|
||||
@@ -189,7 +189,7 @@ def compute_cross_design(
|
||||
paved: bool = False,
|
||||
standard: dict[str, Any] | None = None,
|
||||
rock_boundary_offset_m: float | None = None,
|
||||
two_stage_slope: bool = True,
|
||||
two_stage_slope: bool = False,
|
||||
cut_slope_ratio: float | None = None,
|
||||
ditch_enabled: bool | None = None,
|
||||
ditch_choice: bool | None = None,
|
||||
@@ -212,7 +212,7 @@ def compute_cross_design(
|
||||
paved: 포장 중첩 여부 — 횡단경사·포장층만 포장 그룹 값으로 바꾼다(기하는 지반유형).
|
||||
standard: B06 설정 패널 편집값(STANDARD_CROSS_SECTION 형태). 요청값 → config 순.
|
||||
rock_boundary_offset_m: 암반 경계선 오프셋(지반선 기준, 음수=하향). 암 지반 2단계 절토용.
|
||||
two_stage_slope: 암 지반에서 암반 경계 기준 2단계 경사 적용 여부(기본 True, 토글로 해제).
|
||||
two_stage_slope: 암 지반에서 암반 경계 기준 2단계 경사 적용 여부(기본 False, 토글로 켬).
|
||||
cut_slope_ratio: 이 측점만 쓰는 절토 경사비(1:n 의 n). 사용자가 카드에서 넣은 값이며
|
||||
None 이면 표준 횡단면 설정값을 그대로 쓴다(2026-09-07 사용자 지시).
|
||||
plan_radius_m: 이 측점의 평면 곡선반경(m). 곡선부 확폭(별표2 Ⅰ.2.나.(4))을 정하는
|
||||
|
||||
@@ -151,7 +151,7 @@ def extend_unclosed_sections(
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
||||
cut_slope_ratio=stored_cut_slope(design),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
ditch_choice=design.get("ditch_choice"),
|
||||
|
||||
@@ -208,7 +208,7 @@ def enforce_spoil_fills(
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=standard_spec,
|
||||
rock_boundary_offset_m=design.get("rock_boundary_offset_m"),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
||||
cut_slope_ratio=stored_cut_slope(design),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
ditch_choice=design.get("ditch_choice"),
|
||||
|
||||
@@ -242,7 +242,7 @@ def enforce_pavement_ranges(
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
||||
cut_slope_ratio=stored_cut_slope(design),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
ditch_choice=design.get("ditch_choice"),
|
||||
@@ -302,7 +302,7 @@ def enforce_design_lines(
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
||||
cut_slope_ratio=stored_cut_slope(design),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
ditch_choice=design.get("ditch_choice"),
|
||||
@@ -358,7 +358,7 @@ def enforce_ford_surface_drops(
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", True)),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
||||
cut_slope_ratio=stored_cut_slope(design),
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
ditch_choice=design.get("ditch_choice"),
|
||||
@@ -566,7 +566,7 @@ def recompute_designs_for_alignment(
|
||||
key,
|
||||
stored.get("rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M),
|
||||
),
|
||||
two_stage_slope=bool(stored.get("two_stage_slope", True)),
|
||||
two_stage_slope=bool(stored.get("two_stage_slope", False)),
|
||||
cut_slope_ratio=(
|
||||
(session_cut_slopes[key] or None)
|
||||
if key in session_cut_slopes
|
||||
@@ -657,7 +657,7 @@ def compute_default_designs(
|
||||
rock_boundary_offset_m=(
|
||||
STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M if rock_offset is None else rock_offset
|
||||
),
|
||||
two_stage_slope=bool(user.get("two_stage_slope", True)),
|
||||
two_stage_slope=bool(user.get("two_stage_slope", False)),
|
||||
cut_slope_ratio=stored_cut_slope(user),
|
||||
ditch_choice=user.get("ditch_choice"),
|
||||
surface_drop_m=ford_drop_at(chainage_m, ford_drops),
|
||||
|
||||
@@ -51,7 +51,7 @@ class CrossDesignRequest(BaseModel):
|
||||
# 암 경계선 오프셋(m, 지면선 기준 하향 음수). 암 지반 2단계 절토 무릎 계산에 쓴다.
|
||||
rock_boundary_offset_m: float | None = None
|
||||
# 암 지반 2단계 경사(암반 경계 아래=암 경사, 위=토사 경사) 적용 여부. 토글로 해제 가능.
|
||||
two_stage_slope: bool = True
|
||||
two_stage_slope: bool = False
|
||||
# 이 측점만 쓰는 암 절토 경사비(1:n 의 n) — 카드에서 넣은 값(2026-09-07 사용자 지시).
|
||||
# None 이면 표준 횡단면 설정값을 쓴다.
|
||||
cut_slope_ratio: float | None = Field(default=None, gt=0)
|
||||
|
||||
@@ -338,8 +338,8 @@ export function buildDesignControls(
|
||||
ditch: design?.ditch_side ?? null,
|
||||
ditchType: design?.ditch_type ?? "standard",
|
||||
paved: design?.paved ?? false,
|
||||
// 암 design일 때만 저장값을 신뢰(토사는 two_stage=false echo가 무의미) — 암 전환 시 기본 복합경사(4번).
|
||||
twoStage: design && isRock(design.ground_type) ? (design.two_stage_slope ?? true) : true,
|
||||
// 저장된 선택은 지반유형과 상관없이 그대로 · 값이 없으면 기본 꺼짐(복합경사 기본 = 꺼짐).
|
||||
twoStage: design?.two_stage_slope ?? false,
|
||||
// ⚠ **결과가 아니라 선택을 읽는다.** 결과를 읽으면 한 번 저장된 뒤로
|
||||
// 자동 판정이 영영 안 돈다(2026-09-09에 갈라낸 자리).
|
||||
ditchEnabled: design?.ditch_choice ?? null,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* (`ford_slope_deg` → `slope_pct`)로 지대 낮은 쪽 한 경사로 기울여 낸 면 · 설계 중심점이 그 위.
|
||||
* 노면 밖은 설계선 그대로 — 노면 끝에서 성토 · 절토 사면이 이어짐(늘어나지 못한 자리 = 성토).
|
||||
* · 포장층 = 노면공 포장 형상(`appendPavementOverlay`)을 노견까지.
|
||||
* · 월류 높이선(회색 점선) = 노면 위로 월류 높이(`depth_m`)만큼 · 노면과 같은 각도.
|
||||
* · 월류 높이선(회색 점선) = 노면 위로 월류 높이(`depth_m`)만큼 · 노면과 같은 각도 · 성토 쪽은 성토선을
|
||||
* 점선으로 늘여 이음(315-2).
|
||||
* · 도수로 · 돌붙임 띠 = 유출(지대 낮은 쪽) 성토사면.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -119,15 +120,12 @@ export function appendFordPavementOverlay(
|
||||
if (typeof spec.chainage_m === "number")
|
||||
el.setAttribute("data-structure-id", `pipe-${spec.chainage_m.toFixed(2)}`);
|
||||
});
|
||||
// 월류 높이선 — 콘크리트 윗면(설계 노면) 위로 월류 높이만큼 · 같은 각도(그림_183)
|
||||
// 월류 높이선 — 콘크리트 윗면(설계 노면) 위로 월류 높이만큼 · 같은 각도(그림_183) · 성토 쪽 = 성토선 연장(315-2)
|
||||
const depth = spec.depth_m ?? 0;
|
||||
if (depth > 0)
|
||||
svg.append(
|
||||
line(
|
||||
[
|
||||
`${x(edges.left.offset_m)},${y(edges.left.elevation_m + depth)}`,
|
||||
`${x(edges.right.offset_m)},${y(edges.right.elevation_m + depth)}`,
|
||||
],
|
||||
overflowLine(design, depth).map((p) => `${x(p.offset)},${y(p.elevation)}`),
|
||||
"b06-chart__ford-deck-plan",
|
||||
),
|
||||
);
|
||||
@@ -145,6 +143,35 @@ export function fordEdges(design: CrossDesign): CrossDesign["road_edges"] | unde
|
||||
return reach.side === "left" ? { ...edges, left: end } : { ...edges, right: end };
|
||||
}
|
||||
|
||||
/**
|
||||
* 월류 높이선(315-2) — 노면 양 끝 위 월류 높이 · 노면과 같은 각도. 성토 쪽(노면 끝 밖 설계선이 내려감)은
|
||||
* 성토 사면선을 노면 끝 너머 위 · 안쪽으로 늘여 월류 높이선과 만나는 자리에서 끝 → 노면 끝까지 그 연장선.
|
||||
* 절토 쪽 · 늘인 노면 끝(294-3)은 노면 끝 위 그대로. 좌(+offset) → 우 차례.
|
||||
*/
|
||||
export function overflowLine(design: CrossDesign, depth: number): OffsetPoint[] {
|
||||
const edges = fordEdges(design);
|
||||
if (!edges) return [];
|
||||
const { left, right } = edges;
|
||||
const span = Math.abs(left.offset_m - right.offset_m);
|
||||
const side = (edge: typeof left, other: typeof left): OffsetPoint[] => {
|
||||
const top = { offset: edge.offset_m, elevation: edge.elevation_m + depth };
|
||||
const outward = Math.sign(edge.offset_m - other.offset_m) || 1;
|
||||
const next = (design.design_line ?? [])
|
||||
.filter((p) => (p.offset_m - edge.offset_m) * outward > 1e-6)
|
||||
.sort((a, b) => (a.offset_m - b.offset_m) * outward)[0];
|
||||
const cut = edge.offset_m === design.tilt_reach?.offset_m; // 늘인 끝 = 원지반(294-3 · 절토)
|
||||
if (cut || !next || next.elevation_m >= edge.elevation_m - 1e-6 || span < 1e-9) return [top];
|
||||
// 안쪽 d m 에서 성토선 = edge + fill·d · 월류선 = edge + depth + road·d → d = depth / (fill − road)
|
||||
const fill = (edge.elevation_m - next.elevation_m) / Math.abs(next.offset_m - edge.offset_m);
|
||||
const road = (other.elevation_m - edge.elevation_m) / span;
|
||||
if (fill <= road + 1e-9) return [top];
|
||||
const d = Math.min(depth / (fill - road), span);
|
||||
const meet = { offset: edge.offset_m - outward * d, elevation: edge.elevation_m + fill * d };
|
||||
return [{ offset: edge.offset_m, elevation: edge.elevation_m }, meet];
|
||||
};
|
||||
return [...side(left, right), ...side(right, left).reverse()];
|
||||
}
|
||||
|
||||
/** 원지반 꺾은선의 그 오프셋 표고(밖은 끝값). */
|
||||
function groundZ(ground: readonly OffsetPoint[], offset: number): number | null {
|
||||
if (!ground.length) return null;
|
||||
@@ -193,16 +220,16 @@ export function outletFillLine(
|
||||
|
||||
/**
|
||||
* 물넘이 도수로 · 돌붙임 선(286-3 · 294-4) — 낮은 쪽 끝단에서 성토사면 따라 · 길이 비움 = 원지반 닿으면 끝 ·
|
||||
* 길이 넣음 = 그 길이(사면보다 길면 원지반 따라 이어) · 양측 절토(늘인 노면 끝이 원지반) = 길이 없으면 안 그림.
|
||||
* 횡단 띠 · 3D 띠가 같이 씀(그림 전용).
|
||||
* 길이 넣음 = 그 길이(사면보다 길면 원지반 따라 이어 · 315-1 이은 조각까지 한 선) · 양측 절토(늘인 노면 끝이
|
||||
* 원지반) = 길이 없으면 안 그림. 횡단 띠 · 3D 띠가 같이 씀(그림 전용).
|
||||
*/
|
||||
export function fordChuteLine(
|
||||
design: CrossDesign,
|
||||
chute?: { length_m?: number },
|
||||
ground?: readonly OffsetPoint[],
|
||||
): OffsetPoint[] {
|
||||
return (
|
||||
chuteRun([{ points: outletFillLine(design, ground) }], chute?.length_m, ground)[0]?.points ?? []
|
||||
return chuteRun([{ points: outletFillLine(design, ground) }], chute?.length_m, ground).flatMap(
|
||||
(run, i) => (i ? run.points.slice(1) : run.points),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
||||
ditch_side: design.ditch_side ?? null,
|
||||
ditch_type: design.ditch_type ?? "standard",
|
||||
paved: design.paved,
|
||||
two_stage_slope: design.two_stage_slope ?? true,
|
||||
two_stage_slope: design.two_stage_slope ?? false,
|
||||
ditch_choice: design.ditch_choice ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ export function computeCrossDesign(
|
||||
const rockBoundaryOffsetM = options.rockBoundaryOffsetM ?? null;
|
||||
// 2단계 절토는 암 프리셋에서만, 암반 경계 오프셋이 있을 때만 켠다.
|
||||
const enableTwoStage =
|
||||
presetKey === "rock" && (options.twoStageSlope ?? true) && rockBoundaryOffsetM !== null;
|
||||
presetKey === "rock" && (options.twoStageSlope ?? false) && rockBoundaryOffsetM !== null;
|
||||
// 곡선부 확폭 — 표는 하한이고, 확폭을 더한 유효너비가 법정 상한(5m)을 넘지 않게 자른다.
|
||||
// 짝: 파이썬 `compute_cross_design`. 표·상한 값은 config 한 곳에서 온다.
|
||||
const outerSide = options.curveOuterSide;
|
||||
@@ -528,7 +528,7 @@ export function computeCrossDesign(
|
||||
// 아니다(2026-09-07, 짝: `B06_Section_Engine_Design.py`). 적용 결과를 저장하면 토사
|
||||
// 측점처럼 못 쓰는 자리에서 false 가 저장되고, 그 false 가 다음 재계산 인자로 되먹여져
|
||||
// 사용자의 「켬」이 영구히 사라졌다.
|
||||
two_stage_slope: options.twoStageSlope ?? true,
|
||||
two_stage_slope: options.twoStageSlope ?? false,
|
||||
fill_slope_ratio: round4(geometry.fillRatio),
|
||||
roadbed_width_m: round4(geometry.leftExtent + geometry.rightExtent),
|
||||
carriageway_width_m: round4(geometry.halfRoadLeft + geometry.halfRoadRight),
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/* 315 물넘이포장 도수로 · 돌붙임 길이(성토) · 월류 높이선 성토 쪽 연장 헬퍼 — 진짜 TS 를 Node 로 돌려 JSON 한 벌로. */
|
||||
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 load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const near = (m) => {
|
||||
if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`));
|
||||
if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`));
|
||||
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
|
||||
return new Proxy({}, { get: () => () => {} });
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
|
||||
class Node {
|
||||
constructor(tag) {
|
||||
this.tag = tag;
|
||||
this.attrs = {};
|
||||
this.children = [];
|
||||
this.dataset = {};
|
||||
this.style = {};
|
||||
}
|
||||
setAttribute(k, v) {
|
||||
this.attrs[k] = String(v);
|
||||
}
|
||||
getAttribute(k) {
|
||||
return this.attrs[k] ?? null;
|
||||
}
|
||||
append(...kids) {
|
||||
this.children.push(...kids);
|
||||
}
|
||||
}
|
||||
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
|
||||
const hatch = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"));
|
||||
const fillGround = hatch.groundLine(input.fillSamples);
|
||||
const dipGround = hatch.groundLine(input.dipSamples);
|
||||
const chute = (design, ground, length) =>
|
||||
pave.fordChuteLine(design, length ? { length_m: length } : {}, ground);
|
||||
// 그린 SVG — 띠(돌붙임) · 월류 높이선 점 수
|
||||
const svg = new Node("g");
|
||||
pave.appendFordPavementOverlay(
|
||||
svg,
|
||||
{
|
||||
span_m: 8,
|
||||
depth_m: 0.3,
|
||||
slope_pct: 5,
|
||||
chute: { structure: "돌붙임", thickness_m: 0.3, length_m: 6 },
|
||||
},
|
||||
input.fillDesign,
|
||||
(o) => o,
|
||||
(e) => e,
|
||||
fillGround,
|
||||
);
|
||||
const deck = svg.children.find((k) => k.attrs.class === "b06-chart__ford-deck-plan");
|
||||
const band = svg.children.find((k) => k.attrs.class === "b06-chart__culvert-pitching");
|
||||
|
||||
const out = {
|
||||
fillNone: chute(input.fillDesign, fillGround),
|
||||
fillShort: chute(input.fillDesign, fillGround, 1),
|
||||
fillLong: chute(input.fillDesign, fillGround, 6),
|
||||
dipNone: chute(input.dipDesign, dipGround),
|
||||
dipLong: chute(input.dipDesign, dipGround, 2),
|
||||
fillOverflow: pave.overflowLine(input.fillDesign, 0.3),
|
||||
dipOverflow: pave.overflowLine(input.dipDesign, 0.3),
|
||||
fillEdges: pave.fordEdges(input.fillDesign),
|
||||
dipEdges: pave.fordEdges(input.dipDesign),
|
||||
deckPoints: deck?.attrs.points ?? null,
|
||||
bandPoints: band?.attrs.points ?? null,
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -75,8 +75,10 @@ def test_포장층_노견까지_점선은_노면_위(ts):
|
||||
# 포장층 하나 = 노견 끝(±3)까지 · 윗면 = 설계 노면 · 두께 0.2
|
||||
assert len(ts["pavePoints"]) == 1
|
||||
assert _pts(ts["pavePoints"][0]) == pytest.approx([3, 100.15, -3, 99.85, -3, 99.65, 3, 99.95])
|
||||
# 월류 높이선 = 노면 + 0.4 · 같은 각도
|
||||
assert _pts(ts["planPoints"][0]) == pytest.approx([3, 100.55, -3, 100.25])
|
||||
# 월류 높이선 = 노면 + 0.4 · 같은 각도 · 우(성토) = 성토선 연장과 만난 자리 → 노면 끝(315-2)
|
||||
d = 0.4 / (2.65 / 4 - 0.3 / 6)
|
||||
meet = [-3 + d, 99.85 + 2.65 / 4 * d]
|
||||
assert _pts(ts["planPoints"][0]) == pytest.approx([3, 100.55, *meet, -3, 99.85])
|
||||
assert ts["inflowLeft"] is True # 노면 높은 쪽 = 유입 · 도수로 띠 = 낮은 쪽
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
"""315 물넘이포장 횡단 — 도수로 · 돌붙임 길이(성토) · 월류 높이선 성토 쪽 연장(요구 211-1).
|
||||
|
||||
못박는 것
|
||||
- 315-1 길이 없음 = 성토는 원지반까지(그대로) · 길이 > 성토 사면 = 사면 끝에서 원지반 따라 이어 그 길이
|
||||
(이은 조각까지 한 선 · 옛 흠 = 첫 조각만 그림) · 길이 < 사면 = 사면 따라 그 길이 · 절토(늘인 노면 끝이
|
||||
원지반) = 그대로(길이 없음 = 안 그림 · 길이 = 원지반 따라).
|
||||
- 315-2 월류 높이선 — 절토 쪽 = 노면 끝 위 월류 높이(그대로) · 성토 쪽 = 성토 사면선을 노면 끝 너머 위 ·
|
||||
안쪽으로 점선 연장해 월류 높이선과 만나는 자리에서 끝(같은 점선 양식 한 벌).
|
||||
|
||||
진짜 TS 는 `helper_315_ford_chute_overflow.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
if str(HERE) not in sys.path:
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||||
from test_b06_cross_design_mirror import _dip_ground # noqa: E402
|
||||
|
||||
KW = {
|
||||
"ground_type": "soil",
|
||||
"section_mode": "left_cut",
|
||||
"ditch_side": "left",
|
||||
"paved": True,
|
||||
"tilted": True,
|
||||
"tilt_pct": 5.0,
|
||||
"standard": STANDARD_CROSS_SECTION,
|
||||
}
|
||||
DEPTH = 0.3
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _fill_samples() -> list[dict]:
|
||||
"""좌 = 절토(원지반이 노면 위) · 우(낮은 쪽) = 성토 — 원지반 100 + 0.35·o."""
|
||||
return [
|
||||
{"offset_m": o / 2, "elevation_m": round(100.0 + 0.35 * o / 2, 4), "valid": True}
|
||||
for o in range(-40, 41)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts(tmp_path_factory) -> dict:
|
||||
source = tmp_path_factory.mktemp("s315") / "in.json"
|
||||
payload = {
|
||||
"fillSamples": _fill_samples(),
|
||||
"fillDesign": compute_cross_design(_fill_samples(), 100.4, **KW),
|
||||
"dipSamples": _dip_ground(),
|
||||
"dipDesign": compute_cross_design(_dip_ground(), 100.4, **KW),
|
||||
}
|
||||
source.write_text(json.dumps(payload), encoding="utf-8")
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_315_ford_chute_overflow.cjs"), str(source)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _length(line: list[dict]) -> float:
|
||||
return sum(
|
||||
math.hypot(b["offset"] - a["offset"], b["elevation"] - a["elevation"])
|
||||
for a, b in zip(line, line[1:])
|
||||
)
|
||||
|
||||
|
||||
def _on_ground(p: dict) -> bool:
|
||||
return p["elevation"] == pytest.approx(100.0 + 0.35 * p["offset"], abs=1e-3)
|
||||
|
||||
|
||||
def test_길이_없음_성토는_원지반까지(ts):
|
||||
line = ts["fillNone"]
|
||||
assert line[0]["offset"] == pytest.approx(-2.0) # 낮은 쪽(우) 노견 끝
|
||||
assert _on_ground(line[-1]) and not _on_ground(line[0])
|
||||
assert all(b["elevation"] < a["elevation"] for a, b in zip(line, line[1:]))
|
||||
|
||||
|
||||
def test_성토보다_긴_길이는_원지반_따라_이어_그_길이(ts):
|
||||
slope = _length(ts["fillNone"])
|
||||
line = ts["fillLong"]
|
||||
assert slope < 6
|
||||
assert _length(line) == pytest.approx(6.0, abs=1e-6) # 옛 흠 = 사면 길이에서 멈춤
|
||||
toe = ts["fillNone"][-1]
|
||||
beyond = [p for p in line if p["offset"] < toe["offset"] - 1e-6]
|
||||
assert beyond and all(_on_ground(p) for p in beyond) # 사면 끝 너머 = 원지반 위
|
||||
assert line[: len(ts["fillNone"])] == ts["fillNone"] # 앞은 사면 그대로 · 한 선
|
||||
|
||||
|
||||
def test_짧은_길이는_사면_따라_그_길이(ts):
|
||||
line = ts["fillShort"]
|
||||
assert _length(line) == pytest.approx(1.0, abs=1e-6)
|
||||
assert not any(_on_ground(p) for p in line)
|
||||
|
||||
|
||||
def test_절토_쪽_그대로(ts):
|
||||
assert ts["dipNone"] == [] # 늘인 노면 끝이 원지반 · 길이 없음 = 안 그림
|
||||
line = ts["dipLong"] # 길이 = 늘인 끝에서 원지반 따라 그 길이
|
||||
assert line[0]["offset"] == pytest.approx(ts["dipEdges"]["right"]["offset_m"], abs=1e-6)
|
||||
assert _length(line) == pytest.approx(2.0, abs=1e-6)
|
||||
|
||||
|
||||
def test_그린_띠도_긴_길이(ts):
|
||||
nums = [float(v) for pair in ts["bandPoints"].split() for v in pair.split(",")]
|
||||
offsets = nums[0::2]
|
||||
assert min(offsets) < ts["fillNone"][-1]["offset"] - 1 # 띠가 사면 끝 너머 원지반까지
|
||||
|
||||
|
||||
def test_월류선_절토_쪽은_노면_끝_위(ts):
|
||||
edges = ts["fillEdges"]
|
||||
first = ts["fillOverflow"][0] # 좌 = 절토
|
||||
assert first["offset"] == pytest.approx(edges["left"]["offset_m"])
|
||||
assert first["elevation"] == pytest.approx(edges["left"]["elevation_m"] + DEPTH)
|
||||
dip = ts["dipOverflow"] # 양측 절토 = 두 점 그대로
|
||||
assert len(dip) == 2
|
||||
assert dip[-1]["offset"] == pytest.approx(ts["dipEdges"]["right"]["offset_m"])
|
||||
assert dip[-1]["elevation"] == pytest.approx(ts["dipEdges"]["right"]["elevation_m"] + DEPTH)
|
||||
|
||||
|
||||
def test_월류선_성토_쪽은_성토선_점선_연장과_만남(ts):
|
||||
edges = ts["fillEdges"]
|
||||
left, right = edges["left"], edges["right"]
|
||||
*_, meet, end = ts["fillOverflow"]
|
||||
# 끝 = 우 노면 끝(성토 사면 시작) · 그 앞 교점 = 노면 안쪽
|
||||
assert end == pytest.approx({"offset": right["offset_m"], "elevation": right["elevation_m"]})
|
||||
assert right["offset_m"] < meet["offset"] < left["offset_m"]
|
||||
# 교점은 월류 높이선 위
|
||||
road = (left["elevation_m"] - right["elevation_m"]) / (left["offset_m"] - right["offset_m"])
|
||||
over = right["elevation_m"] + DEPTH + road * (meet["offset"] - right["offset_m"])
|
||||
assert meet["elevation"] == pytest.approx(over, abs=1e-9)
|
||||
# 교점 → 노면 끝 = 성토 사면과 같은 물매(사면선의 연장)
|
||||
slope = ts["fillNone"]
|
||||
fill = (slope[0]["elevation"] - slope[1]["elevation"]) / (
|
||||
slope[0]["offset"] - slope[1]["offset"]
|
||||
)
|
||||
ext = (meet["elevation"] - end["elevation"]) / (meet["offset"] - end["offset"])
|
||||
assert ext == pytest.approx(fill, abs=1e-9)
|
||||
# 그린 점선 = 같은 점(같은 양식 한 벌)
|
||||
assert ts["deckPoints"].split()[-2:] == [
|
||||
f"{meet['offset']},{meet['elevation']}",
|
||||
f"{end['offset']},{end['elevation']}",
|
||||
]
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 316 — 기본값 둘.
|
||||
|
||||
316-1 B06 횡단 「복합경사」(two_stage_slope) 기본 = 꺼짐 · 저장된 고른 값은 그대로.
|
||||
두 짝(파이썬 `compute_cross_design` ↔ TS `computeCrossDesign`)의 같은 기본은
|
||||
`test_b06_cross_design_mirror.py` 「발파암 복합경사 값 없음」 갈래가 실제 대조.
|
||||
316-2 B05 3D 영역 첫 보기 = 원근 · 화면 설정에 기억한 값이 있으면 그 값.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
from B06_Section.B06_Section_Schema import CrossDesignRequest # noqa: E402
|
||||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _samples() -> list[dict]:
|
||||
offsets = [round(-12 + i * 0.5, 3) for i in range(49)]
|
||||
return [{"offset_m": x, "elevation_m": 100.0 - 0.55 * x, "valid": True} for x in offsets]
|
||||
|
||||
|
||||
def _rock(**extra) -> dict:
|
||||
return compute_cross_design(
|
||||
_samples(),
|
||||
99.0,
|
||||
ground_type="blasting_rock",
|
||||
section_mode="left_cut",
|
||||
ditch_side="left",
|
||||
standard=STANDARD_CROSS_SECTION,
|
||||
rock_boundary_offset_m=-1.4,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
def test_engine_default_off():
|
||||
plain, off, on = _rock(), _rock(two_stage_slope=False), _rock(two_stage_slope=True)
|
||||
assert plain["two_stage_slope"] is False
|
||||
assert plain["cut_area_m2"] == off["cut_area_m2"]
|
||||
# 고른 「켬」은 그대로 — 면적이 실제로 달라야 기본값 시험이 뜻이 있음
|
||||
assert on["two_stage_slope"] is True
|
||||
assert on["cut_area_m2"] != off["cut_area_m2"]
|
||||
|
||||
|
||||
def test_request_schema_default_off():
|
||||
field = CrossDesignRequest.model_fields["two_stage_slope"]
|
||||
assert field.default is False
|
||||
|
||||
|
||||
def test_python_stored_reads_default_off():
|
||||
for rel in (
|
||||
"B06_Section/B06_Section_Router_Design.py",
|
||||
"B06_Section/B06_Section_Engine_SpoilFill.py",
|
||||
"B06_Section/B06_Section_Engine_SampleExtend.py",
|
||||
"B05_Profile/B05_Profile_Router_Confirm.py",
|
||||
):
|
||||
src = _read(rel)
|
||||
assert '"two_stage_slope", True' not in src, rel
|
||||
assert '"two_stage_slope", False' in src, rel
|
||||
|
||||
|
||||
def test_ts_defaults_off_and_stored_kept():
|
||||
engine = _read("common_util/common_util_cross_design.ts")
|
||||
assert engine.count("options.twoStageSlope ?? false") == 2
|
||||
assert "twoStageSlope ?? true" not in engine
|
||||
# 카드 토글 — 저장된 고른 값 그대로 · 없으면 꺼짐
|
||||
assert "twoStage: design?.two_stage_slope ?? false," in _read(
|
||||
"B06_Section/B06_Section_UI_Cross_Design.ts"
|
||||
)
|
||||
# 브라우저 재계산 인자 — 세션 선택 → 저장값 → 꺼짐
|
||||
assert "twoStageSlope: choice?.two_stage_slope ?? Boolean(design.two_stage_slope)," in _read(
|
||||
"B06_Section/B06_Section_Cross_Refresh.ts"
|
||||
)
|
||||
assert "two_stage_slope: design.two_stage_slope ?? false," in _read(
|
||||
"B06_Section/B06_Section_UI_Page_Design_Sync.ts"
|
||||
)
|
||||
|
||||
|
||||
def test_3d_first_view_perspective():
|
||||
camera = _read("B05_Profile/B05_Profile_UI_Viewer_Camera.ts")
|
||||
assert 'let kind: ProjectionKind = "perspective";' in camera
|
||||
state = _read("A00_Common/b_page_state.ts")
|
||||
assert re.search(r'"b05-3d-projection": \{ bucket: "pref", scope: "global" \}', state)
|
||||
|
||||
|
||||
def test_3d_remembered_projection_wins():
|
||||
src = _read("B05_Profile/B05_Profile_UI_Panel_View.ts")
|
||||
# 기억 = ortho 일 때만 직교 · 그 밖(없음 · 손상)은 원근
|
||||
assert re.search(
|
||||
r'const initialProjection = readStateRaw\("b05-3d-projection"\) === "ortho"\s*\?\s*"ortho"\s*:\s*"perspective";',
|
||||
src,
|
||||
)
|
||||
assert 'writeStateRaw("b05-3d-projection", kind);' in src
|
||||
assert 'if (initialProjection === "ortho") callbacks.onProjection("ortho");' in src
|
||||
# 단추 강조도 처음 값 따라
|
||||
assert 'element.classList.toggle("is-active", kind === initialProjection);' in src
|
||||
@@ -95,6 +95,18 @@ CASES = [
|
||||
"two_stage_slope": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
# 316-1 — 복합경사 값 없음 = 기본 꺼짐(두 짝 같은 기본)
|
||||
"name": "발파암 복합경사 값 없음",
|
||||
"samples": _ground(-0.55),
|
||||
"design_elevation_m": 99.0,
|
||||
"options": {
|
||||
"ground_type": "blasting_rock",
|
||||
"section_mode": "left_cut",
|
||||
"ditch_side": "left",
|
||||
"rock_boundary_offset_m": -1.4,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "포장 + 측구 없음",
|
||||
"samples": _ground(0.12),
|
||||
|
||||
Reference in New Issue
Block a user