11 Commits
Author SHA1 Message Date
eomsangdonandClaude Opus 5.5 bfa2b3a97f fix(평면): 평면 비탈 끝 · B07 계획평면도 = 벽 측점 횡단 최종 모양이 땅과 만나는 점 (PLAN 578-2 · 요구 438 · 439)
- B06 STRUCTURE_ROW_KEYS · 서버 _AREA_KEYS 에 slope_end_left_m · slope_end_right_m — 벽 트림 측점만 · 트림 사면 → 단마다 벽 앞 → 사이 성토부선 윤곽이 처음 땅에 닿는 오프셋
- 평면 발자국 finalSlopeEnd · B07 final_slope_end(짝) — 얹은 값 먼저 · 없으면 설계선 끝(구조물 없는 측점 그대로)
- 시험 test_578_2_plan_slope_end 새로 · test_346_2 거울에 벽 측점 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:13:39 +09:00
eomsangdonandClaude Opus 5.5 8fcc4c9fbe knowledge(요구): 요구 439 원문 기록 — 578 지형 범위 넓힘 · 평면 비탈 끝 = 횡단 최종 모양 진행
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:02:02 +09:00
eomsangdonandClaude Opus 5.5 a791f7f988 knowledge(요구): 요구 438 원문 기록 — 소단 비탈 끝 화면 ≠ 저장 고침 방향 · 평면 비탈 끝 구조물
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:00:47 +09:00
eomsangdonandClaude Opus 5.5 98023a717e chore(계획): PLAN 577 이관 — 소단 = 옹벽처럼 측점 얹기 한 벌 · 사본 걷음 · 3D 까지(577-2 · 요구 437)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:05:00 +09:00
eomsangdonandClaude Opus 5.5 403722e687 refactor(B05·B06): 소단 = 옹벽처럼 측점에 제원 얹음 — 형상 얹기 한 벌 · 세션 사본 berm 걷음 (PLAN 577-2 · 요구 434 · 437)
- 얹기: attachShapes · attach_shapes 에 소단(section.berm · 덮는 소단 없음 = null) · SHAPE_KEYS 에 berm · TS attachBermSpecs ↔ 파이썬 attach_berm_specs · berm_spans_from 짝
- 읽기: 저장 노선 다시 셈(브라우저 · Node cross.refresh) = 얹힌 section.berm(칸 없음 = 저장분 design.berm) · 노선 초안 · 서버 Node 묶음 cross = crossWithShapes 의 얹힌 제원(bermAt · crossWithBerm 걷음)
- 계단은 계산 안쪽 그대로 · [저장] 길(구조물 정본 → cross.berm_spans) 그대로
- 지움: 시설 줄 syncBerm · putBerm(C4.copy) · writeBermSpans · 세션 칸 berm · bermRequest · design_shared bermAt
- 시험: test_272_berm_source 측점 얹기 거울로 · test_323_4 소단 얹음 · 477 · 511-A · 472 떠남 · 365 · 272 고름 헬퍼가 사본 대신 얹힌 소단 · design_pipeline_page 700 줄 → 699(주석 한 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:33:06 +09:00
eomsangdonandClaude Opus 5.5 189474a507 knowledge(요구): 요구 437 원문 기록 — 577 가 = 소단도 옹벽처럼 측점에 얹음 · 3D 까지
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:14:26 +09:00
eomsangdonandClaude Opus 5.5 31e1debcf6 chore(계획): 요구 436 분석 2차 — 소단 대 옹벽 · 흙막이 길 비교 · 권함 가(측점에 얹음)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:59:33 +09:00
eomsangdonandClaude Opus 5.5 0081353dd0 knowledge(요구): 요구 436 원문 기록 — 577 사면안정 그룹끼리 비교 · 검증_랩탑메인 초기화
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:54:11 +09:00
eomsangdonandClaude Opus 5.5 95dd1f0253 chore(계획): PLAN 575 이관 — 노선 손 고침 [저장] 종단 = 옛 종단 길이 비례(575-2 · 요구 435)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:40:54 +09:00
eomsangdon 08a1bef47b Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1 2026-10-29 09:40:11 +09:00
eomsangdonandClaude Opus 5.5 22ccd1085f chore(계획): 요구 434 분석 — 사면안정 구조물군 길 · 소단만 사본 = 옛 흔적 · PLAN 577
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:31:25 +09:00
44 changed files with 649 additions and 324 deletions
-3
View File
@@ -235,9 +235,6 @@ export const STATE_REGISTRY = {
"design-stations": { bucket: "result", scope: "project" },
"design-profile": { bucket: "result", scope: "project" },
"design-footprint": { bucket: "result", scope: "project" },
/** 소단 사본 — 구조물(소단)에서 셈한 구간 목록(`design_facility_lane` 만 씀 · 고침은 `structures` 칸).
* 다시 셈에 실어 보냄 · 안 실으면 계획선을 고치는 순간 계단이 사라짐(3-9 · 477-4). */
berm: { bucket: "result", scope: "route" },
} as const satisfies Record<string, StateEntry>;
export type StateName = keyof typeof STATE_REGISTRY;
+1 -7
View File
@@ -142,18 +142,12 @@ export interface CalcRequest {
pick: (results: Record<string, unknown>) => unknown;
}
/** F7 구조물 면적 · C4 소단 구간 — 등록부 `node:` 입력 그대로. */
/** F7 구조물 면적 — 등록부 `node:` 입력 그대로. */
export const areasRequest = (rows: unknown[]): CalcRequest => ({
run: ["structure.layouts"],
inputs: { detail: { cross_sections: rows } },
pick: (results) => (results["structure.layouts"] as { areas: unknown }).areas,
});
export const bermRequest = (structures: unknown[]): CalcRequest => ({
run: ["cross.berm_spans"],
inputs: { berm: { structures, chainages: [] } },
pick: (results) => (results["cross.berm_spans"] as { spans: unknown }).spans,
});
export const trialRequest = (id: string, args: unknown[]): CalcRequest => ({
run: [id],
inputs: { args: { [id]: args } },
+1 -43
View File
@@ -8,20 +8,10 @@
* 입력 = 관 알림(배수유역도 · B06 관 목록) · 구조물(보기 고침 · 저장분) · 종류 표 · 측점 간격 · 추가 측점(가게)
* 관 id 장부 = 관 고정 id 가 쥔 세션 id 를 그 자리로(411-B) · 헛 id 정리(411-E) — 어느 보기에서 고쳐도
* 계획고 편집 옮김(사라진 측점 → 새 측점 · 213-2) = 사용자 고침만 · 노선 따라감(`follow`)은 안 옮김(계산 묶음 몫 · 349-0d)
* 소단 사본(세션 `berm`) = 구조물 소단 구간 — 쓰는 자리 여기 하나(B05 만 열어도 · 같으면 안 씀 · 쓰면 cross 한 번 · 467)
* ========================================================================== */
import { useDesignStore } from "./design_store";
import { pageRouteId } from "./design_page_route";
import { readState } from "../b_page_state";
import {
bermSpansFromStructures,
writeBermSpans,
type BermSpan,
} from "../../B06_Section/B06_Section_UI_Page_Persist";
import { pipeIdAt, placePipeIds, prunePipeIds, withPipeIds } from "./design_pipe_ids";
import { calcOnServerNow } from "./design_calc_sites";
import { bermRequest, calcChecked, calcOnServerOnly } from "./design_calc_server";
import { plannedIrregular } from "./design_structure_status";
import { planStations } from "../../common_util/common_util_structure_station";
import { followPipeReports } from "../../B05_Profile/logic/B05_Profile_Pipeline_Follow";
@@ -99,7 +89,7 @@ export function createFacilityLane(
let types: StructureType[] = [];
let stored: StructureInstance[] | null = null;
let structures: StructureInstance[] = [];
let structuresKnown = false; // 구조물을 받기 전 = 소단 사본 안 씀(빈 목록으로 저장분 소단을 지우지 않게)
let structuresKnown = false; // 구조물을 받기 전 = 측점 계획 모름(빈 목록으로 저장 시설 측점을 지우지 않게)
/** 마지막 관 알림 — 노선 따라감 · 관 초안이 바뀌면 이것을 초안 측점으로 다시 맞춤. */
let reported: LanePipe[] | null = null;
let pipes: LanePipe[] = [];
@@ -152,36 +142,6 @@ export function createFacilityLane(
const marks = (): StructureInstance[] => [...structures, ...pipesToStructureMarks(pipes)];
/** 소단 사본 = 지금 구조물의 소단 구간(467 · 사본 없는 동안 다시 셈 = 저장분 `design.berm`) · 같으면 안 씀 ·
* 쓰면 저장 노선 cross 한 번 더(같은 값 측점은 다시 셈 기억이 건너뜀 · 묶음은 늦게 불러옴 · 순환 없음). */
function syncBerm(): void {
const routeId = pageRouteId(projectId);
if (!structuresKnown || routeId === null) return;
// 계산 자리 quantity = server 면 잠잠 뒤 서버 값만(511-A) · 아니면 브라우저 셈
if (calcOnServerNow("quantity")) {
void calcOnServerOnly<BermSpan[]>("quantity", "C4.copy", bermRequest(structures)).then(
(next) => next && putBerm(routeId, next),
);
return;
}
putBerm(
routeId,
calcChecked("quantity", "cross.berm_spans", bermSpansFromStructures, [structures]),
);
}
function putBerm(routeId: number, next: BermSpan[]): void {
const copy = readState<BermSpan[]>("berm", projectId, routeId);
if (Array.isArray(copy) && JSON.stringify(copy) === JSON.stringify(next)) return;
writeBermSpans(projectId, routeId, next);
void Promise.all([import("./design_pipeline_cross"), import("./design_pipeline_page")])
.then(([cross, page]) => {
cross.forceCross();
page.pipelineCross(projectId);
})
.catch(() => undefined);
}
/** 측점 계획 입력이 다 옴 — 모름(관 알림 전 · 구조물 받기 전)을 빈 목록으로 셈하면 종단도가 저장 시설 측점
* (관 등)을 뺀 선으로 횡단을 다시 셈 · 유토 배지가 잠깐 틀림(473 · 요구 272). */
const known = (): boolean => types.length > 0 && reported !== null && structuresKnown;
@@ -234,14 +194,12 @@ export function createFacilityLane(
}
if (next.pipes) reported = [...next.pipes];
refresh(carryEdits);
if (next.structures) run(syncBerm);
},
follow() {
const drafted = store.get("structures");
structures = drafted ?? stored ?? [];
structuresKnown ||= drafted !== null || stored !== null;
refresh(false);
run(syncBerm);
},
setIntervalM(next) {
if (next > 0 && next !== intervalM) {
+2 -3
View File
@@ -338,14 +338,13 @@ function createPagePipeline(
groundPvi: groundPviAt, // 계산 자리 「서버」 = 서버 Node(511-B B-3)
}),
profile: profileAt,
// 단면 유형 = 새 지형 상단측 · 소단 = 지금 구조물(365-5) · 저장 노선 = 다시 셈 창구 한 벌(365-4) · 엔진은
// 창구 출력을 앞 결과로 안 이어 씀(초안 = 새로고침)
// 단면 유형 = 상단측 · 소단 = 얹힌 제원(577-2) · 저장 노선 = 다시 셈 창구(365-4) · 엔진은 창구 출력 안 이어 씀
cross: (ctx) => {
const stations = ctx.out.ground?.stations ?? [];
const status = statusOf(ctx.inputs.pipes, ctx.inputs.structures);
const engine = () =>
crossStage.stage({
...contextCross(ctx, status, options.bermAt?.(ctx.inputs.structures) ?? null),
...contextCross(ctx, status),
prev: ctx.prev && savedOut.has(ctx.prev) ? null : ctx.prev,
});
// 계산 자리 cross = server — 엔진 길도 서버 Node 단계(같은 TS · 511-B B-4) · 못 받음 = 브라우저
@@ -14,7 +14,6 @@ import type {
AlignmentBase,
AlignmentEdits,
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
import type { BermAt } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
import type { DesignStoreValues } from "./design_store";
@@ -61,8 +60,6 @@ export interface PagePipelineOptions {
stationKeys?: (routeId: number, detail: SectionDetailResponse) => string[];
/** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */
onUnstable?: () => void;
/** 소단 구간(지금 구조물 목록 → 누가거리마다 제원) — 초안 cross 가 따라간 구조물로 소단을 셈(365-5). */
bermAt?: (structures: unknown) => BermAt;
/** 저장 노선 cross = 다시 셈 창구 한 벌(365-4 · 측점이 상세와 다르면 null = 엔진) · 주면 종단도는 따로 안 셈. */
savedCross?: (
d: SectionDetailResponse,
+1 -11
View File
@@ -59,8 +59,7 @@ import {
refreshCrossDesigns,
type CrossRefreshInput,
} from "../../B06_Section/B06_Section_Cross_Refresh";
import { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist";
import { areasRequest, bermRequest, calcChecked } from "./design_calc_server";
import { areasRequest, calcChecked } from "./design_calc_server";
import { registeredKeys, stationIntervalOf } from "../../B06_Section/B06_Section_Station_Keys";
import { detailCross } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
import type { CrossOutput, PipelineStation } from "./logic/design_pipeline_types";
@@ -255,15 +254,6 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
each((hooks) => hooks.onFacilitiesFollowed?.(far));
},
onUnstable: () => showToast(L("Design_Pipeline_Unstable"), "warning"),
// 초안 소단 = 지금 구조물(따라간 자리)의 소단 구간(365-5 · 저장 노선은 다시 셈 창구가 세션 사본으로)
bermAt: (structures) => {
const list = Array.isArray(structures) ? structures : [];
// 계산 자리 quantity = server 면 잠잠 뒤 서버 대조만(초안 미리보기 · 511-A)
const spans = calcChecked("quantity", "cross.berm_spans", bermSpansFromStructures, [list], {
request: bermRequest(list),
});
return (chainageM) => bermSpecAt(spans, chainageM);
},
// 저장 노선 횡단 = 다시 셈 창구 한 벌(365-4) — 카드 · 유토곡선 · [저장] 패치가 보는 상세 설계를 늘 고치고 그
// 값을 묶음 출력으로(발자국 · 평면 횡단 판) · 측점이 상세와 다르면(갈아끼움 전) 묶음 엔진(439)
savedCross: (detail, routeId, stations) =>
@@ -18,7 +18,6 @@
* ========================================================================== */
import { readFileSync, statSync } from "node:fs";
import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans";
import type { FollowingStructure } from "@util/common_util_route_follow";
import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog";
import type { PlanPipe, PlanStructure, PlanType } from "@util/common_util_structure_station";
@@ -189,9 +188,7 @@ async function runStages(
ctx.inputs.structures ?? savedStructures,
types,
);
const list = Array.isArray(ctx.inputs.structures) ? ctx.inputs.structures : [];
const spans = bermSpansFromStructures(list);
return crossStage.stage(contextCross(ctx, status, (at) => bermSpecAt(spans, at)));
return crossStage.stage(contextCross(ctx, status));
},
};
if (stages.until === "follow") return followOnly(table, inputs);
@@ -10,7 +10,6 @@ import {
} from "../../../common_util/common_util_route_follow";
import {
crossWithContext,
type BermAt,
type CrossInput,
type ShapeStatus,
} from "../../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
@@ -45,11 +44,10 @@ export function placedFollowInputs(
};
}
/** cross 단계 입력 = 시설 맥락(새 자리 그림) · 단면 유형 기본값(상단측) · 소단(지금 구조물) 얹음(365-5 · 457). */
/** cross 단계 입력 = 시설 맥락(새 자리 그림 · 소단 = 얹힌 제원 · 577-2) · 단면 유형 기본값(상단측) 얹음(365-5 · 457). */
export const contextCross = (
ctx: StageContext<"cross">,
status: ShapeStatus | null,
bermAt: BermAt | null,
): StageContext<"cross"> => ({
...ctx,
inputs: {
@@ -58,7 +56,6 @@ export const contextCross = (
ctx.inputs.cross as CrossInput | null,
ctx.out.ground?.stations ?? [],
status,
bermAt,
),
},
});
@@ -8,7 +8,8 @@
* `resources/tester/test_346_2_pipeline_cross_page.py`.
* crossWithUphill cross 단계 입력에 단면 유형 기본값(상단측)을 얹음 — 서버 노선 저장이 단면 유형 ·
* 측구 방향을 새 지형으로 정하는 것과 같은 규칙(넘기는 칸 = 부르는 쪽 입력).
* crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457).
* crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강 · 소단) = 지금 현황을 새 측점에 다시 얹은 그림에서(457 ·
* 소단 = 얹힌 `berm` 제원 + 세션 쪽 깊이 · 577-2).
* crossAtSeat 자리 칸(암 경계 · 2단 경사 · 손 상단측 · `seats`)은 측점이 그 자리 그대로일 때만 · 새 자리 =
* 기본값(요구 275-3 · 판정 `sameSeat` · 서버 노선 저장 · Node 넘김과 같은 규칙).
* draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 그림 자료 · 카드 이름 = 지금
@@ -113,38 +114,6 @@ export function crossWithUphill(
return { ...input, byKey };
}
/** 소단 한 측점 제원(세션 `berm` 꼴 · 구조물 목록에서 폄). */
export type BermAt = (
chainageM: number,
) => { width_m: number; interval_m: number; slope_deg: number; depth_m?: number } | null;
/** 측점마다 소단 = 지금 구조물(초안 따라간 자리)의 소단 구간 · 쪽 깊이는 세션 고침(`bermDepthLeftM` ·
* `bermDepthRightM` · 측점 열쇠) — 노선 초안 동안 소단 고침도 묶음으로(365-5). 구간 밖 측점은 소단 없음. */
export function crossWithBerm(
input: CrossInput | null,
stations: ReadonlyArray<PipelineStation>,
bermAt: BermAt | null,
): CrossInput | null {
if (!input || !bermAt) return input;
const byKey: CrossInput["byKey"] = { ...input.byKey };
for (const station of stations) {
const { bermDepthLeftM, bermDepthRightM, ...rest } = input.byKey[station.key] ?? {};
const at = bermAt(station.chainageM);
byKey[station.key] = {
...rest,
berm: at && {
widthM: at.width_m,
intervalM: at.interval_m,
slopeDeg: at.slope_deg,
depthM: at.depth_m,
depthLeftM: (bermDepthLeftM as number | undefined) ?? null,
depthRightM: (bermDepthRightM as number | undefined) ?? null,
},
};
}
return { ...input, byKey };
}
const kindOf = (station: PipelineStation): SectionStation["kind"] =>
station.key === "end:BP"
? "bp"
@@ -269,7 +238,8 @@ function shapeContext(section: CrossSection): Record<string, unknown> {
};
}
/** 측점마다 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457). */
/** 측점마다 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강 · 소단) = 지금 현황을 새 측점에 다시 얹은 그림에서
* (457) · 소단 쪽 깊이 = 세션 고침(`bermDepthLeftM` · `bermDepthRightM` · 측점 열쇠 · 365-5) · 구간 밖 = 소단 없음. */
export function crossWithShapes(
input: CrossInput | null,
stations: ReadonlyArray<PipelineStation>,
@@ -285,23 +255,31 @@ export function crossWithShapes(
);
const byKey: CrossInput["byKey"] = { ...input.byKey };
stations.forEach((station, i) => {
byKey[station.key] = { ...byKey[station.key], ...shapeContext(sections[i]) };
const { bermDepthLeftM, bermDepthRightM, ...rest } = byKey[station.key] ?? {};
const at = sections[i].berm;
byKey[station.key] = {
...rest,
...shapeContext(sections[i]),
berm: at && {
widthM: at.width_m,
intervalM: at.interval_m,
slopeDeg: at.slope_deg,
depthM: at.depth_m,
depthLeftM: (bermDepthLeftM as number | undefined) ?? null,
depthRightM: (bermDepthRightM as number | undefined) ?? null,
},
};
});
return { ...input, byKey };
}
/** cross 단계 입력 = 시설 맥락(새 자리 그림) · 단면 유형 기본값(상단측) · 소단(지금 구조물) 얹음 — 페이지 묶음 cross. */
/** cross 단계 입력 = 시설 맥락(새 자리 그림 · 소단 포함) · 단면 유형 기본값(상단측) 얹음 — 페이지 묶음 cross. */
export const crossWithContext = (
input: CrossInput | null,
stations: ReadonlyArray<PipelineStation>,
status: ShapeStatus | null,
bermAt: BermAt | null,
): CrossInput | null =>
crossWithBerm(
crossWithUphill(crossWithShapes(crossAtSeat(input, stations), stations, status), stations),
stations,
bermAt,
);
crossWithUphill(crossWithShapes(crossAtSeat(input, stations), stations, status), stations);
/** 묶음 출력 → 상세. 저장 상세의 종단 머리(이름)는 그대로 · 측점 · 지반 · 횡단 · 계획선 표본은 새 노선
* (십자선 대체 높이도 초안 종단 · 413) · 그림 자료 · 카드 이름 = `status`(지금 현황)를 새 측점에 다시 얹음(457). */
+22 -5
View File
@@ -3,8 +3,8 @@
* 평면 발자국 — 측점 횡단 설계 결과를 평면 선으로(도로 끝 좌 · 우 · 비탈 끝 깎기 · 쌓기 · 눈금) (346-2)
*
* ⚠⚠ 짝: `B07_DesignDetail/B07_DesignDetail_Engine_Cad_PlanSlope.py`
* (`route_frame` · `route_normal` · `side_is_cut` · `slope_end` · `slope_pieces` 와 그 아래
* 단조 3차 보간 · 촘촘히 · 접힘 버림 · 353-1 · 모서리 둥글림 · 364-3) — 한쪽만 고치면
* (`route_frame` · `route_normal` · `side_is_cut` · `slope_end` · `final_slope_end` ·
* `slope_pieces` 와 그 아래 단조 3차 보간 · 촘촘히 · 접힘 버림 · 353-1 · 모서리 둥글림 · 364-3) — 한쪽만 고치면
* 종단 · 횡단 편집 중 평면과 B07 계획평면도가 갈린다.
* `footprintRecord` = `B07_DesignDetail_Router_Support_Plan.slope_records` 의 측점 한 줄.
* 거울 시험: `resources/tester/test_346_2_plan_footprint_mirror.py`
@@ -35,6 +35,9 @@ export interface FootprintStation {
right?: FootprintPoint | null;
} | null;
design_line?: FootprintPoint[] | null;
/** 구조물 처리 뒤 비탈 끝(B06 `STRUCTURE_ROW_KEYS` · 벽 측점만 · 578-2). */
slope_end_left_m?: number | null;
slope_end_right_m?: number | null;
} | null;
samples: FootprintPoint[];
}
@@ -156,6 +159,20 @@ export function slopeEnd(
return end;
}
/** 구조물 처리 뒤 비탈 끝 — 벽 측점은 B06 이 얹은 `slope_end_<쪽>_m`(578-2) · 없으면 설계선 끝.
* 짝: `final_slope_end`. */
export function finalSlopeEnd(
design: NonNullable<FootprintStation["design"]>,
samples: FootprintPoint[],
edge: number,
side: Side,
): number {
const stored = design[`slope_end_${side}_m`];
return typeof stored === "number"
? stored
: slopeEnd(design.design_line ?? [], samples, edge, side);
}
/** 측점 한 자리 → `slope_pieces` 줄 · 도로 끝이 없으면 null. 짝: `slope_records` 한 줄. */
export function footprintRecord(station: FootprintStation): FootprintRecord | null {
const edges = station.design?.road_edges;
@@ -163,14 +180,14 @@ export function footprintRecord(station: FootprintStation): FootprintRecord | nu
const right = Number(edges?.right?.offset_m);
if (edges?.left?.offset_m == null || edges?.right?.offset_m == null) return null;
if (!Number.isFinite(left) || !Number.isFinite(right)) return null;
const line = station.design?.design_line ?? [];
const design = station.design ?? {};
const samples = station.samples ?? [];
const record: FootprintRecord = {
chainage_m: station.chainage_m,
left_edge_m: left,
left_end_m: slopeEnd(line, samples, left, "left"),
left_end_m: finalSlopeEnd(design, samples, left, "left"),
right_edge_m: right,
right_end_m: slopeEnd(line, samples, right, "right"),
right_end_m: finalSlopeEnd(design, samples, right, "right"),
};
for (const side of SIDES) {
const elevation = edges[side]?.elevation_m;
+3
View File
@@ -62,6 +62,7 @@ import type { FordPavementSpec } from "./logic/B06_Section_Cross_Ford_Pavement";
import type { RevetmentSpec } from "./logic/B06_Section_Cross_Revetment_Geom";
import type { PavementEntry } from "@util/common_util_structure_pavement";
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
import type { BermSessionSpec } from "@util/common_util_berm_spans";
export type { FordPavementSpec, RevetmentSpec };
export interface SectionContextResponse {
@@ -343,6 +344,8 @@ export interface CrossSection extends SectionStation {
pavement?: PavementEntry[];
/** 맹암거 — 기준 측점(288-1). 높이 · 경사 · 측구 없음 · 노면 기울임 · 돌 무늬 형상. */
underdrain?: UnderdrainSpec;
/** 소단 — 덮는 구간 제원(577-2 · null = 얹었는데 없음 · 칸 없음 = 아직 안 얹음 → 저장분 `design.berm`). */
berm?: BermSessionSpec | null;
/** 빈칸 경고(323-4 · 읽기 칸 · 저장 안 함) — 「종류 칸」(형상 필수 칸이 빈 관 · 구조물 · 측점 계획 자리). */
blank?: string[];
}
+2 -7
View File
@@ -27,8 +27,6 @@
import type { StandardCrossSectionSpec } from "@util/common_util_cross_design";
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
import { readCutSlopeSession, readRockBoundarySession } from "./B06_Section_UI_Page_Persist";
import type { BermSpan } from "./B06_Section_UI_Page_Persist";
import { readState } from "../A00_Common/b_page_state";
import { USER_TOUCHED_KEYS } from "./logic/B06_Section_Design_Keys";
import { savedDesigns } from "./B06_Section_Section_Canon";
import {
@@ -228,17 +226,14 @@ function gatherRefresh(input: CrossRefreshInput): RefreshInputs | null {
const standard = effectiveStandardCross(projectId) as StandardCrossSectionSpec | null;
if (!standard) return null; // 컨텍스트를 아직 못 받음 — 받은 뒤 다시 셈
const { routeId } = input;
// 소단 사본 없음(시설 줄이 구조물을 받기 전) = 저장분 `design.berm`(파이썬 `load_berm_spans` 못 읽음 →
// 저장분 · `stored_berm` 짝 · 467)
const bermCopy = readState<BermSpan[]>("berm", projectId, routeId);
return {
alignment,
standard,
rockOffsets: [...readRockOffsets(projectId, routeId)],
rockDefault: readRockBoundaryDefault(projectId),
// 측점별 암 절토 경사 · 소단 · 카드 선택 · 노견 확장 = 세션에만 있는 값 — **계산 전에** 실음(2026-09-07 · 3-9)
// 측점별 암 절토 경사 · 소단 깊이 · 카드 선택 · 노견 확장 = 세션에만 있는 값 — **계산 전에** 실음(2026-09-07 ·
// 3-9) · 소단 제원 = 측점에 얹힌 `section.berm`(577-2)
cutSlopes: [...readCutSlopeRatios(projectId, routeId)],
bermSpans: Array.isArray(bermCopy) ? bermCopy : null,
bermDepths: [...readBermDepths(projectId, routeId)],
choices: [...crossDesignChoices(projectId, routeId)],
shoulderExtends: [...readShoulderExtends(projectId, routeId)],
+18 -3
View File
@@ -1,7 +1,7 @@
"""형상 얹기 한 벌(323-4) — 측점(횡단 dict) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료.
구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 차례대로 다시 얹음 — 관 세트 → 벽 → 포장 → 맹암거 →
빈칸 경고. 고르기 = 표 형상(322-1). 서버 상세 읽기(`_read_section_detail`) · 재생성 응답이 이것 하나를 씀.
소단 → 빈칸 경고. 고르기 = 표 형상(322-1). 서버 상세 읽기(`_read_section_detail`) · 재생성 응답이 이것 하나를 씀.
셈 = 늘 떠 있는 Node `facility.attach`(화면과 같은 TS · 511-B B-4) ·
파이썬 몫(`_attach_python`)은 Node 를 못 부를 때만.
TS 짝 = `common_util/common_util_structure_attach.ts` `attachShapes`(화면 = 초안 ?? 저장분 현황) ·
@@ -18,7 +18,12 @@ from typing import Any
from B05_Profile.B05_Profile_Structures_Repository import load_structures
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_specs, culvert_sets_from
from B06_Section.B06_Section_Engine_Structures_Wall import attach_wall_specs, wall_specs_from
from B06_Section.B06_Section_Engine_Structures_Wall import (
attach_berm_specs,
attach_wall_specs,
berm_spans_from,
wall_specs_from,
)
from B06_Section.B06_Section_Engine_Tilt import attach_underdrain_specs, underdrains_from
from common_util.common_util_structure_catalog import catalog_row, field_name, type_ids_with
from common_util.common_util_structure_pavement import (
@@ -35,7 +40,16 @@ from common_util.common_util_structure_station import (
logger = logging.getLogger(__name__)
#: 측점 그림 자료 열쇠 — TS `common_util_structure_shape_keys.SHAPE_KEYS` 짝.
SHAPE_KEYS = ("culvert", "ford", "box", "ford_pavement", "revetment", "pavement", "underdrain")
SHAPE_KEYS = (
"culvert",
"ford",
"box",
"ford_pavement",
"revetment",
"pavement",
"underdrain",
"berm",
)
def _blank_labels(type_id: str, options: dict | None, types: dict[str, Any]) -> list[str]:
@@ -128,6 +142,7 @@ def _attach_python(
pavement_specs_from([s.model_dump() for s in structures], type_ids_with(shape="pavement")),
)
attach_underdrain_specs(sections, underdrains_from(structures))
attach_berm_specs(sections, berm_spans_from(structures))
_attach_blanks(sections, pipes, structures)
return misses
@@ -110,11 +110,10 @@ def wall_specs_from(structures: list[Any]) -> list[dict[str, Any]]:
return found
def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None:
"""구조물 정본의 소단 구간 `[(시작, 끝, 제원)]` — [저장] 서버 다시 셈의 소단 원천(194-1).
def berm_spans_from(structures: list[Any]) -> list[tuple[float, float, dict[str, float]]]:
"""구조물 목록 → 소단 구간 `[(시작, 끝, 제원)]`(형상 얹기 한 벌 577-2 · [저장] 194-1).
짝: TS `bermSpansFromStructures`(같은 규칙 — 폭 0 = 없음 · 깊이 빈 값 = 기본 1.0 · 0 미만 = 0).
못 읽으면 None(부르는 쪽이 저장분 `design.berm` 으로 물러섬).
"""
from common_util.common_util_cross_berm import (
BERM_DEFAULT_DEPTH_M,
@@ -130,35 +129,41 @@ def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, fl
return fallback
return value if math.isfinite(value) and value >= 0 else fallback
spans: list[tuple[float, float, dict[str, float]]] = []
for item in structures:
if (catalog_row(item.type_id) or {}).get("shape") != "berm": # 표 형상(323-4)
continue
anchor = item.chainage_m if item.chainage_m is not None else item.start_m
start = item.start_m if item.start_m is not None else anchor
end = item.end_m if item.end_m is not None else anchor
if start is None or end is None:
continue
options = item.options or {}
width = number(options, "width_m", BERM_DEFAULT_WIDTH_M)
if width <= 0:
continue
raw = options.get("depth_m")
try:
depth = BERM_DEFAULT_DEPTH_M if raw in (None, "") else max(float(raw), 0.0)
except (TypeError, ValueError):
depth = BERM_DEFAULT_DEPTH_M
if not math.isfinite(depth):
depth = BERM_DEFAULT_DEPTH_M
spec = {
"width_m": width,
"interval_m": number(options, "interval_m", BERM_DEFAULT_INTERVAL_M),
"slope_deg": number(options, "slope_deg", BERM_DEFAULT_SLOPE_DEG),
"depth_m": depth,
}
spans.append((float(min(start, end)), float(max(start, end)), spec))
return spans
def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None:
"""구조물 정본의 소단 구간 — [저장] 서버 다시 셈의 소단 원천(194-1) · 못 읽으면 None(부르는 쪽이
저장분 `design.berm` 으로 물러섬)."""
try:
spans: list[tuple[float, float, dict[str, float]]] = []
for item in load_structures(str(project_root))[1]:
if (catalog_row(item.type_id) or {}).get("shape") != "berm": # 표 형상(323-4)
continue
anchor = item.chainage_m if item.chainage_m is not None else item.start_m
start = item.start_m if item.start_m is not None else anchor
end = item.end_m if item.end_m is not None else anchor
if start is None or end is None:
continue
options = item.options or {}
width = number(options, "width_m", BERM_DEFAULT_WIDTH_M)
if width <= 0:
continue
raw = options.get("depth_m")
try:
depth = BERM_DEFAULT_DEPTH_M if raw in (None, "") else max(float(raw), 0.0)
except (TypeError, ValueError):
depth = BERM_DEFAULT_DEPTH_M
if not math.isfinite(depth):
depth = BERM_DEFAULT_DEPTH_M
spec = {
"width_m": width,
"interval_m": number(options, "interval_m", BERM_DEFAULT_INTERVAL_M),
"slope_deg": number(options, "slope_deg", BERM_DEFAULT_SLOPE_DEG),
"depth_m": depth,
}
spans.append((float(min(start, end)), float(max(start, end)), spec))
return spans
return berm_spans_from(load_structures(str(project_root))[1])
except Exception: # noqa: BLE001 — 정본을 못 읽으면 저장분으로 물러섬
logger.exception("B06 소단 구간을 읽지 못했습니다 (저장분 소단을 씀)")
return None
@@ -174,6 +179,24 @@ def berm_span_at(
return None
def attach_berm_specs(
cross_sections: list[dict[str, Any]], spans: list[tuple[float, float, dict[str, float]]]
) -> int:
"""측점마다 소단 제원을 얹음(TS `attachBermSpecs` 짝 · 577-2) — 덮는 소단 없음 = None
(얹었음 표시). 얹은 측점 수."""
attached = 0
for section in cross_sections:
chainage = section.get("chainage_m")
if isinstance(chainage, bool) or not isinstance(chainage, (int, float)):
continue
if not math.isfinite(chainage):
continue
spec = berm_span_at(spans, float(chainage))
section["berm"] = dict(spec) if spec else None
attached += 1 if spec else 0
return attached
def attach_wall_specs(cross_sections: list[dict[str, Any]], walls: list[dict[str, Any]]) -> int:
"""구간 안 측점에 벽 제원을 얹는다(TS `attachWallSpecs` 짝). 얹은 개수를 돌려준다."""
if not walls:
@@ -73,6 +73,8 @@ _AREA_KEYS = (
"box_ground_slope_pct",
"box_length_m",
"ford_pipe_length_m",
"slope_end_left_m",
"slope_end_right_m",
)
+3 -15
View File
@@ -16,7 +16,7 @@ import {
} from "./B06_Section_Api_Fetch";
import type { DesignViewSave } from "../A00_Common/design/design_shared";
import { buildCrossPatches, type CrossPatchSources } from "./logic/B06_Section_UI_Page_Patches";
import { clearState, readByKey, writeByKey, writeState } from "../A00_Common/b_page_state";
import { clearState, readByKey, writeByKey } from "../A00_Common/b_page_state";
import { kindChangedChainages } from "./B06_Section_Api_Culvert_Options";
import {
crossDesignChoices,
@@ -30,15 +30,8 @@ import type { StandardCrossSection } from "./B06_Section_Api_Fetch";
import type { RockBoundaryControl } from "./B06_Section_UI_Section_View";
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
// 소단 구간(C4) — common_util 로 옮김 · 부르는 곳 그대로(다시 내보냄).
import type { BermSpan } from "@util/common_util_berm_spans";
export {
bermSpansFromStructures,
bermSpecAt,
isBermType,
type BermSessionSpec,
type BermSpan,
} from "@util/common_util_berm_spans";
// 소단 종류 판정 — common_util 로 옮김 · 부르는 곳 그대로(다시 내보냄).
export { isBermType } from "@util/common_util_berm_spans";
import { readKeyed, writeKeyed, type KeyedState } from "./B06_Section_Station_Keys";
/**
@@ -124,11 +117,6 @@ function putCell(
}
}
/** 소단 사본(결과 통 · 구조물에서 셈 · 477-4) — 쓰는 곳은 페이지 시설 줄 하나. */
export function writeBermSpans(projectId: string, routeId: number, spans: BermSpan[]): void {
writeState("berm", spans, projectId, routeId);
}
/** 암 경계선 오프셋 저장소 — 값(세션 칸)과 조정창 제어기를 함께 낸다. */
export interface RockBoundaryStore {
/** 지금 세션 칸 — 측점키(누가거리 2자리) → 오프셋(m). `buildCrossPatches` 재료(새 Map). */
@@ -7,7 +7,7 @@
* 순수 — 화면 · fetch · 세션 없음(세션 읽기는 부르는 쪽).
* ========================================================================== */
import { bermSpecAt, type BermSessionSpec, type BermSpan } from "@util/common_util_berm_spans";
import type { BermSessionSpec } from "@util/common_util_berm_spans";
import {
BERM_DEFAULT_DEPTH_M,
BERM_DEFAULT_INTERVAL_M,
@@ -42,8 +42,6 @@ export interface RefreshInputs {
rockDefault: number | null;
/** 측점별 암 절토 경사(0 = 표준값). */
cutSlopes: Array<[number, number]>;
/** 소단 사본(세션 `berm` · 없으면 null = 저장분 `design.berm`). */
bermSpans: BermSpan[] | null;
bermDepths: Array<[number, SideValues]>;
/** 카드 선택(지반 · 측구 · 2단 경사 …). */
choices: Array<[number, CrossDesignChoice]>;
@@ -141,13 +139,12 @@ export function refreshDesigns(
// 측점별 암 절토 경사도 세션에만 있는 값이다 — **계산 전에** 실어야 설계선이 새 경사로
// 그려진다. 계산 뒤에 값만 베껴 붙이면 그림은 옛 경사, 숫자만 새것이 된다(2026-09-07).
const cutSlopeRatios = new Map(inputs.cutSlopes);
// 소단도 같은 성격 — 세션에만 있는 값이라 여기서 실어 주지 않으면 계획선을 고치는
// 순간 계단이 사라진다(계획서 3-9). 사본 없음(시설 줄이 구조물을 받기 전) = 저장분 `design.berm`
// (파이썬 `load_berm_spans` 못 읽음 → 저장분 · `stored_berm` 짝 · 467).
const bermSpans = inputs.bermSpans;
// 소단도 같은 성격 — 계산 전에 실어야 계획선을 고치는 순간 계단이 안 사라진다(계획서 3-9). 제원 = 측점에
// 얹힌 소단(형상 얹기 한 벌 · 577-2) · 아직 안 얹음(칸 없음) = 저장분 `design.berm`(`stored_berm` 짝 · 467)
const bermDepthAt = bermDepthReader(new Map(inputs.bermDepths));
const bermAt = (chainageM: number, design: Record<string, unknown>): BermSpec | null => {
const spec = bermSpans ? bermSpecAt(bermSpans, chainageM) : storedBermSpec(design.berm);
const bermAt = (section: CrossSection, design: Record<string, unknown>): BermSpec | null => {
const chainageM = section.chainage_m;
const spec = section.berm === undefined ? storedBermSpec(design.berm) : section.berm;
return spec
? {
widthM: spec.width_m,
@@ -226,7 +223,7 @@ export function refreshDesigns(
tilted: Boolean(section.ford_pavement || section.underdrain),
tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3)
standard,
berm: bermAt(section.chainage_m, design),
berm: bermAt(section, design),
rockBoundaryOffsetM,
cutSlopeRatio: cutSlopeAt(section.chainage_m, design),
twoStageSlope: choice?.two_stage_slope ?? Boolean(design.two_stage_slope),
@@ -33,6 +33,7 @@ import type {
import { computeFordLayout, fordAdjustOf } from "./B06_Section_Cross_Ford_Geom";
import type { FordLayout } from "./B06_Section_Cross_Ford_Geom";
import { computeRevetmentLayout } from "./B06_Section_Cross_Revetment_Geom";
import type { RevetmentLayout } from "./B06_Section_Cross_Revetment_Geom";
/** 같은 측점으로 볼 누가거리 오차(m) — 정본 반올림 자릿수보다 크게 잡는다. */
const CHAINAGE_TOLERANCE_M = 0.02;
@@ -163,6 +164,9 @@ export const STRUCTURE_ROW_KEYS = [
"box_length_m",
// B08 자재 집계표 세월교 관 한 련 길이(131) — 그린 관 길이 1 m 올림 · 같은 길로 실음.
"ford_pipe_length_m",
// 평면 비탈 끝(578-2) — 벽이 선 쪽 횡단 최종 모양이 땅과 만나는 오프셋 · 평면 발자국 · B07 계획평면도가 읽음.
"slope_end_left_m",
"slope_end_right_m",
] as const;
/** 박스 두 끝(구체 끝) 원지반 경사(%) — 유입 − 유출 ÷ 수평거리. 본체는 늘 수평이라 원지반으로(110). */
@@ -191,6 +195,28 @@ function fordPipeLengthM(ford: FordLayout): number | null {
return length > 0 ? Math.ceil(length - 1e-6) : null;
}
/** 벽(옹벽 · 흙막이 · 돌쌓기)이 선 쪽 — 구조물 처리 뒤 횡단이 땅과 만나는 오프셋(578-2).
* 윤곽 = 트림 사면(노견 → 벽 이음) → 단마다 벽 앞(성토 위 이음 → 전면 발끝) · 사이 성토부선(3D 실루엣 차례).
* 위에서 내려와 처음 땅에 닿는 자리 · 끝까지 안 닿으면(뜬 벽) 마지막 발끝 · 노견부터 땅 밑이면 null. */
function wallSlopeEnd(section: CrossSection, own: RevetmentLayout): number | null {
const groundAt = groundInterpolator(section.samples);
const trim = own.designTrim;
const chain = [...((own.side === "left" ? trim.maxSlope : trim.minSlope)?.points ?? [])];
const segments = own.fillSegments.filter((segment) => segment.kind !== "cut");
own.walls.forEach((wall, index) => {
chain.push(wall.fillTop, wall.bottomFront, ...(segments[index]?.points ?? []));
});
if (!groundAt || chain.length < 2) return null;
let [prev, above] = [chain[0], chain[0].elevation - groundAt(chain[0].offset)];
if (above <= 0) return null;
for (const point of chain.slice(1)) {
const now = point.elevation - groundAt(point.offset);
if (now <= 0) return prev.offset + ((point.offset - prev.offset) * above) / (above - now);
[prev, above] = [point, now];
}
return prev.offset;
}
/** 폐회로 면적 입력(설계선·지반선·트림) — 트림이나 설계선이 모자라면 null. */
function areaInputOf(section: CrossSection, layouts: StoredLayouts) {
const trim = trimOfLayouts(layouts);
@@ -254,6 +280,11 @@ function areaRowOf(
const fordLength =
layouts.ford && isOwner(section.ford?.chainage_m) ? fordPipeLengthM(layouts.ford) : null;
if (fordLength !== null) ownRow.ford_pipe_length_m = fordLength;
// 벽 트림이 그려지는 측점만(관 · 세월교 · BOX 트림이 앞서면 그쪽 모양 · 578-2 범위 밖)
const own = layouts.own;
const wallEnd =
own && trimOfLayouts(layouts) === own.designTrim ? wallSlopeEnd(section, own) : null;
if (own && wallEnd !== null) ownRow[`slope_end_${own.side}_m`] = Number(wallEnd.toFixed(4));
const pipeRow = Object.keys(ownRow).length > 1 ? ownRow : null;
const areaInput = areaInputOf(section, layouts);
if (!areaInput) return pipeRow;
@@ -3,8 +3,8 @@
`_Engine_Cad_PlanKinds.py` 가 700줄을 넘어 떼어냄(141-1) — 파일 입출력 없음 · 좌표 = 사업지 m.
⚠⚠ 짝: `B05_Profile/logic/B05_Profile_PlanFootprint.ts`(route_frame · route_normal · side_is_cut ·
slope_end · slope_pieces 와 그 아래 보간 · 촘촘히 · 접힘 버림 · 모서리 둥글림) — 종단 · 횡단
편집 중 평면 발자국 · 한쪽만 고치면 화면과 계획평면도가 갈림.
slope_end · final_slope_end · slope_pieces 와 그 아래 보간 · 촘촘히 · 접힘 버림 · 모서리 둥글림)
— 종단 · 횡단 편집 중 평면 발자국 · 한쪽만 고치면 화면과 계획평면도가 갈림.
거울 시험: `resources/tester/test_346_2_plan_footprint_mirror.py`
"""
@@ -86,6 +86,19 @@ def slope_end(
return end
def final_slope_end(
design: dict[str, Any], samples: list[dict[str, Any]], edge: float, side: str
) -> float:
"""구조물 처리 뒤 비탈 끝 — 벽 측점은 B06 이 얹은 `slope_end_<쪽>_m`(578-2) · 없으면 설계선 끝.
짝: `finalSlopeEnd`(TS) — 값은 B06 `STRUCTURE_ROW_KEYS` 가 서버 Node 로 셈해 설계 칸에 얹음.
"""
stored = design.get(f"slope_end_{side}_m")
if isinstance(stored, (int, float)) and not isinstance(stored, bool):
return float(stored)
return slope_end(design.get("design_line") or [], samples, edge, side)
def _split_runs(
points: list[list[float]], flags: list[bool]
) -> tuple[list[list[list[float]]], list[list[list[float]]]]:
@@ -42,10 +42,9 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
curve_pieces,
route_frame,
side_is_cut,
slope_end,
slope_pieces,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import tick_step_m
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import final_slope_end, tick_step_m
from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import (
_chunk_route,
_hillshade_for_box,
@@ -375,13 +374,13 @@ def slope_records(
right_edge = float(edges["right"]["offset_m"])
except (KeyError, TypeError, ValueError):
continue
line, samples = (design or {}).get("design_line") or [], source.get("samples") or []
samples = source.get("samples") or []
record: dict[str, Any] = {
"chainage_m": float(chainage),
"left_edge_m": left_edge,
"left_end_m": slope_end(line, samples, left_edge, "left"),
"left_end_m": final_slope_end(design, samples, left_edge, "left"),
"right_edge_m": right_edge,
"right_end_m": slope_end(line, samples, right_edge, "right"),
"right_end_m": final_slope_end(design, samples, right_edge, "right"),
}
for side in ("left", "right"):
elevation = edges[side].get("elevation_m")
+22 -10
View File
@@ -1,10 +1,10 @@
/* =============================================================================
* common_util_berm_spans.ts
* 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4 · 화면 파일
* `B06_Section_UI_Page_Persist.ts` 에서 옮김 · 그쪽이 다시 내보냄).
* 브라우저(시설 줄 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans`)가 같은 셈.
* 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `load_berm_spans` · `berm_span_at`
* (Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py`).
* 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4) · 측점에 얹기(577-2 ·
* 벽처럼 형상 얹기 한 벌 `attachShapes` 가 `section.berm` 을 세움 · 계단은 계산 안쪽 그대로).
* 브라우저(형상 얹기 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans` · `facility.attach`)가 같은 셈.
* 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `berm_spans_from` · `load_berm_spans` · `berm_span_at` ·
* `attach_berm_specs`(Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py` · `test_323_4_attach_mirror.py`).
* ========================================================================== */
import {
@@ -15,7 +15,7 @@ import {
} from "./common_util_cross_berm";
import { catalogRow } from "./common_util_structure_catalog";
/** 측점 하나의 소단 제원 — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
/** 측점 하나의 소단 제원(`section.berm`) — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
export interface BermSessionSpec {
width_m: number;
interval_m: number;
@@ -42,10 +42,6 @@ export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape
/**
* 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다
* (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리).
*
* 세션 열쇠 `berm` 은 그 결과를 담는 **사본**이다. 재계산(브라우저·서버)이 측점마다 값을
* 읽어야 하는데 구조물 목록은 비동기로 오므로, 목록이 바뀔 때마다 여기서 펴 두고 계산은
* 그 사본만 본다.
*/
export function bermSpansFromStructures(
structures: ReadonlyArray<{
@@ -105,3 +101,19 @@ export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpe
}
: null;
}
/** 측점마다 소단 제원을 얹음(파이썬 `attach_berm_specs` 짝 · 제자리) — 덮는 소단 없음 = null(얹었음 표시 ·
* 칸 없음 = 아직 안 얹음 → 다시 셈이 저장분 `design.berm` 으로 물러섬). 얹은 측점 수. */
export function attachBermSpecs(
sections: Array<Record<string, unknown>>,
spans: BermSpan[],
): number {
let attached = 0;
for (const section of sections) {
const chainage = section.chainage_m;
if (typeof chainage !== "number" || !Number.isFinite(chainage)) continue;
section.berm = bermSpecAt(spans, chainage);
if (section.berm) attached += 1;
}
return attached;
}
+6 -1
View File
@@ -3,7 +3,7 @@
* 형상 얹기 한 벌(323-4) — 측점(횡단) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료.
*
* 구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 서버와 같은 차례로 다시 얹음 — 관 세트 → 벽 →
* 포장 → 맹암거 → 빈칸 경고. 고르기 = 표 형상(322-1). 화면은 초안 ?? 저장분 현황으로 이것 하나를 씀
* 포장 → 맹암거 → 소단 → 빈칸 경고. 고르기 = 표 형상(322-1). 화면은 초안 ?? 저장분 현황으로 이것 하나를 씀
* (`B06_Section_Section_Drafts.withDraftShapes`) — 저장 전 그림 = 저장 뒤 그림.
* 파이썬 짝 = `B06_Section/B06_Section_Engine_Attach.py` `attach_shapes`(서버 상세 읽기 · 재생성 응답) ·
* 거울 시험 = `resources/tester/test_323_4_attach_mirror.py`.
@@ -30,6 +30,7 @@ import {
underdrainSpecsFrom,
type UnderdrainInput,
} from "./common_util_structure_underdrain";
import { attachBermSpecs, bermSpansFromStructures } from "./common_util_berm_spans";
import { catalogRow, fieldName } from "./common_util_structure_catalog";
import {
blankKeys,
@@ -163,6 +164,10 @@ export function attachShapes(
),
);
attachUnderdrainSpecs(sections, underdrainSpecsFrom(structures as UnderdrainInput[]));
attachBermSpecs(
sections,
bermSpansFromStructures(structures as Parameters<typeof bermSpansFromStructures>[0]),
);
attachBlanks(sections, pipes, structures, types);
return misses;
}
@@ -13,4 +13,5 @@ export const SHAPE_KEYS = [
"revetment",
"pavement",
"underdrain",
"berm",
] as const;
+10
View File
@@ -0,0 +1,10 @@
# PLAN 575 이관 (2026-10-29 · 요구 432 · 434 · 435)
## 575. 노선 손 고침 [저장] 뒤 종단이 지반고로 (요구 432 · 브레인 조사)
- 정함(브레인 · 분석 432-A): 498-2 탓 아님 · 옛 규칙 — 손 [저장] = 옛 선 1 m 안 측점만 옛 높이 · 나머지 「지반 첫 계획선」(계획고 = 지반고) · 균형 종단은 기준 꼭짓점(base_pvi)에 있음 · 검증_랩탑메인 22:04 교점 19 → 16 = 57 중 39 측점 바뀜 → 최대 76.67 % · 미달 43 · 덧 흠 = 손 [저장]이 「종단 반영」을 끔으로 덮음(`B05_Profile_Router_Replan.py:586-592`) · 근거 `docs/raw/요구_1025/_분석_432-A_종단_지반고.md`.
- [x] 575-1 ⬜ 사용자 결정 — 바뀐 구간 종단: A 그 구간만 균형 종단 다시 풂(권함) · B 옛 높이를 길이 비례로 옮김 · C 지금대로 + 알림 · 고칠 자리 `B05_Profile_Route_Save.py:277` · `B05_Profile_Engine_Grade_Carry.py:120-142` · 짝 TS `B05_Profile_Grade_Carry.ts:163-180` · `B05_Profile_Pipeline_Grade.ts:426-461`.
- 검증완료 — 사용자 B(요구 434 · 435 「진행」).
- 사용자 확정(요구 434 · 435): B = 노선 손 고침 [저장] 때 바뀐 구간 종단 = 옛 종단(기준 꼭짓점) 높이를 길이 비례로 옮김(다시 풀지 않음 · 빠름) · 넘친 기울기 · 미달은 지금 표시로 · 고칠지는 설계자 · 덧 흠(손 [저장]이 「종단 반영」 끔으로 덮음) 같이 고침.
- [x] 575-2 B 고침 — 파이썬 `B05_Profile_Route_Save.py:277` · `B05_Profile_Engine_Grade_Carry.py:120-142` ↔ TS `B05_Profile_Grade_Carry.ts:163-180` · `B05_Profile_Pipeline_Grade.ts:426-461`(짝 · 거울 시험) · 덧 `B05_Profile_Router_Replan.py:586-592` — sub3 Opus high · 확인: 교점 지움 · 큰 끌기 [저장] 뒤 계획고 ≠ 지반고 · 끌 때 화면 = 저장 뒤 · 안 바뀐 구간 0 차 · 「종단 반영」 그대로.
- 검증완료 — 검증_sub3 교점 지움 + 41.8 m 끌기 [저장]: 바뀐 17 측점 계획고=지반고 0 곳(옛 종단 비례) · 같은 자리 꼭짓점 0 mm(측점 계획고는 연장 바뀌어 측점 격자 밀린 곡선 차 ≤ 110 mm · 옛 규칙과 같음) · 최대 기울기 11.72→12.73 % · 미달 19→12 · 끌 때 화면=저장 뒤=새로고침 0.0 mm(74 측점) · 종단 반영 켬 그대로 · 그대로 [저장] 0 mm · 추천 [적용] 정상(켬 기록) · 노선 저장 체인 13.5 s(계획고 옮김 0.4 s · 전 9~12 s · 0.4~1.1 s) · 비례 기준 = 같은 자리 측점 + 시점 0 · 종점(새 ↔ 옛 연장) · 거울 · 고친 대상 시험 통과 · typecheck 0 · 밝음 · 어두움 · ORCA · 되돌림 같음.
+11
View File
@@ -0,0 +1,11 @@
# PLAN 577 이관 (2026-10-29 · 요구 434 · 436 · 437)
## 577. 사면안정 구조물군 한 벌 — 소단 사본 걷음 (요구 434-4 · 435 · 브레인 조사)
- 정함(브레인 · 분석 434): 놓기 · structures.json · M02 표 제어는 구조물군 모두 같은 길 · 횡단 계산만 다름(벽 = 계산 뒤 사면 잘라 덧그림 · 소단 = 계산 앞 설계선 계단 · 성질이라 유지) · 계산 앞 구조물(포장 · 물넘이 · 세월교 하강 · 맹암거)은 사본 없이 감 · 소단만 저장 노선 다시 셈 길에서 세션 사본을 거침(09-07 B06 측점 입력 시절 흔적) · 근거 `docs/raw/요구_1025/_분석_434_사면안정_구조물군.md`.
- 정함(브레인 · 분석 434 2차 · 요구 436 사면안정 그룹끼리): 벽(옹벽 · 흙막이 · 돌쌓기) = 측점마다 얹음 → 계산은 표준 사면 → 벽 상단에서 사면 자르고 면적 칸만 덮음(설계선 = 표준) · 소단 = 측점 얹기 없음 · 계산 안에서 성토 사면 계단(늘림 · 비탈 끝 다시 찾음) · 소단을 벽처럼 계산 뒤에 넣는 길은 안 됨(늘림이라 비탈 끝 · 면적 · 3D 따로 고쳐야) · 권함 = 가(소단도 옹벽처럼 측점에 제원 얹고 계단은 계산 안 · 값 같음 · 사본 사라짐 · 소단 측점 3D 한 번 다시 셈) · 덤 = 평면 발자국 · B07 계획평면도가 벽에 잘린 모양 모름(코드 읽기).
- [x] 577-1 ⬜ 사용자 결정 — 안 A(권함) 저장 노선 길에도 구조물 목록을 넘겨 사본 없앰(`design_pipeline_page.ts` · `design_shared.ts` · `B06_Section_Cross_Refresh.ts` 고침 · `design_facility_lane.ts` · `B06_Section_UI_Page_Persist.ts` · `b_page_state.ts` 사본 지움 · 값 같음) / 안 B 소단도 측점에 얹는 한 벌(범위 큼 · 소단 측점 3D 다시 셈).
- 검증완료 — 사용자 가(요구 437).
- 사용자 확정(요구 437): 가 = 소단도 옹벽처럼 측점에 제원을 얹는 한 벌 · 3D 까지 이어짐 · 꼴 = 소단 자리에서 사면을 자르고 수평선(소단 폭) + 그 끝에서 새 성토선 · 개발 순서상 늦게 들어와 빠졌던 것.
- [x] 577-2 가 고침 — 소단 측점 얹기(TS · 파이썬 짝) · 읽는 쪽(저장 노선 다시 셈 · 노선 초안 · 서버 묶음) · 사본 지움(`design_facility_lane.ts` · `B06_Section_UI_Page_Persist.ts` · `b_page_state.ts`) · 횡단 · 토공 · 평면 비탈 끝 · B07 · 3D · 수량까지 — sub1 Opus high · 확인: 설계 값 전후 같음 · 3D 소단 계단 보임 · 사본 0.
- 검증완료 — 검증_sub1 5+0 소단 폭 1.5: 성토 28.13→46.37 ㎡ · 평면 비탈 끝 16.85→20 m · 3D 바깥선 21.4 m · 횡단 카드 계단(밝음 · 어두움 · ORCA) · ↶ 28.13 ↷ 46.37 · [저장] 뒤 새로고침 같음 · B07 성토 사면 25.38 m(소단 1.5) · 옛 코드 대 새 코드 화면 값 차 0(65 측점 면적 · 설계선) · 세션 berm 칸 0 · 시험 거울(272 · 323-4) 통과 · typecheck 0 · 묶음 다섯 다시 지음 · storage · DB 되돌림
@@ -0,0 +1,180 @@
# 요구 434-4 · 435 — 사면안정 구조물군 로직 조사
조사만 · 코드 · git · storage · 마스터 안 고침 · 서버 · 브라우저 안 씀.
## 0. 한 줄 결론
- 소단도 놓기 · 저장 · M02 표 제어는 다른 사면안정 구조물과 같은 길임.
- 다른 것은 「횡단 계산에 들어가는 문」 하나 — 벽(옹벽 · 흙막이 · 돌쌓기)은 **계산 뒤 덧그림 · 자름**, 소단은 **계산 앞 입력**(설계선 자체가 계단)임. 이것은 성질이라 못 합침.
- 그런데 계산 앞 입력인 다른 시설(포장 · 물넘이 기울임 · 세월교 하강 · 맹암거)은 사본 없이 구조물 목록이나 측점에 얹은 형상에서 바로 읽음. 소단만 **저장 노선 다시 셈 한 길**에서 세션 사본 `berm` 을 거침.
- 사본은 성질 탓이 아니라 이력 탓 — 2026-09-07 처음엔 B06 카드의 측점 입력(세션 `berm` = 「측점키 → 폭 · 간격 · 기울기」 · 암 경계 `rockb` 와 같은 자리)이었고, 같은 날 「구조물 배치」로 옮기며 세션 칸을 구조물에서 편 사본으로 남김(`docs/raw/plans/2026-09-07_plan_completed_items_2.md:866-884`).
## 1. 마스터에 있는 사면안정 구조물군
| 종류 | type_id | 군 | 목록 | 표 형상 | 횡단도 | 3D | 정의 · 부품 줄 |
|---|---|---|---|---|---|---|---|
| 옹벽 | retaining_wall | C | 켜짐 | wall(형태 콘크리트 · 물빼기) | span3 | wall | `resources/master_template/구조물정의.json:919` · `common_util/common_util_structure_parts.json:287` |
| 흙막이 | soil_guard | C | 켜짐 | wall(빈 형태 = 통나무 · 목재틀) | span3 | wall | `구조물정의.json:1468` · `parts.json:318` |
| 소단 | berm | C | 켜짐 | **berm** | span3 | surface(설계선만) | `구조물정의.json:1905` · `parts.json:338` |
| 돌쌓기(찰) | masonry_wet | C | 숨김 | wall | span3 | wall | `구조물정의.json:1028` · `parts.json:298` |
| 돌쌓기(메) | masonry_dry | C | 숨김 | wall | span3 | wall | `구조물정의.json:1247` · `parts.json:308` |
| 큰돌쌓기 | boulder_masonry | C | 숨김 | wall | span3 | wall | `구조물정의.json:1720` · `parts.json:328` |
| (곁) 기슭막이 | revetment | D | 숨김 · 관 지점 정본 | hidden_revet | span3 | wall | `parts.json:366` |
| (곁) 비탈면 녹화 | revegetation | F | B06 만 | none | none | none | `구조물정의.json:2898` · `parts.json:479` |
- 낙석 · 블록 독립 종류 없음 — 블록은 옹벽 「형식」 선택지 「식생옹벽블럭」(detail 단계)뿐.
- 표 09(54-26) 칸 — 흙막이 형태 · 길이 · 높이 · 전 · 후 / 옹벽 형태 · 길이 · 높이 · 전 · 후 / 소단 **폭만**(`구조물정의.json:3379` 「표시」가 깊이 · 간격 · 기울기 · 높이 · 길이 · 전 · 후 숨김 → 깊이 1.0 · 간격 3.0 · 0° 기본값으로 돎).
- 「표 형상」 칸이 길을 가름(`common_util/common_util_structure_catalog.py` `resolve_catalog` · TS 짝) — wall 이면 벽 길 · berm 이면 소단 길 · 종류 키로 안 가름. M02 제어는 이미 같은 문.
## 2. 길 표 — 벽 대 소단
| 단계 | 벽(옹벽 · 흙막이 · 돌쌓기 · 큰돌) | 소단 |
|---|---|---|
| 놓기 | B05 「구조물 배치」 같은 폼 · 옹벽 · 흙막이 「추가 단」(`A00_Common/design/logic/design_structure_slope_tier.ts:26`) | 같은 폼 · 칸은 폭만 |
| 저장(정본) | `structures.json`(`B05_Profile/B05_Profile_Structures_Repository.py:50` 읽기 · `:110` 쓰기) | 같음 |
| 측점에 얹기 | 형상 얹기 한 벌 `attachShapes`(`common_util/common_util_structure_attach.ts:130-168`) → `wallSpecsFrom` · `attachWallSpecs`(`common_util/common_util_structure_walls.ts:105-168`) → `section.revetment` · 파이썬 짝 `B06_Section/B06_Section_Engine_Structures_Wall.py:72-110 · 179-197` · 서버 GET `B06_Section/B06_Section_Engine_Attach.py:169-199` | **안 얹음** — 구조물 목록 → `bermSpansFromStructures`(`common_util/common_util_berm_spans.ts:50-90`) → 구간 → 측점마다 `bermSpecAt`(:93) · 파이썬 짝 `Engine_Structures_Wall.py:113-176` |
| 횡단 계산 | 표준 설계선은 벽을 모르고 셈 → **계산 뒤** 벽 기하(`computeRevetmentLayout`)가 성토 사면을 벽에서 자름(트림) → 폐회로 면적으로 면적 칸 덮어씀(`B06_Section/logic/B06_Section_Structure_Layouts.ts:98-145 · 287 · 379` · `common_util/common_util_cross_structure_areas.ts`) · 저장 설계선 = 표준선 그대로 | **계산 앞** 입력 `berm` → 설계선 자체가 계단(`common_util/common_util_cross_design_geometry.ts:356` · `common_util_cross_berm.ts` · 파이썬 짝 `.py`) → 지반과 만나는 점이 밀림 · 면적 = 기본 적분 · 결과 `design.berm` 저장 |
| 횡단 조절(사용자) | 카드 끌기 → `design.revet_adjust`(횡단 patch · `B06_Section/logic/B06_Section_Design_Keys.ts:18`) | 카드 ▲▼ → 세션 `bermdepth` → `design.berm_depth_left/right_m`(횡단 patch · 같은 파일 :40-41) |
| 횡단 그림 · B07 | 덧그림 `B06_Section/B06_Section_UI_Cross_Revetment.ts` · B07 `B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts:493-500`(트림 설계선) · `:513`(벽) · 전개도 `B07_DesignDetail_Engine_Cad_Development.py:150` | 설계선 그대로(같은 `:493`) · B07 사면 가름 `B07_DesignDetail_Engine_SlopeGeometry.py:178 · 240`(design.berm) |
| 3D | 벽 스윕 `B05_Profile/logic/B05_Profile_Corridor_Structures.ts:300-306`(section.revetment) | surface = 설계선만 → 계단이 면에 그대로 |
| 수량(B08) | 토공 = 트림 폐회로 면적 · 구조물 집계 줄 없음(`B08_Quantity/B08_Quantity_Router_Summary.py:79-81` 배수 종류만) | 토공 = 계단 설계선 면적 · 집계 줄 없음 |
## 3. 계산 앞 입력 — 소단 대 다른 시설
| 길 | 소단 원천 | 포장 · 물넘이 기울임 · 세월교 하강 · 맹암거 원천 |
|---|---|---|
| 노선 초안(브라우저 묶음 cross 엔진) | `ctx.inputs.structures` → `bermAt`(`A00_Common/design/design_pipeline_page.ts:348` · `design_shared.ts:258-266`) → `crossWithBerm`(`B05_Profile/logic/B05_Profile_Pipeline_Detail.ts:123-146`) | 같은 `ctx.inputs.structures` → `crossWithShapes`(같은 파일 :267-286 · 형상 얹기) |
| **저장 노선 다시 셈**(브라우저 · cross=server 면 Node `cross.refresh`) | **세션 사본 `berm`**(`B06_Section/B06_Section_Cross_Refresh.ts:231-242`) ← 시설 줄 `syncBerm` · `putBerm`(`A00_Common/design/design_facility_lane.ts:155-182`) · 사본이 바뀌면 cross 를 한 번 더 부탁(forceCross) | 다시 셈 바로 앞에 형상 다시 얹음(`Cross_Refresh.ts:138` → `withDraftShapes` → `attachShapes`) → 측점 형상 읽음(`B06_Section/logic/B06_Section_Cross_Refresh_Calc.ts:224-243`) |
| 서버 Node 묶음 cross | `ctx.inputs.structures`(`A00_Common/design/logic/design_calc_node_stages.ts:193`) | 같은 status |
| [저장] · [확정] | 구조물 정본(TS `B06_Section/logic/B06_Section_Cross_Save.ts:222-256` · 파이썬 `B06_Section_Router_Design.py:224-228` → `B06_Section_Engine_Design_Node.py:139` Node `cross.berm_spans`) | 구조물 · 관 정본(`facilityRanges` 같은 파일 :222 · 파이썬 tilt_ranges · ford drops) |
| B05 확정 · 넓힘 | 저장 결과 `design.berm`(`B05_Profile_Router_Confirm.py:184` · `Engine_SampleExtend.py:162`) | 저장 결과 `design.paved` 등 |
- 사본은 저장 노선 다시 셈 한 길에만 있음. 까닭 = 묶음이 저장 노선 길 `savedCross(detail, routeId, stations)`(`design_pipeline_page.ts:357`)에 구조물 목록을 안 넘김 — 엔진 길(:348)은 넘김.
## 4. 소단만 다른 까닭
- **비탈 모양을 계산 전에 바꿔야 함 — 맞음.** 소단은 성토 사면 노면 끝 아래 깊이에 한 단 · 계단 뒤 사면이 다시 지반까지 걸어 내려가 만나는 점이 밀림(3-9 기록 — 늘어난 면적이 「폭 × 높이」가 아니라 밀린 거리로 정해짐). 계산 뒤 자르는 벽 방식으로는 못 냄.
- **벽은 비탈 모양을 안 바꿈.** 표준 사면으로 셈한 뒤 벽 자리에서 자르고 성토부선으로 대신 그림 · 면적만 폐회로 값(2026-09-06 확정 「구조물 자체 면적을 빼는 것이 아님」).
- **같은 일을 하는데 길이 다른 것 — 사본 하나.** 같은 「계산 앞 입력」인 포장 · 물넘이 · 세월교 하강 · 맹암거는 사본 없이 감. 소단 사본은 옛 B06 측점 입력의 흔적이고, 그 뒤 465 · 467(B05 만 연 탭) · 477-4(결과 통)로 사본을 덧고쳐 옴.
- 사본 때문에 따로 있는 것 — 시설 줄 `syncBerm` · `putBerm` · 사본 비교 · forceCross 한 바퀴(`design_facility_lane.ts:175-181`) · 계산 자리 서버 갈래 `C4.copy` · 지킴 시험(`test_477_undo_now_berm` · `test_272_berm_source`).
## 5. 한 벌로 합치는 안
### 안 A — 사본 없앰 (가장 작음 · 권함)
- 저장 노선 길에도 엔진 길처럼 구조물 목록을 넘김 — `design_pipeline_page.ts:357` 에서 `ctx.inputs.structures` 를 `savedCross` 에 더 넘김 → `design_shared.ts:270` savedCross 에서 노선 초안 `bermAt`(:262)이 이미 쓰는 `calcChecked("quantity", "cross.berm_spans", bermSpansFromStructures, …)` 로 구간 셈 → `CrossRefreshInput`(`Cross_Refresh.ts:75`)에 `bermSpans` 칸 → `gatherRefresh`(:233-241)가 세션 대신 그 값.
- 지움 — 세션 칸 `berm`(`A00_Common/b_page_state.ts` 결과 통 줄) · `writeBermSpans`(`B06_Section/B06_Section_UI_Page_Persist.ts:128`) · `syncBerm` · `putBerm` · `run(syncBerm)` 두 곳(`design_facility_lane.ts:155-182 · 237 · 244`).
- 고칠 파일 — `A00_Common/design/design_pipeline_page.ts` · `design_pipeline_page_types.ts:67` · `design_shared.ts` · `B06_Section/B06_Section_Cross_Refresh.ts` · `A00_Common/design/design_facility_lane.ts` · `B06_Section/B06_Section_UI_Page_Persist.ts` · `A00_Common/b_page_state.ts`. 시험 — `test_477_undo_now_berm.py` · `test_272_berm_source.py` · `test_511_a_calc_browser.py:62`(`C4.copy` 줄) · `helper_271` · `helper_473`(빈 stub).
- 파이썬 · TS 짝 — 그대로(파이썬 안 고침). 서버 Node 묶음 — `cross.refresh` 가 받는 `bermSpans` 꼴이 같고 `Cross_Refresh_Calc.ts` 안 고침 → 다시 짓기 없음.
- 값 영향 — 없음(같은 구조물 → 같은 구간). 구조물 고친 뒤 cross 한 바퀴 줄어듦 · 옛 사본으로 한 번 셌다 다시 세는 깜빡임 없어짐(코드 읽기 · 실측 안 함).
- 위험 — 낮음. 저장분 옛 꼴 없음(structures.json · design.berm 그대로 · 탭에 남은 옛 세션 `berm` 은 안 읽힘). 정본 자리 그대로 — 소단 제원 = 구조물 배치(structures.json) · 쪽 깊이 = 횡단 patch · design.berm = 계산 결과. 구조물을 아직 모를 때는 지금처럼 저장분 design.berm 으로 물러섬 유지.
### 안 B — 소단도 측점에 얹음(형상 얹기 한 벌에 들임)
- `attachShapes`(TS) · `_attach_python`(파이썬 짝)에 소단 얹기를 더해 측점 칸(예 `berm_span`)을 세움 → 다시 셈은 포장처럼 측점 칸을 읽음(`Cross_Refresh_Calc.ts:150`) · 노선 초안 `shapeContext`(`B05_Profile_Pipeline_Detail.ts:256`)도 같은 칸 → `crossWithBerm` · `bermAt` 옵션 · `bermSpans` 입력 지움 + 안 A 의 사본 지움.
- 고칠 파일 — `common_util_structure_shape_keys.ts` · `common_util_structure_attach.ts` · `common_util_berm_spans.ts` · `B06_Section_Engine_Attach.py`(SHAPE_KEYS · `_attach_python`) · `B06_Section_Engine_Structures_Wall.py`(얹기 짝) · `Cross_Refresh_Calc.ts` · `B05_Profile_Pipeline_Detail.ts` · `design_shared.ts` · `design_pipeline_page(_types).ts` · `design_calc_node_stages.ts` + 안 A 지움.
- 짝 — TS ↔ 파이썬 얹기 거울(`test_323_4_attach_mirror`) 늘림. 서버 Node 묶음(server-calc · corridor) 다시 지어야 반영.
- 값 영향 — 설계 값 없음. `SHAPE_KEYS` 가 3D 지문(`B05_Profile/logic/B05_Profile_UI_Corridor_Structures_Hash.ts:41`)에 들어가 소단 측점 3D 저장본이 한 번 다시 셈 · 서버 GET 상세에 새 칸.
- 위험 — 중간. 정본 못 읽을 때 물러섬 · B07 · E03 이 같은 얹기를 씀(`E03_StabSection/E03_StabSection_Walls.py:50`).
- 이득 — 사면안정 구조물(벽 · 소단) 모두 「표 형상 → 측점 얹기 → 그림 · 계산」 한 길.
### 안 C — 「계산 앞 시설 맥락」 한 함수
- 같은 판정이 셋으로 나뉨 — 노선 초안 `shapeContext`(`B05_Profile_Pipeline_Detail.ts:256-264`) · 다시 셈 안쪽(`Cross_Refresh_Calc.ts:224-243`) · [저장] `facilityAt`(`B06_Section/logic/B06_Section_Cross_Enforce.ts:71`) + 소단 `bermSpecAt`. 안 B 위에서 포장 · 기울임 · 하강 · 소단을 한 함수로 모으고 [저장] 파이썬 짝(`B06_Section_Router_Design.py` 포장 · 기울임 · 소단)도 그 꼴로.
- 범위 큼(파이썬 [저장] 길 · 거울 시험 셋) · 지금 값 어긋남 없음 → 지금은 안 권함.
### 권함
- **안 A.** 물음(「왜 소단만 사본」)의 원인을 바로 없앰 · 값 그대로 · 파이썬 · 서버 묶음 안 건드림.
- 소단이 「계산 앞 입력」인 것은 성질이라 남음 — 벽과 같은 문(계산 뒤 자름)으로 합치는 것은 기하상 불가.
- 안 B 는 측점 얹기까지 한 길로 하고 싶을 때 다음 단계.
## 6. 사용자 물음
- 안 A(사본만 없앰 · 값 같음)로 갈지 · 안 B(소단도 다른 구조물처럼 측점에 얹음 · 소단 측점 3D 한 번 다시 셈)까지 갈지.
## 7. 확인 못 함
- 화면 실측 — 서버 · 브라우저 안 씀(지시대로) · 안 A 의 「cross 한 바퀴 줄어듦」은 코드 읽기로만.
- 낙석 · 블록 — 마스터에 독립 종류 없음 · 사용자 표 09 원본 문서와 대조 안 함.
- 3D surface 가 소단 계단을 그대로 면으로 그리는지 — 부품 이름 「설계선만(몸체 없음)」 · 코리도 빌드가 design_line 을 읽는 것까지만 봄.
- B08 사면안정 구조물 집계 — 지금 집계표가 배수 종류만이라 벽 · 소단 수량 길 없음(재작업 범위 · 이 조사 밖).
- 안 A 에서 계산 자리 quantity = server 일 때 저장 노선 소단 값 — 지금 `C4.copy` 는 서버 값만 · 노선 초안 `bermAt` 은 브라우저 값 + 뒤 대조. 안 A 는 노선 초안 꼴로 맞추는 것으로 봄(코드상 둘 다 가능).
---
## 2차 — 사면안정 그룹끼리 비교 (요구 436)
1차의 포장 · 물넘이 · 세월교 · 맹암거 비교는 걷음. 같은 사면안정 그룹(옹벽 · 흙막이 · 돌쌓기 찰 · 메 · 큰돌쌓기 = 표 형상 wall)과 소단(표 형상 berm)만 견줌.
### 2-1. 벽(옹벽 · 흙막이 · 돌쌓기)이 횡단에 들어가는 길
| 단계 | 하는 일 · 자리 |
|---|---|
| ① 구조물 목록 받기 | 서버 상세 읽기 `B06_Section/B06_Section_Router.py:254 · 462` → `attach_saved_shapes`(`B06_Section_Engine_Attach.py:186-199` · structures.json + 노선 관) · 브라우저 초안 `withDraftShapes`(`B06_Section/B06_Section_Section_Drafts.ts:130-158` · 초안 ?? 저장분) · 다시 셈 바로 앞 한 번 더(`B06_Section_Cross_Refresh.ts:138`) · 노선 초안 `attachStatus`(`B05_Profile/logic/B05_Profile_Pipeline_Detail.ts:234`) |
| ② 측점에 얹기 | 형상 얹기 한 벌 `attachShapes`(`common_util/common_util_structure_attach.ts:130-168` · 서버는 Node `facility.attach` 로 같은 TS · 못 부르면 파이썬 `_attach_python` `Engine_Attach.py:111-132`) → `wallSpecsFrom` · `attachWallSpecs`(`common_util/common_util_structure_walls.ts:105-168`) → **측점 겉 칸 `section.revetment`**(높이 · 형태 · 설치 측 · 기초 · 추가 단 · 전면 기울기) · 정본에 안 씀(읽을 때마다 걷고 다시 얹음) |
| ③ 횡단 계산 | `computeCrossDesign` 은 벽을 모름 → 설계선 · 면적 = 표준 사면 그대로 |
| ④ 계산 뒤 사면 자르기 | `computeRevetmentLayout`(`B06_Section/logic/B06_Section_Cross_Revetment_Geom.ts:160-334`) — 벽 자리(노견 · 조절 x · d) · 벽 도형 · 다단 → `designTrim`(:296-312) = **벽 상단에서 설계선을 끊음** · 노견→벽 상단 성토선 대신 · 벽 바깥은 원지반(면적 0). 묶음 = `computeStoredLayouts`(`Structure_Layouts.ts:95-131`) · 트림 고름 `trimOfLayouts`(:136-143 · 관 ?? 세월교 ?? BOX ?? 벽 — **한 측점에 트림 하나**) |
| ⑤ 측점 설계 칸에 쓰는 것 | 면적 칸만 덮어씀 — `cut_area_m2 · fill_area_m2 · cut_soil_area_m2 · cut_rock_area_m2`(`STRUCTURE_ROW_KEYS` `Structure_Layouts.ts:153` · `areaRowOf` :211 → 폐회로 `computeStructureAreas` `common_util/common_util_cross_structure_areas.ts:127-170` → `applyStructureAreaRows` :379) · 사용자 조절 `design.revet_adjust.own`(횡단 patch) · `design_line` 은 표준선 그대로 · 벽 제원은 design 에 안 씀 |
| ⑥ 면적 셈 부르는 자리 | 다시 셈 `Cross_Refresh.ts:155-163` · 상세 가게 `B06_Section_Section_Store.ts:132` · 노선 초안 `design_shared.ts:286-292` · 서버 Node `design_calc_node.ts:137` · `design_calc_node_cross.ts:95` — 파이썬 짝 없음(서버도 이 TS 실행) |
| ⑦ 토공 면적(B08) | `B08_Quantity/B08_Quantity_Engine_Earthwork.py:125` 가 design 면적 칸(덮어쓴 값)을 읽음 |
| ⑧ 횡단 그림 · B07 | B06 카드 `B06_Section/B06_Section_UI_Cross_Revetment.ts` · B07 덧그림 `B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts:493-500`(트림 설계선) · `:513`(벽) · CAD 장은 화면 덧그림으로 계획선 · 구조물 층을 갈아 끼움(`B07_DesignDetail_Engine_Cad_Sheet.py:248-249`) · 전개도는 structures.json 직접(`B07_DesignDetail_Engine_Cad_Development.py:146-160`) |
| ⑨ 3D | `B05_Profile/logic/B05_Profile_Corridor_Structures.ts:300-306` 벽 스윕(`section.revetment` → `computeRevetmentLayout`) · 비탈 면은 같은 `designTrim` 을 따름(`B05_Profile_Corridor_Envelope.ts:126 · 164`) |
| ⑩ 수량 | B08 구조물 집계 줄 없음(`B08_Quantity_Router_Summary.py:79-81` 배수만) · 벽 몸 ∩ 성토 겹침(`wallBodyInFillM2`)은 관 세트 벽만(`Structure_Layouts.ts:327-356`) |
| ⑪ 안정성(E03) | `E03_StabSection/E03_StabSection_Walls.py:37-50`(같은 `attach_saved_shapes`) · `E03_StabSection_Walls_Node.ts`(같은 `drawnProfile`) |
| 짝 | 제원 TS `common_util_structure_walls.ts` ↔ 파이썬 `B06_Section_Engine_Structures_Wall.py:72-110 · 179-197`(거울 `test_b06_structure_walls_mirror.py`) · 얹기 TS ↔ 파이썬(`test_323_4_attach_mirror.py`) · 자르기 기하 · 폐회로 면적 = TS 한 벌 |
- 곁 발견(안 고침) — 평면 발자국 비탈 끝(`B05_Profile/logic/B05_Profile_PlanFootprint.ts:133-166` · 짝 B07 계획평면도 `B07_DesignDetail_Engine_Cad_PlanSlope.py`)은 `design_line` 만 읽고 벽 트림을 모름 → 벽이 선 자리도 표준 비탈 끝으로 그림(코드 읽기 · 화면 확인 못 함).
### 2-2. 소단이 지금 들어가는 길(같은 단계로)
| 단계 | 소단 |
|---|---|
| ① 목록 | 같은 structures.json · 받는 자리는 셋으로 갈림 — 노선 초안 `design_pipeline_page.ts:348` · 저장 노선 다시 셈 = 세션 사본(`Cross_Refresh.ts:231-242` ← `design_facility_lane.ts:155-182`) · [저장] `Cross_Save.ts:222-256` · `Router_Design.py:224` |
| ② 측점에 얹기 | **없음** — 측점 칸 없이 구간 목록(`common_util_berm_spans.ts:50-90`) → 측점마다 `bermSpecAt` |
| ③ 횡단 계산 | **계산 안쪽** — 성토 사면 꼭짓점에 계단(`common_util_cross_design_geometry.ts:346-361` `fillPoints` · 설계선 꼭짓점 :539-548) → 계단 뒤 사면이 지반과 새로 만나는 점(비탈 끝)을 찾음 |
| ④ 계산 뒤 | 없음 |
| ⑤ 설계 칸 | `design_line`(계단 든 선) · 면적(기본 적분) · `design.berm`(제원 메아리) · `berm_depth_left/right_m`(▲▼ 조절 · 횡단 patch) |
| ⑦–⑪ | 모두 `design_line` · 면적 칸을 그대로 읽어 따로 할 일 없음 — 평면 비탈 끝 · B07 사면길이(`SlopeGeometry.py:178 · 240`) · 3D 면 · E03 · 미교차 넓힘이 다 계단을 앎 |
### 2-3. 소단을 벽 길로 옮길 수 있나
- **(ㄱ) 측점 칸 꼴만 같게(벽처럼 `attachShapes` 로 측점에 제원을 얹고 계산은 그 칸에서 읽음)** — 가능 · 값 0 차. 계단은 지금처럼 계산 안쪽.
- **(ㄴ) 계산 뒤 계단 덧 처리(벽과 완전 같은 문)** — 기하로는 되나 같은 값이 나오는 자리가 적음.
- 성질 차이 — 벽 트림은 **자름**(표준 사면을 벽 상단에서 끊고 벽면으로 지반에 닫음 · 새 교점 찾기 없음). 소단은 **늘림**(계단 뒤 사면이 더 바깥에서 지반과 만남 · 비탈 끝을 다시 찾아야 함 = 지금 계산 안쪽이 하는 일을 뒤에서 한 번 더).
- 밀리는 거리 — 폭 ÷ (1 − 성토 경사 n × 지반 기울기). 예 성토 1:1.2 · 지반 1:2(0.5) → 폭 0.5 m 가 비탈 끝을 1.25 m 밖으로 · 지반이 가파를수록 커짐.
- 토공 면적 — 뒤 처리가 같은 함수(`fillProfilePoints` · 지반 교점)를 다시 부르면 표본 안쪽 측점은 0 차. 밀린 비탈 끝이 지반 표본 밖이면 지금은 `slope_unclosed` → DTM 넓힘(+5 m · `extendUnclosed`)이 받아 주는데, 뒤 처리는 그 판정이 끝난 뒤라 못 받음 → 면적이 표본 끝에서 잘려 **작아짐**.
- 비탈 끝(daylight) 자리 — `design_line` 이 표준선으로 남음 → 평면 발자국 · B07 계획평면도 · 용지 경계가 소단 없는 자리(밀린 거리만큼 안쪽)로 그려짐.
- 횡단 그림 — 트림 선을 그리면 B06 · B07 덧그림은 같게 보임. 그러나 `trimOfLayouts` 가 한 측점에 트림 하나만 고름 → 벽 + 소단이 한 측점에 같이 서면 하나를 잃음 · 트림 합치기 새로 필요. 지금은 계단이 설계선 안에 있어 벽 트림이 그 선을 그대로 자름(겹쳐도 됨).
- 사면 길이 · 사면보호공(B07 횡단 수량표) · 3D 면 · E03 모형 · 성토 지반 기울기 칸 — 다 `design_line` 을 읽음 → 각 자리가 소단 트림을 따로 배워야 지금과 같음(한 벌이 아니라 여러 벌).
- 수량(B08 토공) — 면적 칸 덮어쓰기라 표본 안쪽은 0 차 · 표본 밖은 위처럼 작아짐.
- 결론 — 입구(측점 칸)는 벽과 같게 할 수 있음 · 계단을 넣는 시점(계산 안쪽)은 비탈 끝을 바꾸는 성질이라 그대로 두는 것이 맞음.
### 2-4. 안 다시 정리
**가 — 사면안정 그룹 한 벌(소단도 옹벽처럼 측점에 얹음 · 계단은 계산 안쪽 그대로)**
- 범위 — 얹기: `common_util/common_util_structure_walls.ts`(또는 `common_util_berm_spans.ts`)에 소단 얹기 · 파이썬 짝 `B06_Section_Engine_Structures_Wall.py` · `common_util_structure_attach.ts:130-168` 한 줄 · `common_util_structure_shape_keys.ts` · `B06_Section_Engine_Attach.py:38 · 111-132`. 읽기: `B06_Section/logic/B06_Section_Cross_Refresh_Calc.ts:147-160`(측점 칸) · `B05_Profile_Pipeline_Detail.ts:256-264`(노선 초안도 측점 칸) · `crossWithBerm`(:123-146) · `design_shared.ts:258-266` bermAt · `design_pipeline_page.ts:348` · `design_pipeline_page_types.ts:65` · `design_calc_node_stages.ts:193`. 사본 지움: `design_facility_lane.ts:155-182 · 237 · 244` · `B06_Section_UI_Page_Persist.ts:128` · `b_page_state.ts`. [저장] 길(`Cross_Save.ts:222-256` · `Router_Design.py:224`)은 구조물 직접 그대로 둠.
- 시험 — `test_323_4_attach_mirror` · `test_b06_structure_walls_mirror` 늘림 · `test_272_berm_source` · `test_477_undo_now_berm` · `test_511_a_calc_browser.py:62` · helper 271 · 473.
- 값 영향 — 설계 값 0 차(같은 구조물 · 같은 셈). 측점 칸이 3D 지문(`B05_Profile_UI_Corridor_Structures_Hash.ts:41`)에 들어가 소단 측점 3D 저장본이 한 번 다시 셈 · 서버 상세에 새 칸.
- 위험 — 중간. 파이썬 얹기 짝 · 서버 Node 묶음(server-calc · corridor) 다시 짓기 · E03 이 같은 얹기를 씀. 저장분 옛 꼴 없음(structures.json · design.berm 그대로). 정본 자리 그대로(제원 = 구조물 배치 · 쪽 깊이 = 횡단 patch).
**나 — 1차 안 A(사본만 없앰)**
- 범위 — 파일 7(`design_pipeline_page.ts` · `_types.ts` · `design_shared.ts` · `Cross_Refresh.ts` · `design_facility_lane.ts` · `Persist.ts` · `b_page_state.ts`) · 파이썬 · Node 묶음 없음.
- 값 영향 0 차 · 위험 낮음. 다만 소단 입구는 여전히 「구간 목록 → 측점마다 읽기」라 벽(「측점에 얹기」)과 다름. 가 의 일부(가 를 하면 나 도 됨).
**다 — 그 밖**
- 다-1 계산 뒤 계단 덧 처리(벽과 완전 같은 문) — 2-3 (ㄴ) 까닭으로 권하지 않음(면적 · 비탈 끝 · 겹침 · 여러 자리 따로 고침).
- 다-2 그대로 둠 — 값 문제 없음 · 사본 · 두 번 셈은 남음.
**권함 — 가.** 사용자 뜻(「사면안정 그룹끼리 같게」)에 맞는 것은 입구를 같게 하는 것 · 계단 넣는 시점은 성질이라 남김 · 값 0 차. 한 번에 크면 나 → 가 차례로 해도 끝은 같음.
### 2-5. 사용자 물음
- 소단을 옹벽처럼 「측점에 얹는 한 벌」(가)로 갈지 · 사본만 없앨지(나).
- 벽과 소단을 한 구간에 같이 놓을 일이 있는지 — 지금 코드로는 벽 상단 바깥 사면을 벽이 잘라 그 아래 소단은 사라짐(화면 확인 못 함).
### 2-6. 확인 못 함
- 화면 · 서버 실측 없음 — 밀리는 거리 · 면적 차는 식과 코드 읽기로만 · 실제 측점 수치 안 냄.
- 평면 발자국이 벽 트림을 모르는 것(곁 발견) — 코드 읽기만.
- 벽 + 소단 한 측점 겹침 모양 — 코드 읽기만.
@@ -579,3 +579,21 @@ CAD 시험에서 원래부터 실패하던 11건 정리 <<< 진행하고 나와
2번 진행
4번 전체적으로 사면안정 구조물군의 로직을 보고 어떻게 반영하는게 좋을지 조사.
## 요구 436 (10-29 · 사용자)
577은 횡단배수 구조물말고 옹벽같은 구조물인 ㅇ사면안정 그룹과 비교해보라고
검증 랩탑메인은 초기화해도 무관
## 요구 437 (10-29 · 사용자 · 577 답)
577은 '가'로 진행되어야함. 3d까지 연걀이되어야하고 개발순서 상 나중에 반영되다보니 누락되었던것임. 따지고 보면 옹벽과 크게 다를것고 없는데 소단들어가는 부분을 자르고 수평선과 거기에 따른 신규 성토선이 들어오면 되는것임
## 요구 438 (10-29 · 사용자)
1.위에 흠에 대한 개선 방향성이 문제임. 이전에는 서버와 브라우져의 계산값이 저장시 다를것을 대비하여 저장시 브라우져의 사용자 입력값을 서버로 젘달하게 했던걸로 기억함. 동일한게 반영할건지
2. 평면 비탈끝은 구조물이 있는경우 어떻게할건지 설몉
## 요구 439 (10-29 · 사용자 · 438 답)
진행
@@ -35,8 +35,6 @@ const STUBS = {
}),
"ui_template_elements.ts": anyModule(),
"B04_PreProcess_Api_Fetch.ts": anyModule(),
// 소단 사본(467) — 끌어오는 줄이 import.meta 를 써 이 로더로 못 읽음 · 소단 몫은 test_272_berm_source(vite 묶음)
"B06_Section_UI_Page_Persist.ts": anyModule({ bermSpansFromStructures: () => [] }),
};
const resolve = (base) =>
+14 -37
View File
@@ -1,7 +1,7 @@
/* 467 헬퍼 — 소단 원천 한 벌: 구조물 → 소단 구간(TS) · 저장분 소단 제원(TS) · 시설 줄이 세션 사본을 씀 ·
* 다시 셈 = 사본 없으면 저장분 `design.berm`.
/* 467 · 577-2 헬퍼 — 소단 원천 한 벌: 구조물 → 소단 구간(TS) · 측점 얹기(TS) · 저장분 소단 제원(TS) ·
* 다시 셈 = 얹힌 `section.berm` · 칸 없으면 저장분 `design.berm`.
* node helper_272_berm_source.mjs <묶음.mjs> <job.json>
* job = { standard, rockDefault, structureCases, storedCases, ground } · 짝 = `test_272_berm_source.py`.
* job = { standard, rockDefault, structureCases, storedCases, ground, chainages } · 짝 = `test_272_berm_source.py`.
*/
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
@@ -45,34 +45,12 @@ const out = {};
out.spans = job.structureCases.map((list) => m.bermSpansFromStructures(list));
out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec));
/* 2. 시설 줄이 사본을 씀 — 구조물 받기 전 안 씀 · 같으면 안 씀 · 바뀌면 한 번 */
{
m.setPageRouteId(P, R);
const key = m.stateKey("berm", P, R);
const count = () => writes.filter((k) => k === key).length;
const copy = () => m.readState("berm", P, R);
const lane = m.createFacilityLane(P, () => null);
const steps = {};
lane.setTypes([]);
lane.follow(); // 저장분 · 가게 구조물 없음 = 아직 모름
steps.beforeKnown = { copy: copy(), writes: count() };
const saved = job.structureCases[0];
lane.setStored(saved);
lane.set({ structures: saved }, false);
steps.loaded = { copy: copy(), writes: count() };
lane.set({ structures: saved.map((item) => ({ ...item })) });
lane.set({ pipes: [] });
steps.same = { writes: count() };
const moved = saved.map((item) =>
item.type_id === "berm" ? { ...item, start_m: item.start_m + 5, end_m: item.end_m + 5 } : item,
);
lane.set({ structures: moved });
steps.moved = { copy: copy(), writes: count() };
lane.set({ structures: [] });
steps.removed = { copy: copy(), writes: count() };
lane.dispose();
out.lane = steps;
}
/* 2. 측점 얹기 — 구조물마다 측점 목록에 얹은 칸(덮는 소단 없음 = null · 누가거리 없음 = 칸 없음) */
out.attach = job.structureCases.map((list) => {
const sections = job.chainages.map((c) => ({ chainage_m: c }));
m.attachBermSpecs(sections, m.bermSpansFromStructures(list));
return sections;
});
/* 2-2. 시설 줄 칠함 — 종류 표 받기 전 측점(빈 목록)을 종단도에 안 씀 · 받은 뒤 한 번 */
{
@@ -132,10 +110,9 @@ out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec));
],
balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 },
};
const run = async (copy) => {
if (copy === undefined) sessionStorage.removeItem(m.stateKey("berm", P, R));
else m.writeState("berm", copy, P, R);
const run = async (berm) => {
const detail = detailOf();
if (berm !== undefined) detail.cross_sections[0].berm = berm;
await m.refreshCrossDesigns({
projectId: P,
routeId: R,
@@ -147,9 +124,9 @@ out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec));
return { fill: design.fill_area_m2 ?? null, cut: design.cut_area_m2 ?? null };
};
out.refresh = {
noCopy: await run(undefined),
copySame: await run(m.bermSpansFromStructures(job.structureCases[0])),
copyEmpty: await run([]),
unattached: await run(undefined),
attachedSame: await run(job.storedBerm),
attachedNone: await run(null),
};
}
@@ -1,7 +1,7 @@
/* 467 시험 묶음 진입점 — 소단 원천 한 벌(시설 줄 사본 · 다시 셈 저장분 폴백 · 진짜 모듈 · vite ssr). */
/* 467 · 577-2 시험 묶음 진입점 — 소단 원천 한 벌(측점 얹기 · 다시 셈 저장분 폴백 · 진짜 모듈 · vite ssr). */
export { createFacilityLane } from "../../A00_Common/design/design_facility_lane";
export { setPageRouteId } from "../../A00_Common/design/design_page_route";
export { refreshCrossDesigns, storedBermSpec } from "../../B06_Section/B06_Section_Cross_Refresh";
export { bermSpansFromStructures } from "../../B06_Section/B06_Section_UI_Page_Persist";
export { attachBermSpecs, bermSpansFromStructures } from "../../common_util/common_util_berm_spans";
export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel";
export { readState, stateKey, writeState } from "../../A00_Common/b_page_state";
+2 -7
View File
@@ -38,13 +38,6 @@ const R = 6;
const AT = 40;
m.writeState("std-cross", job.standard, P);
m.rememberRockBoundaryDefault(P, job.rockDefault);
// 소단 사본 = 측점을 덮는 구간(소단 깊이가 먹게)
m.writeState(
"berm",
[{ start_m: 0, end_m: 100, width_m: 1, interval_m: 3, slope_deg: 0, depth_m: 1 }],
P,
R,
);
const canonDetail = () => ({
longitudinal: { length_m: 100, stations: [], profile_alignment: { policy: {} } },
cross_sections: [
@@ -59,6 +52,8 @@ const canonDetail = () => ({
center_y: 0,
frame: { left_xy: [0, 1] },
uphill_side: "left",
// 측점에 얹힌 소단(577-2 · 소단 깊이가 먹게)
berm: { width_m: 1, interval_m: 3, slope_deg: 0, depth_m: 1 },
samples: job.ground,
design: {
design_elevation_m: 100,
+20 -10
View File
@@ -80,8 +80,24 @@ STUBS["B05_Profile_Api_Replan"] = {
station_snap: false,
}),
};
// ⑪ 소단 프로젝트 = 저장 구조물에 소단 한 구간(577-2 · 측점에 얹힌 제원으로 셈)
const BERM_SPAN = { from: 30, to: 70 };
STUBS["B05_Profile_Api_Structures"] = {
fetchStructures: async () => ({ structures: [] }),
fetchStructures: async (projectId) => ({
structures:
projectId === "proj-365c"
? [
{
structure_id: "berm-1",
type_id: "berm",
placement: "interval",
start_m: BERM_SPAN.from,
end_m: BERM_SPAN.to,
options: { width_m: 1.5, interval_m: 5, slope_deg: 60, depth_m: 1 },
},
]
: [],
}),
fetchStructureTypes: async () => job.types,
};
STUBS["B05_Profile_Api_TerrainGrid"] = {
@@ -421,17 +437,11 @@ async function run() {
pageMod.disposePagePipeline(Q);
page.clearDrafts(Q);
// ⑪ 초안 소단(365-5) — 소단 = 지금 구조물 구간(bermAt) · 쪽 깊이 = 세션 고침(측점 열쇠) → 묶음 cross
// ⑪ 초안 소단(365-5) — 소단 = 지금 구조물 구간(측점에 얹힌 제원 · 577-2) · 쪽 깊이 = 세션 고침(측점 열쇠) → 묶음 cross
const B = "proj-365c";
window.localStorage.setItem("frd_current_project_id", B);
const span = { from: 30, to: 70 };
const bBridge = pageMod.routeDraftBridge(B, link(), {
stationIntervalM: job.interval,
bermAt: () => (c) =>
c >= span.from && c <= span.to
? { width_m: 1.5, interval_m: 5, slope_deg: 60, depth_m: 1 }
: null,
});
const span = BERM_SPAN;
const bBridge = pageMod.routeDraftBridge(B, link(), { stationIntervalM: job.interval });
pageMod.pipelineEarthwork(B, { conversion: job.conversion });
const bPipe = pageMod.pagePipeline(B);
await settle(runs().length + 1);
@@ -69,10 +69,6 @@ const savedStations = m.buildStations(savedLine(), savedCurves(), pipes, structu
});
const crossSource = m.pipelineCrossSource(P, detail, savedStations, R);
const edits = m.readAlignment(detail.longitudinal).edits;
const bermAt = (list) => {
const spans = m.bermSpansFromStructures(Array.isArray(list) ? list : []);
return (chainageM) => m.bermSpecAt(spans, chainageM);
};
const crossStage = m.createCrossStage({ grid: () => ({ grid }) });
const pipe = m.createDesignPipeline({
stages: {
@@ -119,7 +115,6 @@ const pipe = m.createDesignPipeline({
ctx.inputs.cross,
ctx.out.ground?.stations ?? [],
m.shapeStatus(ctx.inputs.pipes, ctx.inputs.structures, types),
bermAt(ctx.inputs.structures),
),
},
}),
@@ -30,7 +30,6 @@ export {
applyStructureAreaRows,
structureAreaRows,
} from "../../B06_Section/logic/B06_Section_Structure_Layouts";
export { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist";
export { parseDtmGrid } from "../../common_util/common_util_surface_sampler";
export { computeMassHaul } from "../../common_util/common_util_mass_haul";
export { writeState, writeStateRaw } from "../../A00_Common/b_page_state";
@@ -49,7 +49,6 @@ refreshDesigns(
rockOffsets: [],
rockDefault: job.rockDefault,
cutSlopes: [],
bermSpans: null,
bermDepths: [],
choices: [],
shoulderExtends: [],
@@ -183,14 +183,9 @@ await sync.handleDesignChange(20, { two_stage_slope: true }).catch(() => undefin
out.choice = Object.fromEntries(m.crossDesignChoices("pD", 7));
await sync.handleDesignChange(20, {}).catch(() => undefined);
out.choiceAfterEmpty = Object.fromEntries(m.crossDesignChoices("pD", 7));
// 소단 사본(시설 줄이 열 때 구조물에서 씀) — 떠남 확인에 안 잡힘 · 진짜 고침은 잡힘
// 결과 통 칸(계산 묶음 결과) — 떠남 확인에 안 잡힘 · 진짜 고침은 잡힘(소단 사본 칸은 걷음 · 577-2)
sessionStorage.clear();
m.writeState(
"berm",
[{ start_m: 0, end_m: 40, width_m: 1, interval_m: 5, slope_deg: 45, depth_m: 1 }],
"pB",
7,
);
m.writeState("design-stations", [{ chainage_m: 0 }], "pB");
out.dirtyBermOnly = m.designDirty("pB");
m.writeCrossDesignChoice("pB", 7, 20, { ditch_choice: false });
out.dirtyWithEdit = m.designDirty("pB");
@@ -35,8 +35,6 @@ const STUBS = {
}),
"ui_template_elements.ts": anyModule(),
"B04_PreProcess_Api_Fetch.ts": anyModule(),
// 소단 사본(467) — 끌어오는 줄이 import.meta 를 써 이 로더로 못 읽음 · 소단 몫은 test_272_berm_source(vite 묶음)
"B06_Section_UI_Page_Persist.ts": anyModule({ bermSpansFromStructures: () => [] }),
};
const resolve = (base) =>
+50 -26
View File
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
"""467 (요구 272 · 분석 4장 (B2)) — 소단 원천 한 벌.
"""467 · 577-2 (요구 272 · 434 · 437) — 소단 원천 한 벌 = 측점에 얹은 제원(옹벽처럼).
못박는 것
- 소단 사본(세션 `berm`) 쓰는 자리 = 페이지 시설 줄 하나 — 구조물 받기 전 안 씀 ·
같으면 안 씀 · 바뀌면 한 번 · 지우면 빈 목록(B05 만 열어도 씀 · 횡단 보기는 안 씀).
- 다시 셈 = 사본 없으면 저장분 `design.berm`(파이썬 `load_berm_spans` 규칙) —
B05 만 연 값 = B06 연 뒤 값 = [저장] 서버 값(`enforce_design_lines`) · 빈 사본 = 소단 없음.
- 측점 얹기 — 구조물 → 측점마다 `section.berm`(덮는 소단 없음 = null · 겹치면 먼저 놓은 것) ·
TS `attachBermSpecs` ↔ 파이썬 `attach_berm_specs`.
- 다시 셈 = 얹힌 제원 · 칸 없음(아직 안 얹음) = 저장분 `design.berm` · null = 소단 없음 —
얹힌 값 = 저장분 값 = [저장] 서버 값(`enforce_design_lines`).
- 거울 — TS `bermSpansFromStructures` ↔ 파이썬 `load_berm_spans` ·
TS `storedBermSpec` ↔ 파이썬 `berm_from`.
- 세션 사본(`berm` 칸 · 시설 줄 `syncBerm`) 없음.
Node 헬퍼 `helper_272_berm_source.mjs` 가 vite ssr 로 묶은 진짜 모듈을 부름.
"""
@@ -25,7 +26,10 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B06_Section.B06_Section_Engine_Structures_Wall import load_berm_spans # noqa: E402
from B06_Section.B06_Section_Engine_Structures_Wall import ( # noqa: E402
attach_berm_specs,
load_berm_spans,
)
from B06_Section.B06_Section_Router_Design import berm_from, enforce_design_lines # noqa: E402
from config.config_system import ( # noqa: E402
STANDARD_CROSS_SECTION,
@@ -69,6 +73,7 @@ GROUND = [
for o in range(-40, 41)
]
STORED_BERM = {"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0}
CHAINAGES = [0.0, 10.0, 25.0, 30.0, 35.0, 55.0, 65.0, 100.0, 100.5, None]
LONG = {
"design_profiles": [
{
@@ -126,6 +131,7 @@ def out(tmp_path_factory):
"storedCases": STORED_CASES,
"ground": GROUND,
"storedBerm": STORED_BERM,
"chainages": CHAINAGES,
},
ensure_ascii=False,
),
@@ -163,25 +169,27 @@ def test_거울_저장분_소단_제원(out):
assert ts == pytest.approx(want), spec
def test_시설_줄이_사본을_씀(out):
lane = out["lane"]
assert lane["beforeKnown"] == {"copy": None, "writes": 0} # 구조물 받기 전 = 안 씀
assert lane["loaded"]["writes"] == 1
assert lane["loaded"]["copy"] == out["spans"][0] # 저장 구조물 소단 구간
assert lane["same"]["writes"] == 1 # 같은 구간 · 관만 알림 = 안 씀
assert lane["moved"]["writes"] == 2
assert lane["moved"]["copy"][0]["start_m"] == 5.0 and lane["moved"]["copy"][0]["end_m"] == 105.0
assert lane["removed"] == {
"copy": [],
"writes": 3,
} # 소단 지움 = 빈 사본(저장분 소단으로 안 물러섬)
def test_거울_측점_얹기(out, tmp_path):
for index, (structures, ts) in enumerate(zip(STRUCTURE_CASES, out["attach"], strict=True)):
spans = load_berm_spans(_project(tmp_path / f"a{index}", structures))
py = [{"chainage_m": c} for c in CHAINAGES]
attach_berm_specs(py, spans)
assert [("berm" in r) for r in ts] == [("berm" in r) for r in py], index
for got, exp in zip(ts, py, strict=True):
if exp.get("berm") is None:
assert got.get("berm") is None, (index, got, exp)
else:
assert got["berm"] == pytest.approx(exp["berm"]), (index, got, exp)
first = out["attach"][0]
assert first[1]["berm"] == pytest.approx(STORED_BERM) and first[8]["berm"] is None
assert "berm" not in first[-1] # 누가거리 없음 = 칸 없음
def test_다시_셈_B05만_B06_저장_같은_값(out, tmp_path):
r = out["refresh"]
no_copy, same, empty = r["noCopy"], r["copySame"], r["copyEmpty"]
assert no_copy["fill"] == same["fill"] and no_copy["cut"] == same["cut"] # B05 만 = B06 연 뒤
assert empty["fill"] != no_copy["fill"] # 빈 사본 = 소단 없음(저장분으로 안 물러섬)
no_copy, same, empty = r["unattached"], r["attachedSame"], r["attachedNone"]
assert no_copy["fill"] == same["fill"] and no_copy["cut"] == same["cut"] # 안 얹음 = 저장분
assert empty["fill"] != no_copy["fill"] # 얹었는데 없음 = 소단 없음(저장분으로 안 물러섬)
# [저장] = 서버가 구조물 정본으로 다시 셈
root = _project(tmp_path / "save", SAVED)
design = {"ground_type": "soil", "section_mode": "both_fill", "ditch_type": "standard"}
@@ -193,11 +201,27 @@ def test_다시_셈_B05만_B06_저장_같은_값(out, tmp_path):
assert abs(bare[0]["design"]["fill_area_m2"] - empty["fill"]) < 1e-3
def test_쓰는_자리_하나():
sync = (ROOT / "B06_Section" / "B06_Section_UI_Page_Design_Sync.ts").read_text(encoding="utf-8")
assert "writeBermSpans" not in sync and "useStructureIds(projectId, structures)" in sync
lane = (ROOT / "A00_Common" / "design" / "design_facility_lane.ts").read_text(encoding="utf-8")
assert "writeBermSpans(projectId, routeId, next)" in lane
def test_사본_없음():
for rel in (
"A00_Common/design/design_facility_lane.ts",
"A00_Common/b_page_state.ts",
"B06_Section/B06_Section_UI_Page_Persist.ts",
"B06_Section/B06_Section_Cross_Refresh.ts",
"A00_Common/design/design_shared.ts",
):
text = (ROOT / rel).read_text(encoding="utf-8")
for word in (
"writeBermSpans",
"syncBerm",
"putBerm",
'readState<BermSpan[]>("berm"',
"bermAt:",
):
assert word not in text, (rel, word)
shape = (ROOT / "common_util" / "common_util_structure_shape_keys.ts").read_text(
encoding="utf-8"
)
assert '"berm"' in shape
def test_시설_줄_종류_표_전_측점_안_칠함(out):
+5 -1
View File
@@ -188,10 +188,14 @@ def test_every_shape_lands(ts: dict) -> None:
"revetment",
"pavement",
"underdrain",
"berm",
} <= keys
assert any(section.get("berm") for section in ts["cases"]["all"]) # 소단 = 측점에 얹음(577-2)
old = ts["cases"]["all"][1] # 옛 물넘이 자리(5 m) — 걷힘
assert "ford_pavement" not in old
assert all(len(section) == 1 for section in ts["cases"]["empty"])
# 구조물 없음 = 소단 「얹었는데 없음」(null)만
assert all(set(section) == {"chainage_m", "berm"} for section in ts["cases"]["empty"])
assert all(section["berm"] is None for section in ts["cases"]["empty"])
def test_blank_chips(ts: dict) -> None:
@@ -19,9 +19,9 @@ if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import ( # noqa: E402
final_slope_end,
route_frame,
side_is_cut,
slope_end,
slope_pieces,
)
@@ -93,6 +93,9 @@ def _stations() -> list[dict]:
"samples": samples,
}
)
# 벽 측점(578-2) — B06 이 얹은 구조물 처리 뒤 비탈 끝이 설계선 끝보다 앞섬
stations[5]["design"]["slope_end_left_m"] = 4.2
stations[6]["design"]["slope_end_right_m"] = -3.1
stations.append({**stations[3], "chainage_m": 70.0, "design": {"road_edges": None}})
stations.append({**stations[4], "chainage_m": 90.0, "samples": []}) # 지반 없음
return stations
@@ -107,13 +110,13 @@ def _py_record(station: dict) -> dict | None:
right_edge = float(edges["right"]["offset_m"])
except (KeyError, TypeError, ValueError):
return None
line, samples = design.get("design_line") or [], station["samples"] or []
samples = station["samples"] or []
record = {
"chainage_m": float(station["chainage_m"]),
"left_edge_m": left_edge,
"left_end_m": slope_end(line, samples, left_edge, "left"),
"left_end_m": final_slope_end(design, samples, left_edge, "left"),
"right_edge_m": right_edge,
"right_end_m": slope_end(line, samples, right_edge, "right"),
"right_end_m": final_slope_end(design, samples, right_edge, "right"),
}
for side in ("left", "right"):
elevation = edges[side].get("elevation_m")
+1 -1
View File
@@ -104,6 +104,6 @@ def test_세션_고름은_바꾼_칸끼리_얹음(out):
assert out["choiceAfterEmpty"] == out["choice"]
def test_소단_사본은_떠남_확인에_안_잡힘(out):
def test_결과_통은_떠남_확인에_안_잡힘(out):
assert out["dirtyBermOnly"] is False
assert out["dirtyWithEdit"] is True
+7 -6
View File
@@ -3,7 +3,8 @@
못박는 것
- 477-3 조작 놓은 직후(찍기 기다림 400 ms 안) ↶ 켜짐 · 단추 맞춤 알림 · 바뀐 것 없는 누름은 안 켜짐.
- 기다림 중 ↶ = 먼저 찍고 되돌림(한 번에 조작 전) · ↷ = 다시 · 서버 다시 셈 뒤 쓰기 묶음은 그대로 한 걸음.
- 477-4 `berm` = 결과 통(사진 · 떠남 물음 밖) · 떠남 제외 목록에 사본 줄 없음.
- 477-4 결과 통 칸은 사진 밖 · 소단 사본 칸 `berm` 은 걷음(577-2 · 측점에 얹힌 제원) ·
떠남 목록에 줄 없음.
./venv/Scripts/python.exe -m pytest resources/tester/test_477_undo_now_berm.py -q
"""
@@ -41,7 +42,7 @@ const { createProfileHistory, onPageHistory } = loadTs(process.argv[4]);
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const PIPES = "aislo:draft:pipes:p1";
const CROSS = "aislo:draft:crossdesign:p1:7";
const BERM = "aislo:result:berm:p1:7";
const BERM = "aislo:result:design-stations:p1";
const out = {};
let changes = 0;
onPageHistory({ change: () => (changes += 1) });
@@ -81,7 +82,7 @@ sessionStorage.setItem(PIPES, "b");
lift();
await wait(200);
sessionStorage.setItem(CROSS, "y");
sessionStorage.setItem(BERM, '[{"s":1}]'); // 사본 = 결과 통 — 사진 밖
sessionStorage.setItem(BERM, '[{"s":1}]'); // 결과 통 칸 — 사진 밖
await wait(300);
h2.undo();
out.grouped = [sessionStorage.getItem(PIPES), sessionStorage.getItem(CROSS)];
@@ -128,15 +129,15 @@ def test_기다림_중_되돌리기_먼저_찍고_한_걸음(result) -> None:
def test_서버_다시_셈_묶음_그대로(result) -> None:
assert result["grouped"] == ["a", "x"] and result["groupedLeft"] is False
assert result["bermKept"] == '[{"s":1}]' # 결과 통 사본은 되돌리기가 안 건드림
assert result["bermKept"] == '[{"s":1}]' # 결과 통 칸은 되돌리기가 안 건드림
def test_바뀐_것_없는_누름은_안_켜짐(result) -> None:
assert result["idleCanUndo"] is False
def test_소단_사본은_결과_통() -> None:
def test_소단_사본_칸_없음() -> None:
state = (ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8")
assert re.search(r'^\s*berm: \{ bucket: "result", scope: "route" \}', state, re.M)
assert not re.search(r"^\s*berm: \{", state, re.M) # 577-2 — 측점에 얹힌 제원(사본 걷음)
leave = (ROOT / "A00_Common" / "design" / "design_leave.ts").read_text(encoding="utf-8")
assert '"berm"' not in leave
+3 -3
View File
@@ -57,9 +57,9 @@ def test_자리마다_판_열쇠_저장_길_안_부름():
assert 'calcOnServerNow("quantity")' in refresh and '"F7.saved"' in refresh
shared = _text("A00_Common/design/design_shared.ts")
assert 'calcChecked("quantity", "structure.layouts"' in shared
assert 'calcChecked("quantity", "cross.berm_spans"' in shared
lane = _text("A00_Common/design/design_facility_lane.ts")
assert '"C4.copy", bermRequest(structures)' in lane
# 소단 = 측점에 얹힌 제원(577-2) — 사본 · 초안 구간 셈 부름 없음
assert '"cross.berm_spans"' not in shared
assert '"C4.copy"' not in _text("A00_Common/design/design_facility_lane.ts")
mass = _text("B05_Profile/B05_Profile_UI_Profile_MassHaul.ts")
assert 'calcFromServer<MassHaulSeries[]>("quantity", "masshaul.series"' in mass
assert "openCalcSites(projectId)" in _text("A00_Common/design/design_page.ts")
@@ -0,0 +1,100 @@
"""578-2 평면 비탈 끝 = 횡단 최종 모양(구조물 처리 뒤)이 땅과 만나는 점.
- 벽(옹벽) 측점 — `structure.layouts`(서버 Node 묶음 · B06 `STRUCTURE_ROW_KEYS`)가
`slope_end_<쪽>_m` 을 얹음 · 벽 앞(성토 위 이음 → 전면 발끝)이 땅과 만나는 자리
· 설계선 끝보다 안쪽
- 구조물 없는 측점 · 벽 반대쪽 — 키 없음(평면 선 전후 0 차)
- 파이썬 짝 `final_slope_end` — 키가 있으면 그 값 · 없으면 설계선 끝
./venv/Scripts/python.exe -m pytest resources/tester/test_578_2_plan_slope_end.py -q
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanSlope import ( # noqa: E402
final_slope_end,
slope_end,
)
from common_util.common_util_calc_runner import BUNDLE, run_calc # noqa: E402
def _ground(offset: float) -> float:
"""좌(+)로 내려가는 지반 — 좌 = 쌓기 · 우 = 깎기."""
return 100.0 - 0.5 * offset
def _section(chainage: float, wall: bool) -> dict:
offsets = [-20.0 + 0.5 * i for i in range(81)]
line = []
for o in offsets:
if -2.5 <= o <= 2.5:
z = 100.0
elif o > 2.5: # 쌓기 1:1.5 → 10 m 에서 땅
z = max(100.0 - (o - 2.5) / 1.5, _ground(o))
else: # 깎기 1:1 → -5 m 에서 땅
z = min(100.0 + (-2.5 - o), _ground(o))
line.append({"offset_m": o, "elevation_m": z})
section = {
"chainage_m": chainage,
"center_x": 0.0,
"center_y": chainage,
"samples": [{"offset_m": o, "elevation_m": _ground(o), "valid": True} for o in offsets],
"design": {
"design_elevation_m": 100.0,
"section_mode": "right_cut",
"road_edges": {
"left": {"offset_m": 2.5, "elevation_m": 100.0},
"right": {"offset_m": -2.5, "elevation_m": 100.0},
},
"design_line": line,
"cut_area_m2": 1.0,
"fill_area_m2": 1.0,
},
}
if wall:
section["revetment"] = {
"structure_id": "w1",
"type_id": "retaining_wall",
"name": "옹벽",
"start_m": 0.0,
"end_m": 20.0,
"anchor_m": 10.0,
"form": "콘크리트",
"height_m": 3.0,
"side": "좌",
}
return section
@pytest.mark.skipif(not BUNDLE.is_file(), reason="design_calc 묶음 없음(npm run build:design-calc)")
def test_벽_측점만_비탈_끝을_얹음() -> None:
sections = [_section(0.0, False), _section(10.0, True), _section(20.0, False)]
results = run_calc(["structure.layouts"], {"detail": {"cross_sections": sections}})
assert results is not None, "Node 묶음 실패"
rows = {row["chainage_m"]: row for row in results["structure.layouts"]["areas"]}
plain = slope_end(sections[1]["design"]["design_line"], sections[1]["samples"], 2.5, "left")
assert plain == pytest.approx(10.0)
end = rows[10.0]["slope_end_left_m"]
assert 2.5 < end < plain, f"벽이 자른 끝({end})이 설계선 끝({plain}) 안쪽이어야"
assert "slope_end_right_m" not in rows[10.0], "벽 반대쪽은 그대로"
for chainage in (0.0, 20.0):
assert not any(key.startswith("slope_end_") for key in rows.get(chainage, {}))
def test_파이썬_짝은_얹은_값을_먼저() -> None:
section = _section(10.0, False)
design, samples = section["design"], section["samples"]
assert final_slope_end(design, samples, 2.5, "left") == pytest.approx(10.0)
assert final_slope_end({**design, "slope_end_left_m": 4.25}, samples, 2.5, "left") == 4.25
assert final_slope_end({**design, "slope_end_left_m": None}, samples, 2.5, "left") == (
pytest.approx(10.0)
)