Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -73,7 +73,6 @@ export interface StructureSpanControl {
|
||||
/** 이 카드가 구간값을 만질 대상 측점 — 링크면 소유 측점, 아니면 자기 자신. */
|
||||
ownerOf: (section: CrossSection) => CrossSection | null;
|
||||
valuesFor: (section: CrossSection, role: SpanRole) => SpanValues | null;
|
||||
update: (section: CrossSection, role: SpanRole, patch: Partial<SpanValues>) => void;
|
||||
/** 다단 한 단의 구간값 — 기준벽과 달리 **단마다 따로** 잡는다(2026-08-29 사용자).
|
||||
* 값은 세션에 담기고 [저장]·[확정] 때 정본으로 간다. 없으면 기본 10m(5/5). */
|
||||
tierValuesFor: (section: CrossSection, key: RevetKey) => SpanValues;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createCadHost, type CadHostDrawing } from "@ui/cad_host/cad_host";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
@@ -45,7 +46,19 @@ export function readStructureNoteCads(
|
||||
return new Map(Object.entries(raw ?? {}).map(([key, cad]) => [Number(key), cad]));
|
||||
}
|
||||
|
||||
/** 표 위 CAD 요소 한도 — 서버 `CrossNoteCad.entities max_length` 짝(넘으면 횡단 patch 통째 422 · 433-20). */
|
||||
export const NOTE_CAD_MAX_ENTITIES = 2000;
|
||||
|
||||
function writeNoteCad(section: CrossSection, cad: CrossNoteCad, at: NoteCadPlace): void {
|
||||
// 한도 넘음 = 초안에 안 씀(앞 초안 그대로) · 알림
|
||||
if (cad.entities.length > NOTE_CAD_MAX_ENTITIES) {
|
||||
showToast(
|
||||
`표 위 CAD 요소 ${cad.entities.length}개 — 한도 ${NOTE_CAD_MAX_ENTITIES}개를 넘어 저장하지 않습니다. 요소를 줄이세요.`,
|
||||
"warning",
|
||||
8000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 캐시 설계에도 얹음 — B05 [저장](`crossPatchesFromCache`)이 같은 값을 실음
|
||||
if (section.design) section.design.structure_note_cad = cad;
|
||||
const all = readState<Record<string, CrossNoteCad>>(
|
||||
|
||||
@@ -194,13 +194,9 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
|
||||
const role = spanRoleOf(key, ctx.inletIsBasin());
|
||||
return role && ctx.structureSpan ? ctx.structureSpan.valuesFor(ctx.section, role) : null;
|
||||
},
|
||||
// 구간 줄 = 다단 벽만(기준벽 · 집수정은 폼 칸 = 관 초안 · 433-19)
|
||||
setSpan: (key, patch) => {
|
||||
if (isTierKey(key)) {
|
||||
ctx.structureSpan?.updateTier(ctx.section, key, patch);
|
||||
return;
|
||||
}
|
||||
const role = spanRoleOf(key, ctx.inletIsBasin());
|
||||
if (role) ctx.structureSpan?.update(ctx.section, role, patch);
|
||||
if (isTierKey(key)) ctx.structureSpan?.updateTier(ctx.section, key, patch);
|
||||
},
|
||||
// 벽을 **그리지 않는 카드에는 [연동]을 내지 않는다**(2026-09-06). 링크 카드는 소유
|
||||
// 측점의 벽을 빌려 그리는데, 빌릴 벽이 없는 카드에도 버튼이 떠 눌러도 아무 일이
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* 구조물 **구간값(길이·전/후)** 제어 — 다단 단별 구간값과 유입·유출·집수정 구간값.
|
||||
*
|
||||
* `B06_Section_UI_Page_Station_Controls` 에서 떼어냈다(700줄 제한, 2026-09-02).
|
||||
* 세션 맵(`extraSpans`)과 캐시(design)·정본 큐(`culvertOptions`)를 함께 만지는
|
||||
* 자리라 한 덩어리로 옮겼다. 판정 규칙은 종전대로 `_Cross_Culvert_Const` 몫이다.
|
||||
* 세션 맵(`extraSpans`)과 캐시(design)를 함께 만지는 자리라 한 덩어리로 옮겼다.
|
||||
* 기준벽 · 집수정 구간값은 폼 칸(관 초안)이 정본 — 여기는 읽기만(433-19 · 444).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CrossDesign, CrossSection, StoredWallSpan } from "./B06_Section_Api_Fetch";
|
||||
@@ -25,8 +25,6 @@ export interface SpanControlDeps {
|
||||
extraSpans: Map<string, SpanValues>;
|
||||
persistExtraSpans: () => void;
|
||||
patchCachedDesign: (chainageM: number, patch: Partial<CrossDesign>) => void;
|
||||
/** 정본 반영 큐(관 옵션). */
|
||||
culvertOptions: { queue: (chainageM: number, values: Record<string, number>) => void };
|
||||
}
|
||||
|
||||
/** 구간값 제어기와 단별 구간값 조회를 만든다. */
|
||||
@@ -36,7 +34,7 @@ export function createSpanControl(deps: SpanControlDeps): {
|
||||
tierSpansOf: (owner: CrossSection) => Record<string, StoredWallSpan>;
|
||||
spanKeyOf: (chainageM: number, wall: string) => string;
|
||||
} {
|
||||
const { extraSpans, persistExtraSpans, patchCachedDesign, ownerOf, culvertOptions } = deps;
|
||||
const { extraSpans, persistExtraSpans, patchCachedDesign, ownerOf } = deps;
|
||||
const spanKeyOf = (chainageM: number, wall: string): string => `${chainageM.toFixed(2)}:${wall}`;
|
||||
|
||||
const storedTierSpan = (owner: CrossSection, wall: string): SpanValues => {
|
||||
@@ -91,40 +89,6 @@ export function createSpanControl(deps: SpanControlDeps): {
|
||||
const owner = ownerOf(section);
|
||||
return owner ? CulvertConst.spanValuesOf(owner, role) : null;
|
||||
},
|
||||
update: (section, role, patch) => {
|
||||
const owner = ownerOf(section);
|
||||
if (!owner?.culvert) return;
|
||||
const current = CulvertConst.spanValuesOf(owner, role);
|
||||
if (!current) return;
|
||||
// 길이↔전/후 산식은 다단과 공용(`applySpanPatch`).
|
||||
const { lengthM, beforeM, afterM } = CulvertConst.applySpanPatch(current, patch);
|
||||
const spec = role === "outlet" ? owner.culvert.outlet : owner.culvert.inlet;
|
||||
const keys = CulvertConst.SPAN_OPTION_KEYS[role];
|
||||
if (role === "basin") {
|
||||
spec.basin_length_m = lengthM;
|
||||
spec.basin_before_m = beforeM;
|
||||
spec.basin_after_m = afterM;
|
||||
} else {
|
||||
spec.revet_length_m = lengthM;
|
||||
spec.revet_before_m = beforeM;
|
||||
spec.revet_after_m = afterM;
|
||||
}
|
||||
culvertOptions.queue(owner.chainage_m, {
|
||||
[keys.length]: lengthM,
|
||||
[keys.before]: beforeM,
|
||||
[keys.after]: afterM,
|
||||
});
|
||||
// 연장이 바뀌면 링크되는 옆 측점 목록이 달라진다. 다시 그릴 대상은 **옛 연장과
|
||||
// 새 연장을 합친 구간**뿐이다 — 측점 23개를 통째로 다시 그리면 조정창이 매번
|
||||
// 새로 만들어져 연타한 +/-가 중간에 삼켜진다(2026-08-24 화면 실측: 3번 눌러
|
||||
// 2번만 반영).
|
||||
const reach = Math.max(current.beforeM, current.afterM, beforeM, afterM);
|
||||
for (const other of deps.detail()?.cross_sections ?? []) {
|
||||
if (Math.abs(other.chainage_m - owner.chainage_m) <= reach + 1e-9) {
|
||||
deps.refreshCard(other.chainage_m);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return { control, tierSpansOf, spanKeyOf };
|
||||
|
||||
@@ -505,7 +505,6 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
extraSpans,
|
||||
persistExtraSpans,
|
||||
patchCachedDesign,
|
||||
culvertOptions,
|
||||
});
|
||||
const structureSpanControl = spanControl.control;
|
||||
const spanKeyOf = spanControl.spanKeyOf;
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
* B06_Section_Station_Swap.ts
|
||||
* 측점 수가 바뀔 때 횡단 목록 갈아끼움(PLAN 346-2 4파) — 계산 묶음 측점(`PipelineStation[]`) → 새 상세 객체.
|
||||
*
|
||||
* · 같은 측점(열쇠 · 없으면 같은 자리 같은 종류)은 앞 횡단을 이어 받음 — 설계 · 구조물 제원 · 이름 그대로 ·
|
||||
* 자리(누가거리 · 좌표 · 방위 · 확폭)와 지반 표본(채워 왔으면)만 새것.
|
||||
* · 같은 측점(열쇠 · MC 가 움직여 바뀐 곡선 열쇠 `curveMoves` · 없으면 같은 자리 같은 종류)은 앞 횡단을 이어
|
||||
* 받음 — 설계 · 구조물 제원 · 이름 그대로 · 자리(누가거리 · 좌표 · 방위 · 확폭)만 새것(433-4).
|
||||
* · 지반 표본 · 상단측 · 중심고 = 자리 · 축이 같은 앞 횡단이면 앞 값(서버 샘플링 정본 · 덧 측점만 넣어도 나머지
|
||||
* 측점 면적이 브라우저 지형으로 안 바뀜 · 433-11) · 새 측점 · 옮긴 측점만 지반 표본(채워 왔으면).
|
||||
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
|
||||
* · 상단측 = 새 지반 표본으로 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
|
||||
* · 상단측 = 새 지반 표본으로 다시 셈한 측점은 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
|
||||
* · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자 칸(만진 칸 · 지반 등 입력 `ROW_CARRY_KEYS`) — 서버가
|
||||
* 그 행을 지우고 기본 설계에 [저장] patch 를 얹음(`sync_cross_section_rows` · `compute_default_designs` · 428) ·
|
||||
* patch 가 그 칸을 모두 실음(`crossPatchesFromCache` · 445 H2).
|
||||
@@ -20,6 +22,7 @@
|
||||
import type { PipelineStation } from "../../A00_Common/design/logic/design_pipeline_types";
|
||||
import type { CrossSection, SectionDetailResponse, SectionStation } from "../B06_Section_Api_Fetch";
|
||||
import { uphillSide } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import { curveMoves } from "../../common_util/common_util_station_key";
|
||||
|
||||
interface Sample {
|
||||
offset_m: number;
|
||||
@@ -141,9 +144,21 @@ export interface StationSwap {
|
||||
moved: boolean;
|
||||
}
|
||||
|
||||
/** 앞 횡단과 자리(누가거리 · 좌표 0.01 m) · 축(왼쪽 단위 벡터)이 같은가 — 같으면 앞 지반 표본을 그대로 씀. */
|
||||
function samePlace(prior: CrossSection, station: PipelineStation): boolean {
|
||||
const [lx, ly] = prior.frame?.left_xy ?? [Number.NaN, Number.NaN];
|
||||
return (
|
||||
Math.abs(prior.chainage_m - station.chainageM) < 0.01 &&
|
||||
Math.hypot(prior.center_x - station.x, prior.center_y - station.y) <= 0.01 &&
|
||||
Math.abs(lx - station.left[0]) < 1e-4 &&
|
||||
Math.abs(ly - station.left[1]) < 1e-4
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 측점 목록으로 상세를 새로 짬. `oldKeys` = 지금 상세 측점마다 열쇠(같은 차례 · `stationKeysOf`) ·
|
||||
* 열쇠가 안 맞으면 같은 자리(0.01 m) · 같은 종류의 앞 횡단을 씀.
|
||||
* 열쇠가 안 맞으면 옮긴 곡선 열쇠(`curveMoves` · 서버 `carry_cross_inputs` 같은 규칙) · 그다음 같은 자리(0.01 m) ·
|
||||
* 같은 종류의 앞 횡단을 씀.
|
||||
*/
|
||||
export function swapCrossSections(
|
||||
detail: SectionDetailResponse,
|
||||
@@ -157,8 +172,18 @@ export function swapCrossSections(
|
||||
const added: string[] = [];
|
||||
const moves = new Map<string, string>();
|
||||
const text = (chainageM: number) => chainageM.toFixed(2);
|
||||
// 새 곡선 열쇠 → 옛 곡선 열쇠(R 바꿔 MC 가 움직임 · 433-4)
|
||||
const curveFrom = new Map(
|
||||
[
|
||||
...curveMoves(
|
||||
detail.cross_sections.map((section, i) => [oldKeys[i], section.chainage_m] as const),
|
||||
stations.map((station) => [station.key, station.chainageM] as const),
|
||||
),
|
||||
].map(([from, to]) => [to, from]),
|
||||
);
|
||||
const priorOf = (station: PipelineStation): CrossSection | undefined => {
|
||||
const hit = byKey.get(station.key);
|
||||
const from = curveFrom.get(station.key);
|
||||
const hit = byKey.get(station.key) ?? (from === undefined ? undefined : byKey.get(from));
|
||||
if (hit && !used.has(hit)) return hit;
|
||||
return detail.cross_sections.find(
|
||||
(section) =>
|
||||
@@ -182,6 +207,8 @@ export function swapCrossSections(
|
||||
moved = true;
|
||||
if (prior && text(prior.chainage_m) !== text(station.chainageM))
|
||||
moves.set(text(prior.chainage_m), text(station.chainageM));
|
||||
// 자리 같은 앞 횡단 = 서버 표본 그대로(433-11) · 아니면 새 지반 표본(채워 왔으면)
|
||||
const fresh = !!station.ground && !(prior?.samples?.length && samePlace(prior, station));
|
||||
const kind = kindOf(station);
|
||||
const onGrid =
|
||||
kind === "irregular" &&
|
||||
@@ -193,7 +220,9 @@ export function swapCrossSections(
|
||||
label: formatStation(station.chainageM),
|
||||
kind,
|
||||
...(onGrid ? { grid: true } : {}),
|
||||
center_z: station.ground?.centerZ ?? prior?.center_z ?? null,
|
||||
center_z: fresh
|
||||
? (station.ground?.centerZ ?? prior?.center_z ?? null)
|
||||
: (prior?.center_z ?? null),
|
||||
azimuth_deg: station.azimuthDeg,
|
||||
plan_radius_m: station.planRadiusM,
|
||||
curve_outer_side: station.curveOuterSide,
|
||||
@@ -203,8 +232,8 @@ export function swapCrossSections(
|
||||
frame: { left_xy: [station.left[0], station.left[1]] as [number, number] },
|
||||
};
|
||||
longStations.push({ ...place, ...(prior?.structure ? { structure: prior.structure } : {}) });
|
||||
const samples = station.ground ? groundSamples(station) : (prior?.samples ?? []);
|
||||
const uphill = station.ground ? uphillSide(station) : (prior?.uphill_side ?? null);
|
||||
const samples = fresh ? groundSamples(station) : (prior?.samples ?? []);
|
||||
const uphill = fresh ? uphillSide(station) : (prior?.uphill_side ?? null);
|
||||
if (!prior) return { ...place, uphill_side: uphill, samples, design: freshDesign(uphill) };
|
||||
const reseed =
|
||||
(station.kind === "pipe" || station.kind === "structure") &&
|
||||
|
||||
@@ -113,6 +113,7 @@ const NEW = (dy) => [
|
||||
kinds: swap.detail.cross_sections.map((s) => s.kind),
|
||||
labels: [swap.detail.cross_sections[2].label, swap.detail.cross_sections[7].label],
|
||||
samples: swap.detail.cross_sections[1].samples.map((s) => s.offset_m),
|
||||
movedSamples: swap.detail.cross_sections[2].samples.map((s) => s.offset_m),
|
||||
oldUntouched:
|
||||
before.cross_sections.length === 7 && before.cross_sections[3].chainage_m === 45.054,
|
||||
length: swap.detail.longitudinal.length_m,
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
// 433-4 · 433-11 — 측점 갈아끼움(`swapCrossSections` · 진짜) · 횡단 엔진(`computeCrossDesign` · 진짜) ·
|
||||
// 유토(`computeMassHaul` · 진짜). 인자 = 표준단면 json · 토량환산계수 json.
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
const idOf = (m) => `station_${String(Math.round(m * 1000)).padStart(12, "0")}`;
|
||||
const STUBS = {
|
||||
B05_Profile_Pipeline_Grade: { stationIdOf: idOf },
|
||||
B05_Profile_UI_Profile_Data: { toDesignProfile: () => ({}) },
|
||||
};
|
||||
const cache = new Map();
|
||||
function load(full) {
|
||||
if (cache.has(full)) return cache.get(full).exports;
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box);
|
||||
const req = (r) => {
|
||||
const name = r.split("/").pop();
|
||||
if (STUBS[name]) return STUBS[name];
|
||||
if (r.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${r.slice(6)}.ts`));
|
||||
if (r.startsWith(".")) return load(path.resolve(path.dirname(full), `${r}.ts`));
|
||||
throw new Error(`stub 없음: ${r}`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
return box.exports;
|
||||
}
|
||||
const swap = load(path.join(ROOT, "B06_Section/logic/B06_Section_Station_Swap.ts"));
|
||||
const { computeCrossDesign } = load(path.join(ROOT, "common_util/common_util_cross_design.ts"));
|
||||
const { computeMassHaul } = load(path.join(ROOT, "common_util/common_util_mass_haul.ts"));
|
||||
const standard = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const conversion = JSON.parse(fs.readFileSync(process.argv[3], "utf8"));
|
||||
|
||||
const st = (key, m, kind, ground = null) => ({
|
||||
key,
|
||||
chainageM: m,
|
||||
kind,
|
||||
x: m,
|
||||
y: 0,
|
||||
left: [0, 1],
|
||||
azimuthDeg: 90,
|
||||
planRadiusM: null,
|
||||
curveOuterSide: null,
|
||||
curveWideningM: 0,
|
||||
ground,
|
||||
});
|
||||
|
||||
/* ── 433-4: R 바꿔 MC 측점이 움직임(curve:47.35 → curve:49.10) ── */
|
||||
const mcSec = (m, kind) => ({
|
||||
station_id: idOf(m),
|
||||
chainage_m: m,
|
||||
kind,
|
||||
center_x: m,
|
||||
center_y: 0,
|
||||
frame: { left_xy: [0, 1] },
|
||||
samples: [],
|
||||
uphill_side: "left",
|
||||
design: { memo: `MY-EDIT@${m}` },
|
||||
});
|
||||
const mcDetail = {
|
||||
longitudinal: { stations: [], length_m: 100 },
|
||||
cross_sections: [
|
||||
mcSec(0, "bp"),
|
||||
mcSec(20, "regular"),
|
||||
mcSec(47.35, "curve"),
|
||||
mcSec(60, "regular"),
|
||||
mcSec(100, "ep"),
|
||||
],
|
||||
};
|
||||
const mcOut = swap.swapCrossSections(
|
||||
mcDetail,
|
||||
[
|
||||
st("end:BP", 0, "end"),
|
||||
st("grid:1", 20, "grid"),
|
||||
st("curve:49.10", 49.1, "curve"),
|
||||
st("grid:3", 60, "grid"),
|
||||
st("end:EP", 101, "end"),
|
||||
],
|
||||
["end:BP", "grid:1", "curve:47.35", "grid:3", "end:EP"],
|
||||
20,
|
||||
);
|
||||
// 멀리(> CURVE_NEAR_M) 간 MC = 못 이음(새 측점 · 노선 밖)
|
||||
const farOut = swap.swapCrossSections(
|
||||
mcDetail,
|
||||
[
|
||||
st("end:BP", 0, "end"),
|
||||
st("grid:1", 20, "grid"),
|
||||
st("curve:62.00", 62, "curve"),
|
||||
st("grid:3", 60, "grid"),
|
||||
st("end:EP", 100, "end"),
|
||||
],
|
||||
["end:BP", "grid:1", "curve:47.35", "grid:3", "end:EP"],
|
||||
20,
|
||||
);
|
||||
|
||||
/* ── 433-11: 덧 측점 넣기 · 빼기 — 서버 표본(A) 대 브라우저 지형(B) ── */
|
||||
const OFFSETS = [-12, -9, -6, -4, -2, 0, 2, 4, 6, 9, 12];
|
||||
const groundA = (m, o) => 100 + 0.02 * m + 0.35 * o; // 서버 샘플링(저장 상세)
|
||||
const groundB = (m, o) => groundA(m, o) + 0.6 * Math.sin(m / 7 + o / 3); // 브라우저 설계 지형
|
||||
const planZ = (m) => 100.2 + 0.02 * m;
|
||||
const samplesOf = (fn, m) =>
|
||||
OFFSETS.map((o) => ({ offset_m: o, elevation_m: fn(m, o), valid: true }));
|
||||
const groundOf = (fn, m) => ({
|
||||
centerZ: fn(m, 0),
|
||||
left: OFFSETS.filter((o) => o > 0).map((o) => ({ offsetM: o, z: fn(m, o) })),
|
||||
right: OFFSETS.filter((o) => o < 0).map((o) => ({ offsetM: -o, z: fn(m, o) })),
|
||||
});
|
||||
const designOf = (samples, m) => ({
|
||||
...computeCrossDesign(samples, planZ(m), {
|
||||
standard,
|
||||
groundType: "soil",
|
||||
sectionMode: "left_cut",
|
||||
}),
|
||||
design_elevation_m: planZ(m),
|
||||
});
|
||||
const baseKeys = [
|
||||
["end:BP", 0, "end"],
|
||||
["grid:1", 20, "grid"],
|
||||
["grid:2", 40, "grid"],
|
||||
["grid:3", 60, "grid"],
|
||||
["end:EP", 80, "end"],
|
||||
];
|
||||
const kindOf = (kind, key) =>
|
||||
kind === "end" ? (key.endsWith("EP") ? "ep" : "bp") : kind === "grid" ? "regular" : "irregular";
|
||||
const savedSec = ([key, m, kind]) => {
|
||||
const samples = samplesOf(groundA, m);
|
||||
return {
|
||||
station_id: idOf(m),
|
||||
chainage_m: m,
|
||||
kind: kindOf(kind, key),
|
||||
center_x: m,
|
||||
center_y: 0,
|
||||
center_z: groundA(m, 0),
|
||||
frame: { left_xy: [0, 1] },
|
||||
uphill_side: "left",
|
||||
samples,
|
||||
design: designOf(samples, m),
|
||||
};
|
||||
};
|
||||
const saved = {
|
||||
longitudinal: { stations: [], length_m: 80 },
|
||||
cross_sections: baseKeys.map(savedSec),
|
||||
};
|
||||
const savedKeys = baseKeys.map(([key]) => key);
|
||||
// 브라우저 다시 셈 = 상세 표본으로 면적(Cross_Refresh 꼴)
|
||||
const recross = (detail) =>
|
||||
detail.cross_sections.map((s) => ({ ...s, design: designOf(s.samples, s.chainage_m) }));
|
||||
const cumulative = (sections) =>
|
||||
computeMassHaul(sections, conversion).points.map((p) => [
|
||||
p.chainage_m,
|
||||
Math.round(p.cumulative_volume_m3 * 1000) / 1000,
|
||||
]);
|
||||
|
||||
const plusList = [...baseKeys.slice(0, 2), ["pipe:p30", 30, "pipe"], ...baseKeys.slice(2)];
|
||||
const plusStations = plusList.map(([key, m, kind]) => st(key, m, kind, groundOf(groundB, m)));
|
||||
const added = swap.swapCrossSections(saved, plusStations, savedKeys, 20);
|
||||
const addedSections = recross(added.detail);
|
||||
// 서버 기준 = 저장 상세(표본 A) + 덧 측점(브라우저 표본 B · 새 측점은 표본이 그것뿐)
|
||||
const serverPlus = [...saved.cross_sections];
|
||||
serverPlus.splice(2, 0, addedSections[2]);
|
||||
const backStations = baseKeys.map(([key, m, kind]) => st(key, m, kind, groundOf(groundB, m)));
|
||||
const removed = swap.swapCrossSections(
|
||||
added.detail,
|
||||
backStations,
|
||||
plusList.map(([key]) => key),
|
||||
20,
|
||||
);
|
||||
// 노선이 옮겨 간 측점(좌표 다름)은 새 지형 표본
|
||||
const shifted = swap.swapCrossSections(
|
||||
saved,
|
||||
baseKeys.map(([key, m, kind]) => ({ ...st(key, m, kind, groundOf(groundB, m)), y: 1.5 })),
|
||||
savedKeys,
|
||||
20,
|
||||
);
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
mc: {
|
||||
added: mcOut.added,
|
||||
removed: mcOut.removed,
|
||||
moves: [...mcOut.moves],
|
||||
memo: mcOut.detail.cross_sections[2].design.memo ?? null,
|
||||
stationId: mcOut.detail.cross_sections[2].station_id,
|
||||
far: { added: farOut.added, removed: farOut.removed },
|
||||
},
|
||||
plus: {
|
||||
keptSamples: added.detail.cross_sections
|
||||
.filter((_, i) => i !== 2)
|
||||
.every(
|
||||
(s, i) => JSON.stringify(s.samples) === JSON.stringify(saved.cross_sections[i].samples),
|
||||
),
|
||||
keptCenterZ: added.detail.cross_sections.filter((_, i) => i !== 2).map((s) => s.center_z),
|
||||
savedCenterZ: saved.cross_sections.map((s) => s.center_z),
|
||||
newSamples:
|
||||
JSON.stringify(added.detail.cross_sections[2].samples) ===
|
||||
JSON.stringify(samplesOf(groundB, 30)),
|
||||
browser: cumulative(addedSections),
|
||||
server: cumulative(serverPlus),
|
||||
back: cumulative(recross(removed.detail)),
|
||||
saved: cumulative(saved.cross_sections),
|
||||
// 고치기 전 꼴(모든 측점 브라우저 표본) — 차가 큼을 보임
|
||||
allBrowser: cumulative(
|
||||
plusList.map(([, m], i) => ({
|
||||
...addedSections[i],
|
||||
design: designOf(samplesOf(groundB, m), m),
|
||||
})),
|
||||
),
|
||||
shiftedFresh: shifted.detail.cross_sections.every(
|
||||
(s, i) => JSON.stringify(s.samples) === JSON.stringify(samplesOf(groundB, baseKeys[i][1])),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -5,7 +5,8 @@ const path = require("path");
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
const idOf = (m) => `station_${String(Math.round(m * 1000)).padStart(12, "0")}`;
|
||||
const REAL = /(B05_Profile_Pipeline_Detail|B06_Section_Station_Swap|design_pipeline_cross)$/;
|
||||
const REAL =
|
||||
/(B05_Profile_Pipeline_Detail|B06_Section_Station_Swap|design_pipeline_cross|common_util_station_key)$/;
|
||||
const STUBS = {
|
||||
B05_Profile_Pipeline_Grade: { stationIdOf: idOf },
|
||||
common_util_station: { stationLabel: (m) => String(m) },
|
||||
|
||||
@@ -96,7 +96,9 @@ def test_셋_늘고_둘_빠진_목록으로_갈아끼움(out):
|
||||
"ep",
|
||||
]
|
||||
assert pure["labels"] == ["STA.0+047.000", "STA.0+090.000"]
|
||||
assert pure["samples"] == [-0.5, 0, 0.5]
|
||||
# 자리 같은 앞 단면 = 앞 표본(서버 정본 · 433-11) · 옮긴 측점 = 새 지반 표본
|
||||
assert pure["samples"] == [0]
|
||||
assert pure["movedSamples"] == [-0.5, 0, 0.5]
|
||||
assert pure["oldUntouched"] is True and pure["length"] == 90
|
||||
assert pure["same"] is False and pure["sameSelf"] is True
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""433-11 — 덧 측점 넣기 · 빼기 뒤 브라우저 누가토량 = 서버(0.1 ㎥ 안).
|
||||
|
||||
뿌리: 갈아끼움이 안 바뀐 측점까지 지반 표본 · 상단측 · 중심고를 브라우저 지형 값으로 갈음.
|
||||
못박는 것
|
||||
- 자리 · 축이 같은 앞 단면 = 서버 표본 · 중심고 그대로 · 새 측점만 브라우저 표본.
|
||||
- 덧 측점 넣은 뒤 누가토량 = 서버 기준(저장 표본 + 새 측점) · 빼면 저장 값 그대로.
|
||||
- 노선이 옮겨 간 측점(좌표 다름)은 새 지형 표본.
|
||||
- 고치기 전 꼴(모든 측점 브라우저 표본)은 1 ㎥ 넘게 어긋남(시험이 차를 잼).
|
||||
|
||||
Node 헬퍼 `helper_433_swap.cjs` — 진짜 갈아끼움 · 횡단 엔진 · 유토.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||||
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS # noqa: E402
|
||||
|
||||
TOL_M3 = 0.1
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def plus(tmp_path_factory):
|
||||
work = tmp_path_factory.mktemp("b433_11")
|
||||
standard = work / "standard.json"
|
||||
conversion = work / "conversion.json"
|
||||
standard.write_text(json.dumps(STANDARD_CROSS_SECTION), encoding="utf-8")
|
||||
conversion.write_text(json.dumps(EARTHWORK_CONVERSION_FACTORS), encoding="utf-8")
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_433_swap.cjs"), str(standard), str(conversion)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)["plus"]
|
||||
|
||||
|
||||
def _max_gap(a, b):
|
||||
assert [p[0] for p in a] == [p[0] for p in b]
|
||||
return max(abs(x[1] - y[1]) for x, y in zip(a, b))
|
||||
|
||||
|
||||
def test_안_바뀐_측점은_서버_표본_중심고(plus):
|
||||
assert plus["keptSamples"]
|
||||
assert plus["keptCenterZ"] == plus["savedCenterZ"]
|
||||
assert plus["newSamples"]
|
||||
|
||||
|
||||
def test_덧_측점_넣은_뒤_누가토량_서버와_같음(plus):
|
||||
assert _max_gap(plus["browser"], plus["server"]) < TOL_M3
|
||||
|
||||
|
||||
def test_덧_측점_뺀_뒤_저장_값_그대로(plus):
|
||||
assert _max_gap(plus["back"], plus["saved"]) < TOL_M3
|
||||
|
||||
|
||||
def test_고치기_전_꼴은_어긋남(plus):
|
||||
assert _max_gap(plus["allBrowser"], plus["server"]) > 1.0
|
||||
|
||||
|
||||
def test_노선이_옮긴_측점은_새_지형(plus):
|
||||
assert plus["shiftedFresh"]
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""433-19 · 433-20 — 구간값 죽은 길 걷음 · 표 위 CAD 요소 한도(서버 2000 짝).
|
||||
|
||||
못박는 것
|
||||
- 구간값 제어에 기준벽 `update`(culvertopt 큐 쓰기) 없음 · 구간 줄은 다단 벽만(444 관 초안 한 벌).
|
||||
- 표 위 CAD 한도 = 서버 `CrossNoteCad.entities max_length` · 넘으면 초안에 안 쓰고 알림.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B06 = ROOT / "B06_Section"
|
||||
|
||||
|
||||
def _read(name: str) -> str:
|
||||
return (B06 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_구간값_제어에_culvertopt_큐_쓰기_없음():
|
||||
span = _read("B06_Section_UI_Page_Span_Control.ts")
|
||||
assert "culvertOptions" not in span
|
||||
assert "update: (section, role" not in span
|
||||
wire = _read("B06_Section_UI_Cross_Culvert_Wire.ts")
|
||||
assert not re.search(r"^\s*update: \(section: CrossSection, role", wire, re.M)
|
||||
view = _read("B06_Section_UI_Cross_View_Structure.ts")
|
||||
assert "structureSpan?.update(" not in view
|
||||
|
||||
|
||||
def test_CAD_한도_서버와_같음():
|
||||
schema = _read("B06_Section_Schema.py")
|
||||
server = re.search(
|
||||
r"class CrossNoteCad\(BaseModel\):.*?entities:.*?max_length=(\d+)", schema, re.S
|
||||
)
|
||||
cad = _read("B06_Section_UI_Cross_Detail_Cad.ts")
|
||||
screen = re.search(r"NOTE_CAD_MAX_ENTITIES = (\d+)", cad)
|
||||
assert server and screen and server.group(1) == screen.group(1)
|
||||
|
||||
|
||||
def test_CAD_한도_넘으면_초안에_안_씀():
|
||||
cad = _read("B06_Section_UI_Cross_Detail_Cad.ts")
|
||||
body = cad[cad.index("function writeNoteCad(") :]
|
||||
guard = body.index("NOTE_CAD_MAX_ENTITIES")
|
||||
assert guard < body.index("structure_note_cad = cad")
|
||||
assert "showToast(" in body[guard : body.index("structure_note_cad = cad")]
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""433-4 — 곡선 R 바꿔 MC 측점이 움직임(curve:47.35 → curve:49.10).
|
||||
|
||||
상세 갈아끼움이 옛 MC 횡단을 이어 받음.
|
||||
|
||||
못박는 것
|
||||
- MC 설계(사용자 칸) 남음 · 측점 id 그대로 · added · removed 빔.
|
||||
- moves 에 MC 짝(누가거리 열쇠 칸이 따라감).
|
||||
- CURVE_NEAR_M 밖으로 간 MC = 못 이음(새 측점 · 옛 열쇠 removed).
|
||||
|
||||
Node 헬퍼 `helper_433_swap.cjs` 가 진짜 모듈(`B06_Section_Station_Swap.ts`)을 부름.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||||
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory):
|
||||
work = tmp_path_factory.mktemp("b433")
|
||||
standard = work / "standard.json"
|
||||
conversion = work / "conversion.json"
|
||||
standard.write_text(json.dumps(STANDARD_CROSS_SECTION), encoding="utf-8")
|
||||
conversion.write_text(json.dumps(EARTHWORK_CONVERSION_FACTORS), encoding="utf-8")
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_433_swap.cjs"), str(standard), str(conversion)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_MC_설계가_새_곡선_열쇠로_남음(out):
|
||||
mc = out["mc"]
|
||||
assert mc["memo"] == "MY-EDIT@47.35"
|
||||
assert mc["stationId"] == "station_000000047350"
|
||||
assert mc["added"] == []
|
||||
assert mc["removed"] == []
|
||||
|
||||
|
||||
def test_moves_에_MC_짝(out):
|
||||
assert ["47.35", "49.10"] in out["mc"]["moves"]
|
||||
|
||||
|
||||
def test_멀리_간_MC_는_못_이음(out):
|
||||
far = out["mc"]["far"]
|
||||
assert far["added"] == ["curve:62.00"]
|
||||
assert far["removed"] == ["curve:47.35"]
|
||||
Reference in New Issue
Block a user