feat(계산묶음): 346-1 · 346-2 남은 단계 붙임 — cross · masshaul · haul · drainage · corridor · 노선 초안 동안 유토곡선 = 새 노선 횡단
- design_pipeline_page: 단계 다섯 한 줄씩(부르기만) · 단면 유형 기본값 = 새 지형 상단측(서버 노선 저장 규칙) · haul · corridor 는 노선 초안 동안만 · 열 때 한 번 셈(발자국 바로 보임) - 노선 초안 동안 종단도 = 새 노선 상세(pipelineDetail — 지반 · 측점 · 횡단 면적 → 유토곡선) · 저장 노선 횡단 다시 셈 안 함 - 횡단 입력 = 저장 상세의 측점 입력을 측점 열쇠로(design_pipeline_cross · 넘기는 칸 = 서버 CARRY_INPUT_KEYS 와 같음) - 저장 관 목록(좌표 · id) = 관 입력 바탕 · 첫 계산도 저장 노선 기준으로 좌표 따라감(createFollowStage) - 노선 초안 동안 계획고 = alignment-draft 노선 0 칸(DRAFT_ROUTE_ID) · 초안 버리면 비움 · [저장] 이음 = sub5 - 상단측 짝(logic/B05_Profile_Pipeline_Detail uphillSide ↔ Engine_Sections_Core uphill_side) · 측점 열쇠 = common_util_station_key 한 벌 - 시험 test_346_2_pipeline_cross_page 14 · 옛 시험 둘 새 꼴에 맞춤(317 · 230_2) · 346-1 도움 파일 창구 셋 가짜 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
⚠ 파이썬·TS 짝 — `generate_sections` 측점 부분(격자 · 스냅 · 구조물 측점 병합 · 종류 · 방위각) ·
|
||||
`_plan_radii` · `_curve_widenings` · `_design_curve_widenings` ↔ `B05_Profile/logic/B05_Profile_Sections_Stations.ts`
|
||||
(`buildStations`). 거울 시험 `resources/tester/test_344_3_pipeline_stations_mirror.py`.
|
||||
⚠ 파이썬·TS 짝 — `generate_sections` 상단측(`uphill_side`) ↔ `B05_Profile/logic/B05_Profile_Pipeline_Detail.ts`
|
||||
(`uphillSide`). 거울 시험 `resources/tester/test_346_2_pipeline_cross_page.py`.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_Pipeline_Detail.ts
|
||||
* 계산 묶음(PLAN 346-1 · 346-2) 출력 → 종단도 · 유토곡선이 읽는 상세 꼴(`SectionDetailResponse`).
|
||||
* 노선 초안 동안 종단도가 저장 노선 상세 대신 이 상세를 그림(지반 · 측점 · 횡단 면적이 새 노선 것).
|
||||
*
|
||||
* uphillSide 측점 상단측(좌 · 우 지반 평균이 높은 쪽) — 서버 단면 유형 기본값(`{쪽}_cut`) 재료.
|
||||
* ⚠ ① 짝 — `B05_Profile_Engine_Sections_Core.py` `uphill_side` · 거울 시험
|
||||
* `resources/tester/test_346_2_pipeline_cross_page.py`.
|
||||
* crossWithUphill cross 단계 입력에 단면 유형 기본값(상단측)을 얹음 — 서버 노선 저장이 단면 유형 ·
|
||||
* 측구 방향을 새 지형으로 정하는 것과 같은 규칙(넘기는 칸 = 부르는 쪽 입력).
|
||||
* draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 얹힌 시설 제원).
|
||||
*
|
||||
* 순수 — 화면 · fetch · 세션 없음.
|
||||
* ========================================================================== */
|
||||
|
||||
import type {
|
||||
CrossSection,
|
||||
SectionDetailResponse,
|
||||
SectionStation,
|
||||
} from "../../B06_Section/B06_Section_Api_Types";
|
||||
import type {
|
||||
CrossOutput,
|
||||
GroundOutput,
|
||||
PipelineStation,
|
||||
} from "../../A00_Common/design/design_pipeline_types";
|
||||
import type { ProfileAlignment } from "../B05_Profile_UI_Profile_Alignment";
|
||||
import { stationLabel } from "../../common_util/common_util_station";
|
||||
import { stationIdOf } from "./B05_Profile_Pipeline_Grade";
|
||||
|
||||
/** 측점에 얹힌 시설 제원(저장 상세에서 측점 열쇠로 옮김) — 낡음 판정 · 그림이 읽음. */
|
||||
export type SectionSpecs = Partial<
|
||||
Pick<
|
||||
CrossSection,
|
||||
| "structure"
|
||||
| "culvert"
|
||||
| "ford"
|
||||
| "box"
|
||||
| "ford_pavement"
|
||||
| "revetment"
|
||||
| "pavement"
|
||||
| "underdrain"
|
||||
>
|
||||
>;
|
||||
|
||||
/** cross 단계 입력 꼴(`B06_Section_Cross_Stage` `CrossStageInput`) — 칸은 그 단계 몫. */
|
||||
export interface CrossInput {
|
||||
standard: unknown;
|
||||
byKey: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
const mean = (values: number[]): number => values.reduce((sum, v) => sum + v, 0) / values.length;
|
||||
|
||||
/** 상단측 — 좌 · 우 유효 지반 평균이 높은 쪽 · 한쪽이 비거나 차이가 1e-6 안이면 null(서버와 같음). */
|
||||
export function uphillSide(station: PipelineStation): "left" | "right" | null {
|
||||
const valid = (side: "left" | "right"): number[] =>
|
||||
(station.ground?.[side] ?? [])
|
||||
.filter((sample) => sample.offsetM > 1e-9 && sample.z !== null && Number.isFinite(sample.z))
|
||||
.map((sample) => sample.z as number);
|
||||
const left = valid("left");
|
||||
const right = valid("right");
|
||||
if (!left.length || !right.length) return null;
|
||||
const gap = mean(left) - mean(right);
|
||||
if (Math.abs(gap) <= 1e-6) return null;
|
||||
return gap > 0 ? "left" : "right";
|
||||
}
|
||||
|
||||
/** 측점마다 단면 유형 기본값(상단측 깎기 · 모르면 좌) — 입력이 정한 단면 유형은 그대로. */
|
||||
export function crossWithUphill(
|
||||
input: CrossInput | null,
|
||||
stations: ReadonlyArray<PipelineStation>,
|
||||
): CrossInput | null {
|
||||
if (!input) return null;
|
||||
const byKey: CrossInput["byKey"] = {};
|
||||
for (const station of stations)
|
||||
byKey[station.key] = {
|
||||
sectionMode: `${uphillSide(station) ?? "left"}_cut`,
|
||||
...input.byKey[station.key],
|
||||
};
|
||||
return { standard: input.standard, byKey };
|
||||
}
|
||||
|
||||
const kindOf = (station: PipelineStation): SectionStation["kind"] =>
|
||||
station.key === "end:BP"
|
||||
? "bp"
|
||||
: station.key === "end:EP"
|
||||
? "ep"
|
||||
: station.kind === "grid"
|
||||
? "regular"
|
||||
: "irregular";
|
||||
|
||||
/** 묶음 출력 → 상세. 저장 상세의 종단 머리(이름 · 계획선 묶음)는 그대로 · 측점 · 지반 · 횡단은 새 노선. */
|
||||
export function draftSectionDetail(
|
||||
saved: SectionDetailResponse,
|
||||
ground: GroundOutput,
|
||||
profile: ProfileAlignment,
|
||||
cross: CrossOutput | null,
|
||||
specs: ReadonlyMap<string, SectionSpecs>,
|
||||
intervalM: number,
|
||||
): SectionDetailResponse {
|
||||
const sections: CrossSection[] = ground.stations.map((station) => {
|
||||
const own = specs.get(station.key) ?? {};
|
||||
const center = station.ground?.centerZ ?? null;
|
||||
const head: SectionStation = {
|
||||
station_id: stationIdOf(station.chainageM),
|
||||
chainage_m: station.chainageM,
|
||||
label: stationLabel(station.chainageM, intervalM),
|
||||
kind: kindOf(station),
|
||||
...(own.structure ? { structure: own.structure } : {}),
|
||||
center_z: center,
|
||||
azimuth_deg: station.azimuthDeg,
|
||||
plan_radius_m: station.planRadiusM,
|
||||
curve_outer_side: station.curveOuterSide,
|
||||
curve_widening_m: station.curveWideningM,
|
||||
center_x: station.x,
|
||||
center_y: station.y,
|
||||
uphill_side: uphillSide(station),
|
||||
frame: { left_xy: station.left },
|
||||
};
|
||||
const sample = (offset: number, z: number | null) => ({
|
||||
offset_m: offset,
|
||||
elevation_m: z,
|
||||
valid: z !== null && Number.isFinite(z),
|
||||
});
|
||||
const samples = [
|
||||
...[...(station.ground?.right ?? [])].reverse().map((s) => sample(-s.offsetM, s.z)),
|
||||
sample(0, center),
|
||||
...(station.ground?.left ?? []).map((s) => sample(s.offsetM, s.z)),
|
||||
];
|
||||
const design = cross?.byKey[station.key]?.detail as CrossSection["design"] | undefined;
|
||||
return { ...head, ...own, samples, ...(design ? { design } : {}), preview: true };
|
||||
});
|
||||
const { chainageM, z } = ground.longitudinal;
|
||||
return {
|
||||
longitudinal: {
|
||||
...saved.longitudinal,
|
||||
length_m: chainageM[chainageM.length - 1] ?? 0,
|
||||
samples: chainageM.map((chainage, index) => ({
|
||||
chainage_m: chainage,
|
||||
elevation_m: z[index],
|
||||
z: z[index],
|
||||
valid: z[index] !== null,
|
||||
})),
|
||||
stations: sections.map(({ samples: _s, design: _d, preview: _p, ...head }) => head),
|
||||
profile_alignment: profile,
|
||||
},
|
||||
cross_sections: sections,
|
||||
balloon_offsets: saved.balloon_offsets,
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* `A00_Common/design/design_pipeline_types.ts`. 셈은 이미 있는 짝을 부르기만(어댑터):
|
||||
*
|
||||
* follow — 관 · 구조물 좌표 따라감(`common_util_route_follow`) · 계획고 편집 옮김(`Grade_Carry`
|
||||
* `carryProfileEdits` 의 edits 몫) · 옛 노선(`before.route`)이 있을 때만.
|
||||
* `carryProfileEdits` 의 edits 몫 · 옛 노선 `before.route` 가 있을 때만).
|
||||
* planline — 노선 초안이 없으면 저장 종단 기준 그대로 · 있으면 새 지반 첫 계획선(`Grade_Ground`)으로
|
||||
* 바뀐 구간만 다시 맞추고 밖은 저장 꼭짓점을 밂(`carryProfileEdits` 의 basePvi 몫).
|
||||
* profile — `buildAlignment`(첫 계획선 + 계획고 편집) — 종단 보기가 쓰는 같은 함수.
|
||||
@@ -72,24 +72,32 @@ export function gradeRange(
|
||||
};
|
||||
}
|
||||
|
||||
/** follow — 노선이 바뀐 차례에 한 번 · 관 · 구조물 좌표 따라감 · 계획고 편집 옮김. */
|
||||
export const followStage: StageFn<"follow"> = (ctx) => {
|
||||
const before = ctx.before.route;
|
||||
const now = ctx.out.route;
|
||||
if (!before || !now) return {};
|
||||
const line = xy(now.line);
|
||||
const patch: FollowPatch = {};
|
||||
const { pipes, structures, alignment } = ctx.inputs;
|
||||
if (Array.isArray(pipes) && pipes.length)
|
||||
patch.pipes = projectPipePoints(pipes as FollowingPoint[], line);
|
||||
if (Array.isArray(structures) && structures.length)
|
||||
patch.structures = followStructures(structures as FollowingStructure[], line).map(
|
||||
(moved) => moved.item,
|
||||
);
|
||||
const span = changedSpan(xy(before.line), line);
|
||||
if (span && alignment) patch.alignment = carryProfileEdits([], alignment, span, []).edits;
|
||||
return patch;
|
||||
};
|
||||
/**
|
||||
* follow — 노선이 바뀐 차례에 한 번 · 관 · 구조물 좌표 따라감 · 계획고 편집 옮김. 첫 계산(옛 노선 없음)은
|
||||
* 저장 노선(`savedLine`)을 옛 노선으로 보고 좌표만 따라감(투영은 몇 번 해도 같음 · 계획고 옮김은 안 함 —
|
||||
* 열 때의 계획고 초안은 이미 그 노선 기준).
|
||||
*/
|
||||
export function createFollowStage(deps: { savedLine(): Vertex[] | null }): StageFn<"follow"> {
|
||||
return (ctx) => {
|
||||
const before = ctx.before.route?.line ?? deps.savedLine();
|
||||
const now = ctx.out.route;
|
||||
if (!before || !now) return {};
|
||||
const line = xy(now.line);
|
||||
const patch: FollowPatch = {};
|
||||
const { pipes, structures, alignment } = ctx.inputs;
|
||||
if (Array.isArray(pipes) && pipes.length)
|
||||
patch.pipes = projectPipePoints(pipes as FollowingPoint[], line);
|
||||
if (Array.isArray(structures) && structures.length)
|
||||
patch.structures = followStructures(structures as FollowingStructure[], line).map(
|
||||
(moved) => moved.item,
|
||||
);
|
||||
const span = ctx.before.route ? changedSpan(xy(before), line) : null;
|
||||
if (span && alignment) patch.alignment = carryProfileEdits([], alignment, span, []).edits;
|
||||
return patch;
|
||||
};
|
||||
}
|
||||
|
||||
export const followStage = createFollowStage({ savedLine: () => null });
|
||||
|
||||
export interface PlanlineDeps {
|
||||
/** 저장 노선의 종단 기준(측점 한 벌 · 종단 보기 `listed`) — 없으면 아직 못 셈. */
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type PlanStructure,
|
||||
type PlanType,
|
||||
} from "../../common_util/common_util_structure_station";
|
||||
import { stationKey as commonStationKey } from "../../common_util/common_util_station_key";
|
||||
import type {
|
||||
DesignCurve,
|
||||
PipelineStation,
|
||||
@@ -66,15 +67,18 @@ const round = (value: number, digits: number): number => Number(value.toFixed(di
|
||||
export const stationText = (chainageM: number): string => chainageM.toFixed(2);
|
||||
|
||||
/** 측점 열쇠 — 격자 = `grid:k`(k = 원래 격자 번호 · 노선 모양이 바뀌어도 같은 번호) · 관 = `pipe:<id>` ·
|
||||
* 구조물 = `structure:<id>`(구간형 시작 · 끝은 `:start` · `:end`) · 시종점 = `end:BP` · `end:EP`. */
|
||||
* 구조물 = `structure:<id>`(구간형 시작 · 끝은 `:start` · `:end`) · 시종점 = `end:BP` · `end:EP`.
|
||||
* 규칙 한 벌 = `common_util_station_key`(파이썬 짝) — 여기는 꼴만 옮김. */
|
||||
export function stationKey(
|
||||
kind: StationKind,
|
||||
ref: number | string,
|
||||
role: "start" | "anchor" | "end" = "anchor",
|
||||
): string {
|
||||
if (kind === "grid") return `grid:${ref}`;
|
||||
if (kind === "end") return `end:${ref}`;
|
||||
return `${kind}:${ref}${role === "anchor" ? "" : `:${role}`}`;
|
||||
if (kind === "end") return commonStationKey(ref === "BP" ? "bp" : "ep", 0, null);
|
||||
if (kind === "grid") return commonStationKey("regular", Number(ref), 1); // ref = 격자 번호(간격 1)
|
||||
const base = commonStationKey(kind, 0, null, String(ref));
|
||||
// ponytail: 구간형 꼬리 — sub2 가 공용 함수에 넣으면 그 인자로 옮김
|
||||
return role === "anchor" ? base : `${base}:${role}`;
|
||||
}
|
||||
|
||||
/** 짝: `curve_widening_m` — 직선 · 45 m 이상 · 값 없음은 0. */
|
||||
|
||||
Reference in New Issue
Block a user