Merge remote-tracking branch 'origin/dev' into sub_laptop_4
This commit is contained in:
@@ -11,6 +11,7 @@ const ROOT = path.join(__dirname, "..", "..");
|
||||
const DATA = { pipes: [], structures: [], types: [] };
|
||||
const STUBS = {
|
||||
"@ui/cad_host/cad_host": { createCadHost: () => ({}) },
|
||||
"@ui/ui_template_elements": { showToast: () => undefined },
|
||||
"../A00_Common/b_page_state": { readState: () => null, writeState: () => undefined },
|
||||
"../A00_Common/design/design_selection": { PIPE_MATCH_M: 0.51 },
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/* 271-1~3 종단 직선화 측점 고름 · 끝 R 기본 L · 실시간 띠 단위검증 헬퍼 — `_Straighten.ts` ·
|
||||
* `_Tools.ts` · `design_profile_free.ts` · `design_profile_range_band.ts` 를 그 자리에서 트랜스파일해
|
||||
* 돌리고 파이썬 거울 입력(base · edits · TS 곡선)을 JSON 으로 낸다.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
// 화면 틀 파일(@ui 별칭)은 Node 가 못 읽음 — 도구줄 그림만 쓰므로 빈 그림으로 갈음
|
||||
const STUBS = { "./B05_Profile_UI_RouteEdit_Chrome": { HISTORY_ICON: { undo: "", redo: "" } } };
|
||||
const cache = new Map();
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
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.exports);
|
||||
const near = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const LOGIC = path.join(ROOT, "B05_Profile", "logic");
|
||||
const align = loadTs(path.join(LOGIC, "B05_Profile_UI_Profile_Alignment.ts"));
|
||||
const st = loadTs(path.join(LOGIC, "B05_Profile_UI_Profile_Straighten.ts"));
|
||||
const law = loadTs(path.join(LOGIC, "B05_Profile_Profile_CurveLaw.ts"));
|
||||
const tools = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Tools.ts"));
|
||||
const DESIGN = path.join(ROOT, "A00_Common", "design");
|
||||
const free = loadTs(path.join(DESIGN, "logic", "design_profile_free.ts"));
|
||||
const band = loadTs(path.join(DESIGN, "design_profile_range_band.ts"));
|
||||
|
||||
const policy = {
|
||||
station_interval_m: 20,
|
||||
curve_radius_ratio: 1,
|
||||
curve_tangent_max_ratio: 0.5,
|
||||
curve_skip_legal_exception: false,
|
||||
default_curve_length_m: 20,
|
||||
default_curve_radius_m: 300,
|
||||
balance_tolerance_percent: 10,
|
||||
edit_step_m: 0.1,
|
||||
grade_violation_policy: "warn",
|
||||
max_grade_pct: 14,
|
||||
curve_skip_delta_pct: 0,
|
||||
paved: true,
|
||||
min_vertical_radius_m: 100,
|
||||
min_curve_length_m: 20,
|
||||
};
|
||||
// 0~300 m · 20 m 측점 — 마디마다 기울기가 다른 전체 측점 폴리라인(서버와 같이 지정 없는 꺾임 = 기본 L)
|
||||
const S = Array.from({ length: 16 }, (_, i) => i * 20);
|
||||
const g = (s) => 100 + 0.03 * s + 0.6 * Math.sin(s / 13);
|
||||
const base = {
|
||||
policy,
|
||||
onlyExplicitCurves: false,
|
||||
basePvi: S.map((s) => ({ chainage_m: s, elevation_m: +g(s).toFixed(6) })),
|
||||
chainage: S,
|
||||
ground: S.map((s) => +g(s).toFixed(6)),
|
||||
stations: S.map((s) => ({ station_id: `S${s}`, chainage_m: s })),
|
||||
};
|
||||
const tidy = (e) => st.tidyEdits(base, e);
|
||||
const build = (e) => align.buildAlignment(base, e);
|
||||
const curves = (e) => build(e).curves.map((c) => ({ at: c.chainage_m, l: c.l_m, r: c.r_m }));
|
||||
const shortfalls = (e) => build(e).warnings.filter((w) => w.includes("미달"));
|
||||
|
||||
const out = {};
|
||||
const mirror = [];
|
||||
const keep = (name, edits) => {
|
||||
mirror.push({ name, edits, curves: curves(edits).map((c) => [c.at, c.r, c.l]) });
|
||||
return edits;
|
||||
};
|
||||
|
||||
// ① 직선화 40~200 — 사이 곡선 없음 · 양 끝 기본 L 20
|
||||
const e1 = keep("straight", tidy(st.straightenBetween(base, align.emptyEdits(), 40, 200)));
|
||||
const a1 = build(e1);
|
||||
out.straight = { curves: curves(e1), radii: Object.keys(e1.curve_radii) };
|
||||
|
||||
// ② 이미 직선인 범위 = 부분 구간 · 반대 방향 같음 · 꺾인 · 섞인 범위 = null
|
||||
out.sub = st.straightSubRun(a1, 80, 160);
|
||||
out.subReverse = st.straightSubRun(a1, 160, 80);
|
||||
out.subBent = st.straightSubRun(build(align.emptyEdits()), 40, 200);
|
||||
out.subMixed = st.straightSubRun(a1, 100, 260);
|
||||
out.subOneSpan = st.straightSubRun(a1, 80, 100);
|
||||
|
||||
// ③ 부분 구간 ▲(틸트) = 가운데 + 양 끝 L 20 · 거짓 미달 없음
|
||||
const e3 = keep("tiltSub", tidy(st.tiltStraightRun(base, e1, out.sub, 0.5)));
|
||||
out.tiltSub = { curves: curves(e3), shortfalls: shortfalls(e3) };
|
||||
// 판정 오차 짝(1e-3) — 옛 1e-6 이면 L 19.99998 이 미달로 읽힘
|
||||
out.tolerance = law.SHORTFALL_TOLERANCE_M;
|
||||
out.nearMiss = law.verticalCurveShortfalls(
|
||||
[{ chainage_m: 120, l_m: 19.99998, r_m: 800, omitted: false, skip_allowed: false }],
|
||||
policy,
|
||||
);
|
||||
out.realMiss = law.verticalCurveShortfalls(
|
||||
[{ chainage_m: 120, l_m: 19.9, r_m: 800, omitted: false, skip_allowed: false }],
|
||||
policy,
|
||||
);
|
||||
|
||||
// ④ 끝에 저장 자동 R → 지움(기본 L 20) · 사용자 R 은 둠
|
||||
const saved = { "40.000": 5330, "200.000": 1084 };
|
||||
const savedEdits = { station_offsets: {}, curve_radii: { ...saved } };
|
||||
const e4 = keep("autoEnds", tidy(st.straightenBetween(base, savedEdits, 40, 200, saved)));
|
||||
out.autoEnds = { radii: Object.keys(e4.curve_radii), curves: curves(e4) };
|
||||
const userEdits = { station_offsets: {}, curve_radii: { "40.000": 600, "200.000": 1084 } };
|
||||
const e4u = tidy(st.straightenBetween(base, userEdits, 40, 200, saved));
|
||||
out.userEnd = { radii: e4u.curve_radii };
|
||||
// 자동 R 기준 없이(옛 부름) = 그대로 남음
|
||||
out.noAuto = Object.keys(tidy(st.straightenBetween(base, savedEdits, 40, 200)).curve_radii);
|
||||
|
||||
// ⑤ BP · EP — 시종점 곡선 없음
|
||||
const e5 = keep("bpEp", tidy(st.straightenBetween(base, align.emptyEdits(), 0, 300)));
|
||||
out.bpEp = curves(e5);
|
||||
const eBP = tidy(st.straightenBetween(base, align.emptyEdits(), 0, 200));
|
||||
const subBP = st.straightSubRun(build(eBP), 0, 80);
|
||||
const e5t = keep("tiltBp", tidy(st.tiltStraightRun(base, eBP, subBP, 0.5)));
|
||||
out.tiltBp = curves(e5t).map((c) => c.at);
|
||||
|
||||
// ⑥ 곡선 섞인 범위 = 안쪽 R 지움 · 양 끝 L 20
|
||||
const mixedIn = { station_offsets: {}, curve_radii: { "120.000": 900, "140.000": 700 } };
|
||||
const e6 = keep("mixed", tidy(st.straightenBetween(base, mixedIn, 100, 260, {})));
|
||||
out.mixed = { radii: Object.keys(e6.curve_radii), curves: curves(e6) };
|
||||
|
||||
// ⑦ 자유지정 끝 자동 R 지움 · 사용자 R 둠
|
||||
const z = (s) => align.controlElevationAt(build(savedEdits), s);
|
||||
const vertices = [
|
||||
{ chainage_m: 40, elevation_m: z(40) },
|
||||
{ chainage_m: 120, elevation_m: z(120) + 1 },
|
||||
{ chainage_m: 200, elevation_m: z(200) },
|
||||
];
|
||||
const e7 = keep("freeAuto", tidy(free.buildFreeEdits(base, savedEdits, vertices, saved)));
|
||||
out.freeAuto = Object.keys(e7.curve_radii);
|
||||
out.freeUser = Object.keys(tidy(free.buildFreeEdits(base, userEdits, vertices, saved)).curve_radii);
|
||||
|
||||
// ⑧ 자유지정 range 단계 실시간 띠 · Esc = 시작만 취소
|
||||
{
|
||||
const stations = S;
|
||||
let edits = align.emptyEdits();
|
||||
const log = { closed: 0 };
|
||||
const draw = free.createFreeDraw({
|
||||
base: () => base,
|
||||
alignment: () => build(edits),
|
||||
edits: () => edits,
|
||||
apply: (next) => (edits = next),
|
||||
close: () => (log.closed += 1),
|
||||
changed: () => undefined,
|
||||
});
|
||||
draw.click(41, 0, stations);
|
||||
draw.hover({ chainageM: 117, elevationM: 0, stations });
|
||||
out.liveBand = draw.marks().band;
|
||||
out.liveEdges = draw.marks().edges;
|
||||
draw.hover({ chainageM: 13, elevationM: 0, stations });
|
||||
out.liveReverse = draw.marks().band;
|
||||
draw.hover(null);
|
||||
out.outside = { band: draw.marks().band, edges: draw.marks().edges };
|
||||
out.hintPending = draw.hint();
|
||||
out.escTaken = draw.key("Escape");
|
||||
out.afterEsc = { phase: draw.phase(), edges: draw.marks().edges, closed: log.closed };
|
||||
draw.click(41, 0, stations);
|
||||
draw.click(158, 0, stations);
|
||||
draw.hover({ chainageM: 260, elevationM: 0, stations });
|
||||
out.fixedBand = draw.marks().band;
|
||||
out.armedPhase = draw.phase();
|
||||
out.escArmed = draw.key("Escape");
|
||||
out.afterEscArmed = { phase: draw.phase(), closed: log.closed };
|
||||
}
|
||||
|
||||
// ⑩ 범위 띠 그림 — 가짜 DOM
|
||||
class El {
|
||||
constructor(tag) {
|
||||
this.tag = tag;
|
||||
this.attrs = {};
|
||||
this.children = [];
|
||||
this.parent = null;
|
||||
this.textContent = "";
|
||||
this.listeners = {};
|
||||
this.isConnected = true;
|
||||
this.classList = { toggle: () => undefined, add: () => undefined };
|
||||
}
|
||||
setAttribute(name, value) {
|
||||
this.attrs[name] = value;
|
||||
}
|
||||
append(...items) {
|
||||
items.forEach((item) => {
|
||||
item.parent = this;
|
||||
this.children.push(item);
|
||||
});
|
||||
}
|
||||
remove() {
|
||||
if (!this.parent) return;
|
||||
this.parent.children = this.parent.children.filter((c) => c !== this);
|
||||
this.parent = null;
|
||||
}
|
||||
addEventListener(type, fn, opts) {
|
||||
(this.listeners[type] ??= []).push(fn);
|
||||
opts?.signal?.addEventListener("abort", () => {
|
||||
this.listeners[type] = this.listeners[type].filter((f) => f !== fn);
|
||||
});
|
||||
}
|
||||
fire(type, event = {}) {
|
||||
(this.listeners[type] ?? []).forEach((fn) => fn(event));
|
||||
}
|
||||
getBoundingClientRect() {
|
||||
return { left: 0, top: 0 };
|
||||
}
|
||||
}
|
||||
global.document = {
|
||||
createElementNS: (_ns, tag) => new El(tag),
|
||||
createElement: (tag) => new El(tag),
|
||||
};
|
||||
const shape = (svg) =>
|
||||
svg.children.map((c) =>
|
||||
c.tag === "text"
|
||||
? `text:${c.textContent}`
|
||||
: c.tag === "rect"
|
||||
? `rect:${c.attrs.x}+${c.attrs.width}`
|
||||
: `line:${c.attrs.x1}`,
|
||||
);
|
||||
{
|
||||
const x = (m) => m * 2;
|
||||
const svg = new El("svg");
|
||||
band.appendRangeBand(svg, x, 100, 120, 40);
|
||||
out.bandShape = shape(svg);
|
||||
const one = new El("svg");
|
||||
band.appendRangeBand(one, x, 100, 40, null);
|
||||
out.bandStart = shape(one);
|
||||
|
||||
const chart = new El("div");
|
||||
const scroller = new El("div");
|
||||
const geom = {
|
||||
chart,
|
||||
scroller,
|
||||
x,
|
||||
toChainage: (px) => px / 2,
|
||||
widthPx: 640,
|
||||
heightPx: 100,
|
||||
stations: S,
|
||||
};
|
||||
let start = null;
|
||||
const view = band.createRangeBandView((hover) => (start === null ? null : [start, hover]));
|
||||
view.mount(geom, true);
|
||||
out.viewIdle = chart.children.length;
|
||||
start = 40;
|
||||
view.redraw();
|
||||
out.viewStart = shape(chart.children[0]);
|
||||
chart.fire("mousemove", { clientX: 2 * 117 });
|
||||
out.viewHover = shape(chart.children[0]);
|
||||
chart.fire("mouseleave");
|
||||
out.viewLeave = shape(chart.children[0]);
|
||||
chart.fire("mousemove", { clientX: 2 * 61 });
|
||||
view.mount(geom, true); // 다시 그림(줌 · 스크롤 · 계획선 바뀜) 뒤에도 커서 자리 이어짐
|
||||
out.viewRemount = shape(chart.children[0]);
|
||||
view.mount(geom, false);
|
||||
out.viewOff = chart.children.length;
|
||||
}
|
||||
|
||||
// ⑨ 도구 상태 — [직선화] 부분 구간은 붉은 강조(selectedRuns) 아님 · Esc 취소 · 모드 유지
|
||||
{
|
||||
const picked = [];
|
||||
const t = tools.createProfileTools({
|
||||
onStraighten: (from, to) => picked.push([from, to]),
|
||||
onShift: () => undefined,
|
||||
onTilt: (run, delta) => picked.push(["tilt", run.fromM, run.toM, delta]),
|
||||
onResetToSaved: () => undefined,
|
||||
canResetToSaved: () => false,
|
||||
onUndo: () => undefined,
|
||||
onRedo: () => undefined,
|
||||
canUndo: () => false,
|
||||
canRedo: () => false,
|
||||
hasStraightRuns: () => true,
|
||||
onChanged: () => undefined,
|
||||
onModeChange: () => undefined,
|
||||
freeHint: () => "",
|
||||
mountOverlay: () => undefined,
|
||||
});
|
||||
const press = (label) => {
|
||||
const button = t.render().children.find((c) => c.textContent === label);
|
||||
button.fire("click", { stopPropagation: () => undefined });
|
||||
};
|
||||
const hintOf = () => t.render().children.find((c) => c.tag === "em")?.textContent ?? null;
|
||||
out.toolsNone = t.handleStationPick(40);
|
||||
press("직선화");
|
||||
t.handleStationPick(40);
|
||||
out.pending = { at: t.pendingStation(), hint: hintOf() };
|
||||
out.cancel = { taken: t.cancelPick(), pending: t.pendingStation(), mode: t.mode() };
|
||||
out.cancelAgain = t.cancelPick();
|
||||
t.handleRunPick({ fromM: 80, toM: 160, nodes: [80, 100, 120, 140, 160] });
|
||||
out.subPicked = { sub: t.subRun(), red: t.selectedRuns(), hint: hintOf() };
|
||||
press("▲");
|
||||
out.tiltCall = picked.at(-1);
|
||||
t.handleStationPick(60); // 새 시작 = 부분 구간 풂
|
||||
out.newStart = { pending: t.pendingStation(), sub: t.subRun() };
|
||||
t.handleStationPick(180);
|
||||
out.straightenCall = picked.at(-1);
|
||||
press("직선화"); // 끔
|
||||
press("쉬프트");
|
||||
t.handleRunPick({ fromM: 40, toM: 200, nodes: [40, 200] });
|
||||
out.shiftRed = { red: t.selectedRuns().length, sub: t.subRun(), cancel: t.cancelPick() };
|
||||
}
|
||||
|
||||
out.mirror = { base, cases: mirror };
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -76,6 +76,7 @@ const drafts = loadTs("B06_Section/B06_Section_Section_Drafts.ts", {
|
||||
const hit = sections.find((s) => Math.abs(s.chainage_m - pipe.chainage_m) < 0.005);
|
||||
if (hit) hit.culvert = true;
|
||||
}
|
||||
return { outside: [], far: [] };
|
||||
},
|
||||
},
|
||||
// 측점 열쇠 세우기(344-3) — 이 시험 몫 아님
|
||||
|
||||
@@ -142,6 +142,7 @@ function loadTs(target) {
|
||||
const full = path.resolve(target);
|
||||
if (STUBS.has(full)) return STUBS.get(full);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".css")) return {}; // 화면 꼴(vite 몫)
|
||||
if (full.endsWith(".json")) {
|
||||
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
cache.set(full, data);
|
||||
|
||||
@@ -41,9 +41,9 @@ const hash = load(
|
||||
);
|
||||
catalog.useCatalogDefinition({ types: input.types, ...input.definition });
|
||||
|
||||
const out = { cases: {} };
|
||||
const out = { cases: {}, misses: {} };
|
||||
for (const [name, { sections, pipes, structures }] of Object.entries(input.cases)) {
|
||||
attach.attachShapes(sections, pipes, structures, input.types);
|
||||
out.misses[name] = attach.attachShapes(sections, pipes, structures, input.types);
|
||||
out.cases[name] = sections;
|
||||
}
|
||||
|
||||
|
||||
@@ -514,7 +514,10 @@ async function run() {
|
||||
st("pipe:p28", 28, "pipe"),
|
||||
st("end:EP", 40, "end"),
|
||||
];
|
||||
const source = crossMod.pipelineCrossSource(C, screen, savedList, 7);
|
||||
// 초안 자리 관 측점(27 m) = 등록 열쇠(주인 id)로 저장 관 측점과 짝(457 · 차례 짝 없음)
|
||||
const source = crossMod.pipelineCrossSource(C, screen, savedList, 7, () =>
|
||||
savedList.map((s) => s.key),
|
||||
);
|
||||
const bare = crossMod.pipelineCrossSource(
|
||||
C,
|
||||
{ ...screen, cross_sections: screen.cross_sections.filter((x) => x.chainage_m !== 27) },
|
||||
@@ -534,6 +537,7 @@ async function run() {
|
||||
const viewFree = {
|
||||
grid20: source.input.byKey["grid:1"],
|
||||
pipeSpec: source.specs.get("pipe:p28") ?? null,
|
||||
pipeCarried: Boolean(source.input.byKey["pipe:p28"]),
|
||||
specsDiffer: source.input.specsKey !== bare.input.specsKey,
|
||||
firstStored: firstPvi(planlineOf(() => savedBase.basePvi)),
|
||||
firstShaken: firstPvi(planlineOf(undefined)),
|
||||
|
||||
@@ -9,6 +9,7 @@ const STUBS = {
|
||||
B05_Profile_Pipeline_Grade: { stationIdOf: idOf },
|
||||
B05_Profile_UI_Profile_Data: { toDesignProfile: () => ({}) },
|
||||
};
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
const cache = new Map();
|
||||
function load(full) {
|
||||
if (cache.has(full)) return cache.get(full).exports;
|
||||
@@ -18,6 +19,7 @@ function load(full) {
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box);
|
||||
const req = (r) => {
|
||||
if (CATALOG.isCatalogRequest(r)) return CATALOG; // 형상 얹기(Pipeline_Detail · 457)가 끌어감
|
||||
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`));
|
||||
|
||||
@@ -6,7 +6,8 @@ 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|common_util_station_key)$/;
|
||||
/(B05_Profile_Pipeline_Detail|B06_Section_Station_Swap|design_pipeline_cross|common_util_\w+)$/;
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
const STUBS = {
|
||||
B05_Profile_Pipeline_Grade: { stationIdOf: idOf },
|
||||
common_util_station: { stationLabel: (m) => String(m) },
|
||||
@@ -27,6 +28,9 @@ function load(full) {
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box);
|
||||
const req = (r) => {
|
||||
if (CATALOG.isCatalogRequest(r)) return CATALOG;
|
||||
if (r.endsWith(".json"))
|
||||
return JSON.parse(fs.readFileSync(path.resolve(path.dirname(full), r)));
|
||||
const name = r.split("/").pop();
|
||||
if (STUBS[name]) return STUBS[name];
|
||||
if (REAL.test(r)) return load(path.resolve(path.dirname(full), r + ".ts"));
|
||||
@@ -83,6 +87,13 @@ const draftStations = [
|
||||
const oldKeys = savedStations.map((s) => s.key);
|
||||
|
||||
const source = crossMod.pipelineCrossSource("p1", saved, savedStations, null);
|
||||
// 그림 자료 = 지금 현황(따라간 관 · 457) — 관 p35100 이 354 로
|
||||
const types = [{ type_id: "pipe", name: "배수관", options: [] }];
|
||||
const status = detailMod.shapeStatus(
|
||||
[{ id: "p35100", facility: "pipe", chainage_m: 354, options: { pipe_diameter_mm: "800" } }],
|
||||
[],
|
||||
types,
|
||||
);
|
||||
const draft = detailMod.draftSectionDetail(
|
||||
saved,
|
||||
{ stations: draftStations, longitudinal: { chainageM: [0, 400], z: [0, 0] } },
|
||||
@@ -90,6 +101,7 @@ const draft = detailMod.draftSectionDetail(
|
||||
null,
|
||||
source.specs,
|
||||
20,
|
||||
status,
|
||||
);
|
||||
const swapped = swapMod.swapCrossSections(saved, draftStations, oldKeys, 20).detail;
|
||||
const ids = (detail) => detail.cross_sections.map((s) => s.station_id);
|
||||
@@ -106,6 +118,7 @@ const added = detailMod.draftSectionDetail(
|
||||
null,
|
||||
source.specs,
|
||||
20,
|
||||
status,
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/* 457 헬퍼 — 노선 바뀜 뒤 초안 카드(묶음 새 상세) 그림 · 이름 · 시설 맥락 = 지금 현황을 새 측점에 다시 얹은 것.
|
||||
* 진짜 TS(`design_pipeline_cross` · `B05_Profile_Pipeline_Detail` · 측점 · 따라감 · 형상 얹기) · 세션 창구만 가짜.
|
||||
* 재현 E · F(시점 당김) · G(세션에서 놓은 BOX) · H(관 둘 엇갈려 옮김) — 결과 = 표준출력 JSON. 짝 = `test_457_draft_card.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
|
||||
const STUBS = {
|
||||
b_page_state: { readState: () => null },
|
||||
B06_Section_Section_Canon: { savedDesigns: () => null },
|
||||
B06_Section_UI_Standard_Panel: {
|
||||
effectiveStandardCross: () => ({ road_width_m: 3 }),
|
||||
readRockBoundaryDefault: () => null,
|
||||
},
|
||||
B05_Profile_UI_Profile_Data: { toDesignProfile: () => ({}) },
|
||||
};
|
||||
const cache = new Map();
|
||||
function resolveTs(base) {
|
||||
for (const c of [`${base}.ts`, base]) if (fs.existsSync(c) && fs.statSync(c).isFile()) return c;
|
||||
throw new Error(`없음: ${base}`);
|
||||
}
|
||||
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 = (r0) => {
|
||||
if (CATALOG.isCatalogRequest(r0)) return CATALOG;
|
||||
const r = r0.replace(/\.js$/, "");
|
||||
const stub = STUBS[r.split("/").pop()];
|
||||
if (stub) return stub;
|
||||
if (r.endsWith(".json"))
|
||||
return JSON.parse(fs.readFileSync(path.join(path.dirname(full), r), "utf8"));
|
||||
if (r.startsWith("@util/")) return load(resolveTs(path.join(ROOT, "common_util", r.slice(6))));
|
||||
if (r.startsWith(".")) return load(resolveTs(path.join(path.dirname(full), r)));
|
||||
return require(r);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
return box.exports;
|
||||
}
|
||||
const P = (...a) => path.join(ROOT, ...a);
|
||||
const follow = load(P("common_util", "common_util_route_follow.ts"));
|
||||
const SS = load(P("B05_Profile", "logic", "B05_Profile_Sections_Stations.ts"));
|
||||
const AT = load(P("common_util", "common_util_structure_attach.ts"));
|
||||
const ST = load(P("common_util", "common_util_structure_station.ts"));
|
||||
const CS = load(P("common_util", "common_util_culvert_sets.ts"));
|
||||
const PD = load(P("B05_Profile", "logic", "B05_Profile_Pipeline_Detail.ts"));
|
||||
const CR = load(P("A00_Common", "design", "design_pipeline_cross.ts"));
|
||||
|
||||
const def = JSON.parse(
|
||||
fs.readFileSync(P("resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
);
|
||||
const types = def.types
|
||||
.filter((t) => t.enabled !== false)
|
||||
.map((t) => ({
|
||||
type_id: t.type_id,
|
||||
group: t.group,
|
||||
placement: t.placement,
|
||||
managed_by: t.managed_by,
|
||||
name: t.name,
|
||||
options: (t.options || []).map((o) => ({ key: o.key, label: o.label, default: o.default })),
|
||||
}));
|
||||
const SHAPES = AT.SHAPE_KEYS;
|
||||
const idOf = (m) => `station_${String(Math.round(m * 1000)).padStart(12, "0")}`;
|
||||
const kindOf = (s) =>
|
||||
s.kind === "end"
|
||||
? s.key.endsWith("EP")
|
||||
? "ep"
|
||||
: "bp"
|
||||
: s.kind === "grid"
|
||||
? "regular"
|
||||
: s.kind === "curve"
|
||||
? "curve"
|
||||
: "irregular";
|
||||
const ground = (s) => ({
|
||||
...s,
|
||||
ground: { centerZ: 100, left: [{ offsetM: 5, z: 101 }], right: [{ offsetM: 5, z: 99 }] },
|
||||
});
|
||||
const sectionsFrom = (S) =>
|
||||
S.map((s) => ({
|
||||
station_id: idOf(s.chainageM),
|
||||
chainage_m: s.chainageM,
|
||||
label: "",
|
||||
kind: kindOf(s),
|
||||
center_x: s.x,
|
||||
center_y: s.y,
|
||||
samples: [],
|
||||
design: { design_elevation_m: 101 },
|
||||
}));
|
||||
const xy = (L) => L.map(([x, y]) => ({ x, y }));
|
||||
function at(L, ch) {
|
||||
let walked = 0;
|
||||
for (let i = 1; i < L.length; i += 1) {
|
||||
const [a, b] = [L[i - 1], L[i]];
|
||||
const span = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
if (walked + span >= ch) {
|
||||
const t = (ch - walked) / span;
|
||||
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
||||
}
|
||||
walked += span;
|
||||
}
|
||||
return L[L.length - 1];
|
||||
}
|
||||
const pipe = (id, facility, ch, L, options) => {
|
||||
const [x, y] = at(L, ch);
|
||||
return { id, facility, chainage_m: ch, x, y, options };
|
||||
};
|
||||
const wall = (L) => {
|
||||
const [x, y] = at(L, 70);
|
||||
return {
|
||||
structure_id: "w1",
|
||||
type_id: "retaining_wall",
|
||||
chainage_m: 70,
|
||||
start_m: 60,
|
||||
end_m: 80,
|
||||
x,
|
||||
y,
|
||||
options: { height_m: 2, side: "left" },
|
||||
};
|
||||
};
|
||||
/** 측점 그림 지문 — 얹힌 형상 열쇠 · 그 누가거리(구간이면 시작~끝). */
|
||||
const shapesOf = (s) =>
|
||||
SHAPES.filter((k) => s[k] !== undefined)
|
||||
.map((k) => {
|
||||
const v = s[k];
|
||||
const c = Array.isArray(v)
|
||||
? v.map((x) => `${x.start_m ?? x.chainage_m ?? ""}~${x.end_m ?? ""}`).join("/")
|
||||
: v && (v.chainage_m ?? `${v.start_m ?? ""}~${v.end_m ?? ""}`);
|
||||
return `${k}@${c}`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
/** 정답 = 새 측점(구조물 없는 꼴)에 지금 현황(따라간 관 · 구조물)을 얹은 그림 · 측점 계획 이름(캐시 `withDraftShapes`). */
|
||||
function truthOf(S2, pipes, structures) {
|
||||
const bare = S2.map((s) => ({ chainage_m: s.chainageM }));
|
||||
AT.attachShapes(bare, pipes, structures, types);
|
||||
const planned = ST.planStations(pipes, structures, types, CS.registryDefaults(types));
|
||||
return bare.map((s) => ({
|
||||
shapes: shapesOf(s),
|
||||
name: planned.find((p) => Math.abs(p.chainage_m - s.chainage_m) < 0.005)?.title ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
function scenario(L1, L2, saved, now, opts = {}) {
|
||||
const S1 = SS.buildStations(L1, [], saved.pipes, saved.structures, { types, intervalM: 20 });
|
||||
// 저장 상세(서버) = 저장 측점에 저장 현황 그림 · 초안 둔 채 열기면 화면 상세가 초안 자리(`canon`)
|
||||
const Scanon = opts.canon ?? S1;
|
||||
const detail = {
|
||||
longitudinal: { stations: [], design_profiles: [], length_m: 0, samples: [] },
|
||||
cross_sections: sectionsFrom(Scanon),
|
||||
};
|
||||
AT.attachShapes(
|
||||
detail.cross_sections,
|
||||
opts.canonPipes ?? saved.pipes,
|
||||
opts.canonStructures ?? saved.structures,
|
||||
types,
|
||||
);
|
||||
const pF = follow.projectPipePoints(now.pipes, xy(L2));
|
||||
const sF = follow.followStructures(now.structures, xy(L2), undefined, 20).map((m) => m.item);
|
||||
const S2 = SS.buildStations(L2, [], pF, sF, { types, intervalM: 20 }).map(ground);
|
||||
const keysOf = opts.keys ? () => opts.keys : undefined;
|
||||
const source = CR.pipelineCrossSource("p457", detail, S1, 7, keysOf);
|
||||
// 노선 초안 동안 등록 목록이 새 노선 것으로 바뀜 — 처음 본 열쇠 그대로여야 함
|
||||
const again = opts.keys
|
||||
? CR.pipelineCrossSource("p457", detail, S1, 7, () => [...opts.keys].reverse())
|
||||
: source;
|
||||
const status = PD.shapeStatus
|
||||
? PD.shapeStatus(pF, sF, types)
|
||||
: { pipes: pF, structures: sF, types };
|
||||
const card = PD.draftSectionDetail(
|
||||
detail,
|
||||
{ stations: S2, longitudinal: { chainageM: [0], z: [0] } },
|
||||
{},
|
||||
null,
|
||||
again.specs,
|
||||
20,
|
||||
status,
|
||||
);
|
||||
const truth = truthOf(S2, pF, sF);
|
||||
const shaped = PD.crossWithShapes ? PD.crossWithShapes(source.input, S2, status) : source.input;
|
||||
const ctx = (key) => {
|
||||
const e = shaped.byKey[key] ?? {};
|
||||
return [Boolean(e.paved), Boolean(e.tilted), e.tiltPct ?? null, e.surfaceDropM ?? 0];
|
||||
};
|
||||
return {
|
||||
rows: S2.map((s, i) => ({
|
||||
key: s.key,
|
||||
m: s.chainageM,
|
||||
card: shapesOf(card.cross_sections[i]),
|
||||
truth: truth[i].shapes,
|
||||
name: card.cross_sections[i].structure ?? null,
|
||||
truthName: truth[i].name,
|
||||
id: card.cross_sections[i].station_id,
|
||||
ctx: ctx(s.key),
|
||||
})),
|
||||
canonIds: Object.fromEntries(
|
||||
Scanon.map((s, i) => [s.key, detail.cross_sections[i].station_id]),
|
||||
),
|
||||
sourceKeys: Object.fromEntries([...source.specs].map(([k, v]) => [k, v.station_id])),
|
||||
specFields: [...new Set([...source.specs.values()].flatMap((v) => Object.keys(v)))],
|
||||
};
|
||||
}
|
||||
|
||||
const L1 = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
];
|
||||
const P1 = [
|
||||
pipe("p1", "pipe", 47, L1, { pipe_diameter_mm: "800" }),
|
||||
pipe("fp", "ford_pavement", 125, L1, { ford_width_m: 12 }),
|
||||
];
|
||||
const facilities = { pipes: P1, structures: [wall(L1)] };
|
||||
const out = {};
|
||||
out.E = scenario(
|
||||
L1,
|
||||
[
|
||||
[-13, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
facilities,
|
||||
facilities,
|
||||
);
|
||||
out.F = scenario(
|
||||
L1,
|
||||
[
|
||||
[-7, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
facilities,
|
||||
facilities,
|
||||
);
|
||||
out.G = scenario(
|
||||
L1,
|
||||
[
|
||||
[-7, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
facilities,
|
||||
{
|
||||
pipes: [...P1, pipe("p9", "box_culvert", 90, L1, { body_width_m: 2 })],
|
||||
structures: [wall(L1)],
|
||||
},
|
||||
);
|
||||
// H — 세션에서 관 둘을 엇갈려 옮김(저장 전) · 초안 둔 채 열기(화면 상세 = 옮긴 자리) → 시점 +7
|
||||
{
|
||||
const saved = [
|
||||
pipe("p1", "pipe", 47, L1, { pipe_diameter_mm: "800" }),
|
||||
pipe("p2", "box_culvert", 90, L1, { body_width_m: 2 }),
|
||||
];
|
||||
const moved = [
|
||||
pipe("p1", "pipe", 110, L1, { pipe_diameter_mm: "800" }),
|
||||
pipe("p2", "box_culvert", 30, L1, { body_width_m: 2 }),
|
||||
];
|
||||
const Snow = SS.buildStations(L1, [], moved, [], { types, intervalM: 20 });
|
||||
out.H = scenario(
|
||||
L1,
|
||||
[
|
||||
[-7, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
{ pipes: saved, structures: [] },
|
||||
{ pipes: moved, structures: [] },
|
||||
{ canon: Snow, canonPipes: moved, keys: Snow.map((s) => s.key) },
|
||||
);
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,195 @@
|
||||
/* PLAN 458 노선 바뀜 — 진짜 모듈(따라감 · 측점 · 갈아끼움 · 형상 얹기)을 그 자리에서 트랜스파일해 Node 로 돌림.
|
||||
* 저장 노선 (0,0)→(100,0)→(200,60) 위 관 · 세월교 · 옹벽 → 시점 당김 / 밀어냄 → 갈아끼움 + 지금 현황으로 다시 얹기.
|
||||
* 결과 = 표준출력 JSON. 짝 = `test_458_swap_attach.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
const cache = new Map();
|
||||
function resolveTs(base) {
|
||||
for (const c of [`${base}.ts`, base, path.join(base, "index.ts")])
|
||||
if (fs.existsSync(c) && fs.statSync(c).isFile()) return c;
|
||||
throw new Error(`모름: ${base}`);
|
||||
}
|
||||
function load(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
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.exports);
|
||||
const need = (spec0) => {
|
||||
if (/common_util_structure_catalog(\.js)?$/.test(spec0))
|
||||
return require(path.join(__dirname, "helper_322_catalog_module.cjs"));
|
||||
const spec = spec0.replace(/\.js$/, "");
|
||||
if (spec.endsWith(".json"))
|
||||
return JSON.parse(fs.readFileSync(path.join(path.dirname(full), spec), "utf8"));
|
||||
if (spec.startsWith("@util/"))
|
||||
return load(resolveTs(path.join(ROOT, "common_util", spec.slice(6))));
|
||||
if (spec.startsWith(".")) return load(resolveTs(path.join(path.dirname(full), spec)));
|
||||
return require(spec);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, need);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const P = (...parts) => path.join(ROOT, ...parts);
|
||||
const follow = load(P("common_util", "common_util_route_follow.ts"));
|
||||
const SS = load(P("B05_Profile", "logic", "B05_Profile_Sections_Stations.ts"));
|
||||
const SW = load(P("B06_Section", "logic", "B06_Section_Station_Swap.ts"));
|
||||
const AT = load(P("common_util", "common_util_structure_attach.ts"));
|
||||
const def = JSON.parse(
|
||||
fs.readFileSync(P("resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
);
|
||||
const types = def.types
|
||||
.filter((t) => t.enabled !== false)
|
||||
.map((t) => ({
|
||||
type_id: t.type_id,
|
||||
group: t.group,
|
||||
placement: t.placement,
|
||||
managed_by: t.managed_by,
|
||||
name: t.name,
|
||||
options: (t.options || []).map((o) => ({ key: o.key, label: o.label, default: o.default })),
|
||||
}));
|
||||
|
||||
const INTERVAL = 20;
|
||||
const xy = (L) => L.map(([x, y]) => ({ x, y }));
|
||||
function at(L, ch) {
|
||||
let walked = 0;
|
||||
for (let i = 1; i < L.length; i++) {
|
||||
const [a, b] = [L[i - 1], L[i]];
|
||||
const span = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
if (walked + span >= ch) {
|
||||
const t = (ch - walked) / span;
|
||||
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
||||
}
|
||||
walked += span;
|
||||
}
|
||||
return L[L.length - 1];
|
||||
}
|
||||
const kindOf = (s) =>
|
||||
s.kind === "end"
|
||||
? s.key.endsWith("EP")
|
||||
? "ep"
|
||||
: "bp"
|
||||
: s.kind === "grid"
|
||||
? "regular"
|
||||
: s.kind === "curve"
|
||||
? "curve"
|
||||
: "irregular";
|
||||
/** 저장 상세 — 서버처럼 격자 자리 시설 측점에 `grid` · 설계에 열쇠 메모. */
|
||||
const sectionsFrom = (stations) =>
|
||||
stations.map((s) => ({
|
||||
station_id: SW.stationIdOf(s.chainageM),
|
||||
chainage_m: s.chainageM,
|
||||
label: "",
|
||||
kind: kindOf(s),
|
||||
...(kindOf(s) === "irregular" && s.chainageM % INTERVAL === 0 ? { grid: true } : {}),
|
||||
center_x: s.x,
|
||||
center_y: s.y,
|
||||
center_z: 100,
|
||||
frame: { left_xy: s.left },
|
||||
samples: [{ offset_m: 0, elevation_m: 100, valid: true }],
|
||||
uphill_side: "left",
|
||||
design: { memo: s.key, design_elevation_m: 101 },
|
||||
}));
|
||||
const ground = (s) => ({
|
||||
...s,
|
||||
ground: { centerZ: 100, left: [{ offsetM: 5, z: 101 }], right: [{ offsetM: 5, z: 99 }] },
|
||||
});
|
||||
const KEYS = ["culvert", "ford", "box", "ford_pavement"];
|
||||
const view = (sections) =>
|
||||
sections.map((s) => ({
|
||||
chainage_m: s.chainage_m,
|
||||
kind: s.kind,
|
||||
grid: !!s.grid,
|
||||
shapes: Object.fromEntries(KEYS.filter((k) => s[k]).map((k) => [k, s[k].chainage_m])),
|
||||
}));
|
||||
|
||||
function run(L1, L2, pipes, structures) {
|
||||
const S1 = SS.buildStations(L1, [], pipes, structures, { types, intervalM: INTERVAL });
|
||||
const saved = { longitudinal: { stations: [], length_m: 0 }, cross_sections: sectionsFrom(S1) };
|
||||
AT.attachShapes(saved.cross_sections, pipes, structures, types);
|
||||
saved.cross_sections[0].preview = true; // 초안 맞춤의 구조물 없는 사본 표시(갈아끼움이 안 들고 감)
|
||||
const pipesF = follow.projectPipePoints(pipes, xy(L2));
|
||||
const structuresF = follow
|
||||
.followStructures(structures, xy(L2), undefined, INTERVAL)
|
||||
.map((m) => m.item);
|
||||
const S2 = SS.buildStations(L2, [], pipesF, structuresF, { types, intervalM: INTERVAL }).map(
|
||||
ground,
|
||||
);
|
||||
const swap = SW.swapCrossSections(
|
||||
saved,
|
||||
S2,
|
||||
S1.map((s) => s.key),
|
||||
INTERVAL,
|
||||
);
|
||||
const now = swap.detail.cross_sections.map((s) => ({ ...s }));
|
||||
const misses = AT.attachShapes(now, pipesF, structuresF, types);
|
||||
// 가게 초안이 없는 채(저장분 현황) 얹기 — 상한이 옛 자리 관을 이웃 측점에 안 붙임
|
||||
const stale = swap.detail.cross_sections.map((s) => ({ ...s }));
|
||||
const staleMisses = AT.attachShapes(stale, pipes, structures, types);
|
||||
return {
|
||||
ids: swap.detail.cross_sections.map((s) => [s.chainage_m, s.station_id]),
|
||||
preview: swap.detail.cross_sections.filter((s) => s.preview).length,
|
||||
pipes: pipesF.map((p) => [p.id, p.chainage_m]),
|
||||
now: view(now),
|
||||
misses,
|
||||
staleMisses,
|
||||
stale: view(stale),
|
||||
};
|
||||
}
|
||||
|
||||
const L1 = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
];
|
||||
const pipe = (id, facility, ch, options) => {
|
||||
const [x, y] = at(L1, ch);
|
||||
return { id, facility, chainage_m: ch, x, y, options };
|
||||
};
|
||||
const pipes = [
|
||||
pipe("p1", "pipe", 40, { pipe_diameter_mm: "800" }), // 격자 No.2 위
|
||||
pipe("p2", "ford_bridge", 150, { ford_width_m: 10 }),
|
||||
];
|
||||
const [wx, wy] = at(L1, 70);
|
||||
const wall = {
|
||||
structure_id: "w1",
|
||||
type_id: "retaining_wall",
|
||||
chainage_m: 70,
|
||||
start_m: 60,
|
||||
end_m: 80,
|
||||
x: wx,
|
||||
y: wy,
|
||||
options: { height_m: 2, side: "left" },
|
||||
};
|
||||
|
||||
const out = {
|
||||
// A — 시점 7 m 당김: 관 40 → 47(격자를 떠남) · 세월교 150 → 157
|
||||
shift: run(
|
||||
L1,
|
||||
[
|
||||
[-7, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
pipes,
|
||||
[wall],
|
||||
),
|
||||
// D — 시점을 50 m 로 밀어냄: 관 40 = 새 시점 10 m 바깥 → 누가거리 0
|
||||
outside: run(
|
||||
L1,
|
||||
[
|
||||
[50, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
pipes,
|
||||
[wall],
|
||||
),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""271-1~3 B05 종단 [직선화] 측점 고름 · 끝 R 기본 L · [직선화] · [자유지정] 실시간 띠.
|
||||
|
||||
요구 271 · PLAN 461.
|
||||
|
||||
계산(`logic/B05_Profile_UI_Profile_Straighten.ts` · `_Tools.ts` · `design_profile_free.ts` ·
|
||||
`design_profile_range_band.ts` · `_CurveLaw.ts`)은 Node 헬퍼(`helper_271_straighten_pick.cjs`)가
|
||||
트랜스파일해 돌리고, 파이썬 짝(`build_alignment` · `vertical_curve_shortfalls`)은 같은 base · 편집으로
|
||||
곡선 자리 · R · L · 미달 경고가 같은지 거울로 본다.
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_271_straighten_pick.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import ( # noqa: E402
|
||||
AlignmentPolicy,
|
||||
build_alignment,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Grade_CurveLaw import ( # noqa: E402
|
||||
SHORTFALL_TOLERANCE_M,
|
||||
vertical_curve_shortfalls,
|
||||
)
|
||||
|
||||
|
||||
def _read(relative: str) -> str:
|
||||
return (ROOT / relative).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_271_straighten_pick.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _at(curves, chainage):
|
||||
return next((c for c in curves if abs(c["at"] - chainage) < 1e-6), None)
|
||||
|
||||
|
||||
def test_직선화_끝_기본_L_사이_곡선_없음(out):
|
||||
curves = out["straight"]["curves"]
|
||||
assert not [c for c in curves if 40 < c["at"] < 200]
|
||||
assert _at(curves, 40)["l"] == pytest.approx(20) and _at(curves, 200)["l"] == pytest.approx(20)
|
||||
assert out["straight"]["radii"] == []
|
||||
|
||||
|
||||
def test_이미_직선_범위_부분_구간_반대_방향_같음(out):
|
||||
assert out["sub"] == {"fromM": 80, "toM": 160, "nodes": [80, 100, 120, 140, 160]}
|
||||
assert out["subReverse"] == out["sub"]
|
||||
assert out["subBent"] is None and out["subMixed"] is None
|
||||
assert out["subOneSpan"] is None # 사이 꼭짓점 없음 = 꺾을 곳 없음
|
||||
|
||||
|
||||
def test_부분_구간_꺾기_양_끝_L20_거짓_미달_없음(out):
|
||||
curves = out["tiltSub"]["curves"]
|
||||
for at in (80, 120, 160):
|
||||
assert _at(curves, at)["l"] == pytest.approx(20, abs=1e-3)
|
||||
assert not [c for c in curves if 80 < c["at"] < 160 and c["at"] != 120]
|
||||
assert out["tiltSub"]["shortfalls"] == []
|
||||
|
||||
|
||||
def test_법정_판정_오차_짝(out):
|
||||
assert out["tolerance"] == SHORTFALL_TOLERANCE_M == 1e-3
|
||||
assert out["nearMiss"] == []
|
||||
assert len(out["realMiss"]) == 1
|
||||
near = [{"chainage_m": 120, "l_m": 19.99998, "r_m": 800}]
|
||||
assert vertical_curve_shortfalls(near, 100, 20) == []
|
||||
real = [{"chainage_m": 120, "l_m": 19.9, "r_m": 800}]
|
||||
assert vertical_curve_shortfalls(real, 100, 20) == out["realMiss"]
|
||||
|
||||
|
||||
def test_끝_저장_자동_R_지움_사용자_R_둠(out):
|
||||
assert out["autoEnds"]["radii"] == []
|
||||
curves = out["autoEnds"]["curves"]
|
||||
assert _at(curves, 40)["l"] == pytest.approx(20) and _at(curves, 200)["l"] == pytest.approx(20)
|
||||
assert out["userEnd"]["radii"] == {"40.000": 600}
|
||||
assert out["noAuto"] == ["40.000", "200.000"]
|
||||
|
||||
|
||||
def test_BP_EP_곡선_없음(out):
|
||||
assert out["bpEp"] == []
|
||||
assert out["tiltBp"][:2] == [40, 80] # 시점 0 엔 곡선 없음 · 가운데 · 끝만
|
||||
|
||||
|
||||
def test_곡선_섞인_범위_안쪽_R_지움_양_끝_L20(out):
|
||||
assert out["mixed"]["radii"] == []
|
||||
curves = out["mixed"]["curves"]
|
||||
assert not [c for c in curves if 100 < c["at"] < 260]
|
||||
assert _at(curves, 100)["l"] == pytest.approx(20) and _at(curves, 260)["l"] == pytest.approx(20)
|
||||
|
||||
|
||||
def test_자유지정_끝_자동_R_지움_사용자_R_둠(out):
|
||||
assert out["freeAuto"] == ["120.000"]
|
||||
assert out["freeUser"] == ["40.000", "120.000"]
|
||||
|
||||
|
||||
def test_자유지정_범위_실시간_띠_Esc_시작만(out):
|
||||
assert out["liveBand"] == [40, 120] and out["liveEdges"] == [40, 120]
|
||||
assert out["liveReverse"] == [20, 40]
|
||||
assert out["outside"] == {"band": None, "edges": [40]} # 그래프 밖 = 시작선만
|
||||
assert "Esc 시작 취소" in out["hintPending"]
|
||||
assert out["escTaken"] and out["afterEsc"] == {"phase": "range", "edges": [], "closed": 0}
|
||||
assert out["fixedBand"] == [40, 160] and out["armedPhase"] == "armed" # 둘째 클릭 뒤 고정
|
||||
assert out["escArmed"] and out["afterEscArmed"] == {"phase": "range", "closed": 1}
|
||||
|
||||
|
||||
def test_범위_띠_그림(out):
|
||||
assert out["bandShape"] == ["rect:80+160", "text:40.0–120.0m · 80.0m", "line:80", "line:240"]
|
||||
assert out["bandStart"] == ["line:80"]
|
||||
assert out["viewIdle"] == 0 and out["viewOff"] == 0
|
||||
assert out["viewStart"] == ["line:80"]
|
||||
assert out["viewHover"] == out["bandShape"]
|
||||
assert out["viewLeave"] == ["line:80"]
|
||||
assert out["viewRemount"] == ["rect:80+40", "text:40.0–60.0m · 20.0m", "line:80", "line:120"]
|
||||
|
||||
|
||||
def test_도구_직선화_부분_구간_붉은_강조_아님(out):
|
||||
assert out["toolsNone"] is False
|
||||
assert out["pending"] == {
|
||||
"at": 40,
|
||||
"hint": "40.0m 선택 — 두 번째 측점을 고르세요 (Esc 시작 취소)",
|
||||
}
|
||||
assert out["cancel"] == {"taken": True, "pending": None, "mode": "straighten"}
|
||||
assert out["cancelAgain"] is False
|
||||
assert out["subPicked"]["red"] == [] and out["subPicked"]["sub"]["fromM"] == 80
|
||||
assert out["subPicked"]["hint"] == "이미 직선 — ▲▼ 로 꺾으면 양 끝에 기본 L 곡선"
|
||||
assert out["tiltCall"] == ["tilt", 80, 160, 0.1]
|
||||
assert out["newStart"] == {"pending": 60, "sub": None}
|
||||
assert out["straightenCall"] == [60, 180]
|
||||
assert out["shiftRed"] == {"red": 1, "sub": None, "cancel": False} # [쉬프트] 붉은 강조 그대로
|
||||
|
||||
|
||||
def test_파이썬_거울_곡선_자리_R_L_미달(out):
|
||||
base = out["mirror"]["base"]
|
||||
policy = AlignmentPolicy.from_dict(base["policy"])
|
||||
for case in out["mirror"]["cases"]:
|
||||
got = build_alignment(
|
||||
base_s=np.array([n["chainage_m"] for n in base["basePvi"]]),
|
||||
base_z=np.array([n["elevation_m"] for n in base["basePvi"]]),
|
||||
chainage=np.array(base["chainage"]),
|
||||
ground=np.array(base["ground"]),
|
||||
stations=base["stations"],
|
||||
policy=policy,
|
||||
edits=case["edits"],
|
||||
)
|
||||
py = [[c["chainage_m"], c["r_m"], c["l_m"]] for c in got["curves"]]
|
||||
ts = case["curves"]
|
||||
assert [c[0] for c in py] == pytest.approx([c[0] for c in ts], abs=1e-6), case["name"]
|
||||
assert [c[1] for c in py] == pytest.approx([c[1] for c in ts], rel=1e-6), case["name"]
|
||||
assert [c[2] for c in py] == pytest.approx([c[2] for c in ts], abs=1e-6), case["name"]
|
||||
assert not [w for w in got["warnings"] if "미달" in w], case["name"]
|
||||
|
||||
|
||||
def test_배선():
|
||||
panel = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
assert "tools.handleStationPick(station ?? chainageM)" in panel
|
||||
assert "straightSubRun(alignment, fromM, toM)" in panel
|
||||
assert "straightenBetween(base, ctx.edits(), fromM, toM, ctx.savedRadii())" in panel
|
||||
assert "autoRadii: ctx.savedRadii" in panel
|
||||
assert 'window.addEventListener("keydown", onEscape, true)' in panel
|
||||
assert "createRangeBandView(" in panel
|
||||
view = _read("A00_Common/design/design_profile_free_view.ts")
|
||||
assert "appendRangeBand(svg, x, heightPx" in view
|
||||
css = _read("B05_Profile/B05_Profile_UI_Style_Table.css")
|
||||
rule = css[css.index(".b05-profile-freemark__span {") :]
|
||||
rule = rule[: rule.index("}")]
|
||||
assert "--color-primary" in rule and "--color-surface" in rule # 밝음 · 어두움 토큰
|
||||
|
||||
|
||||
def test_줄_수():
|
||||
for relative in (
|
||||
"B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts",
|
||||
"B05_Profile/B05_Profile_UI_Profile_Tools.ts",
|
||||
"B05_Profile/logic/B05_Profile_UI_Profile_Straighten.ts",
|
||||
"A00_Common/design/logic/design_profile_free.ts",
|
||||
"A00_Common/design/design_profile_free_view.ts",
|
||||
"A00_Common/design/design_profile_range_band.ts",
|
||||
):
|
||||
assert len(_read(relative).splitlines()) <= 700
|
||||
@@ -56,12 +56,13 @@ POINTS = [
|
||||
]
|
||||
CHAINAGES = [0.0, 20.0, 40.0, 60.0, 80.0, 85.0, 90.0, 95.0, 100.0, 105.0]
|
||||
|
||||
# 같은 키 — 옆 측점 연동(88.5 · 폭 10 · 주인 88)이 90 m 에 더 가깝지만 소유(92.4 · 주인 90)가 먼저
|
||||
# 같은 키 — 옆 측점 연동(89.8 · 폭 10 · 주인 89.7)이 90 m 에 더 가깝지만 소유(90.4 · 주인 90)가 먼저
|
||||
# (주인 측점 = 0.51 m 안 · 458)
|
||||
SAME_KEY_POINTS = [
|
||||
{"chainage_m": 88.5, "facility": "ford_pavement", "options": {"ford_width_m": 10.0}},
|
||||
{"chainage_m": 92.4, "facility": "ford_pavement", "options": {"ford_width_m": 0.1}},
|
||||
{"chainage_m": 89.8, "facility": "ford_pavement", "options": {"ford_width_m": 10.0}},
|
||||
{"chainage_m": 90.4, "facility": "ford_pavement", "options": {"ford_width_m": 0.1}},
|
||||
]
|
||||
SAME_KEY_CHAINAGES = [85.0, 88.0, 90.0, 95.0]
|
||||
SAME_KEY_CHAINAGES = [85.0, 89.7, 90.0, 95.0]
|
||||
|
||||
|
||||
def _attach(tmp_path: Path, points: list[dict], chainages: list[float]) -> list[dict]:
|
||||
@@ -103,8 +104,8 @@ def test_흙막이는_자기_관에_밀림_없으면_그대로(sections):
|
||||
|
||||
def test_같은_키는_소유_측점_먼저(tmp_path):
|
||||
sections = _attach(tmp_path, SAME_KEY_POINTS, SAME_KEY_CHAINAGES)
|
||||
assert _at(sections, 90.0)["ford_pavement"]["chainage_m"] == 92.4
|
||||
assert _at(sections, 85.0)["ford_pavement"]["chainage_m"] == 88.5
|
||||
assert _at(sections, 90.0)["ford_pavement"]["chainage_m"] == 90.4
|
||||
assert _at(sections, 85.0)["ford_pavement"]["chainage_m"] == 89.8
|
||||
|
||||
|
||||
def test_수량은_주인_측점_한_번(sections):
|
||||
|
||||
@@ -99,9 +99,35 @@ def _sections(until: float) -> list[dict]:
|
||||
|
||||
PIPES, STRUCTURES = _samples()
|
||||
UNTIL = 15.0 + 30.0 * len(catalog()) + 60
|
||||
|
||||
|
||||
def _moved() -> dict:
|
||||
"""458 노선 바뀜 — 노선 x 축 0 ~ 100 · 측점 10 m 마다(좌표 있음).
|
||||
|
||||
관 30.4(주인 30 · 0.4 m) · 44(가장 가까운 측점 4 m = 측점 없음) · 0(좌표 -3 = 시점 밖) ·
|
||||
100(좌표 100.3 · 옆 1.5 = 종점 안) · 물넘이포장 70(폭 24 = 60 · 80 연동) · 55(주인 5 m = 연동도 안 얹음).
|
||||
"""
|
||||
pipe = _defaults("pipe")
|
||||
pave = {**_defaults("ford_pavement"), "ford_width_m": 24}
|
||||
sections = [
|
||||
{"chainage_m": float(at), "center_x": float(at), "center_y": 0.0}
|
||||
for at in range(0, 101, 10)
|
||||
]
|
||||
pipes = [
|
||||
{"id": "a", "chainage_m": 30.4, "x": 30.4, "y": 0.0, "facility": "pipe", "options": pipe},
|
||||
{"id": "b", "chainage_m": 44.0, "x": 44.0, "y": 0.0, "facility": "pipe", "options": pipe},
|
||||
{"id": "c", "chainage_m": 0.0, "x": -3.0, "y": 0.0, "facility": "pipe", "options": pipe},
|
||||
{"id": "d", "chainage_m": 100.0, "x": 100.3, "y": 1.5, "facility": "pipe", "options": pipe},
|
||||
{"id": "e", "chainage_m": 70.0, "facility": "ford_pavement", "options": pave},
|
||||
{"id": "f", "chainage_m": 55.0, "facility": "ford_pavement", "options": pave},
|
||||
]
|
||||
return {"sections": sections, "pipes": pipes, "structures": []}
|
||||
|
||||
|
||||
CASES = {
|
||||
"all": {"sections": _sections(UNTIL), "pipes": PIPES, "structures": STRUCTURES},
|
||||
"empty": {"sections": _sections(30), "pipes": [], "structures": []},
|
||||
"moved": _moved(),
|
||||
}
|
||||
|
||||
|
||||
@@ -122,22 +148,36 @@ def ts() -> dict:
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def _py(case: dict) -> list[dict]:
|
||||
def _py(case: dict) -> tuple[list[dict], dict]:
|
||||
sections = copy.deepcopy(case["sections"])
|
||||
pipes = parse_pipe_points(case["pipes"])
|
||||
structures = [StructureInstance.model_validate(s) for s in case["structures"]]
|
||||
attach_shapes(sections, pipes, structures)
|
||||
return json.loads(json.dumps(sections))
|
||||
misses = attach_shapes(sections, pipes, structures)
|
||||
return json.loads(json.dumps(sections)), misses
|
||||
|
||||
|
||||
def test_mirror_python_equals_ts(ts: dict) -> None:
|
||||
for name, case in CASES.items():
|
||||
py, js = _py(case), ts["cases"][name]
|
||||
(py, misses), js = _py(case), ts["cases"][name]
|
||||
assert misses == ts["misses"][name], name
|
||||
assert len(py) == len(js), name
|
||||
for left, right in zip(py, js):
|
||||
assert left == right, (name, left["chainage_m"])
|
||||
|
||||
|
||||
def test_owner_cap_and_outside(ts: dict) -> None:
|
||||
"""458 — 주인 측점 0.51 m 상한 · 시종점 밖 = 안 얹고 경고 · 연동 이웃 = 폭 절반."""
|
||||
by_at = {s["chainage_m"]: s for s in ts["cases"]["moved"]}
|
||||
assert ts["misses"]["moved"] == {"outside": [0.0], "far": [44.0, 55.0]}
|
||||
assert by_at[30.0]["culvert"]["chainage_m"] == 30.4
|
||||
assert by_at[100.0]["culvert"]["chainage_m"] == 100.0 # 종점 0.3 m 넘음 · 옆 1.5 m = 노선 안
|
||||
assert "culvert" not in by_at[0.0] and "culvert" not in by_at[40.0]
|
||||
assert "culvert" not in by_at[50.0]
|
||||
linked = {at for at, s in by_at.items() if "ford_pavement" in s}
|
||||
assert linked == {60.0, 70.0, 80.0}
|
||||
assert all(by_at[at]["ford_pavement"]["chainage_m"] == 70.0 for at in linked)
|
||||
|
||||
|
||||
def test_every_shape_lands(ts: dict) -> None:
|
||||
keys = {key for section in ts["cases"]["all"] for key in section if key != "chainage_m"}
|
||||
assert {
|
||||
|
||||
@@ -170,10 +170,9 @@ def test_365_5_들어온_화면_무관(r):
|
||||
g = v["grid20"]
|
||||
# 설계 칸 = 세션 저장 상세(화면 상세가 굳힌 선택 false 대신 저장값) · 옛 측구 결과도 실음
|
||||
assert g["ditchChoice"] is None and g["ditchEnabled"] is False
|
||||
assert v["pipeSpec"] == {
|
||||
"culvert": {"type": "pipe", "diameter_m": 0.8}
|
||||
} # 초안 자리 관 = 차례 짝
|
||||
assert v["specsDiffer"] # 제원이 바뀌면 입력 지문도 바뀜(고리가 다시 셈)
|
||||
# 초안 자리 관 = 등록 열쇠 짝 · 넘기는 것 = 카드 번호 · 사용자 칸만(그림은 새 자리에서 · 457)
|
||||
assert v["pipeSpec"] == {} and v["pipeCarried"] # 짝 있음(시험 상세는 카드 번호 없음)
|
||||
assert v["specsDiffer"] # 짝지은 측점이 바뀌면 입력 지문도 바뀜(고리가 다시 셈)
|
||||
assert v["firstStored"] == v["savedFirst"] and v["firstShaken"] != v["savedFirst"]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""457 노선 바뀜 — 초안 카드 그림 · 이름 · 횡단 맥락 = 지금 현황을 새 측점에 다시 얹음(요구 270).
|
||||
|
||||
재현 E · F(시점 당김 +13 · +7 — 배관 · 물넘이포장 · 옹벽) · G(세션에서 놓은 BOX) ·
|
||||
H(관 둘 엇갈려 옮김 · 초안 둔 채 열기).
|
||||
① 모든 측점 카드 그림 · 이름 = 새 측점에 따라간 관 · 구조물을 얹은 것(캐시 `withDraftShapes` 한 벌)
|
||||
② 시설마다 제 측점 하나 · 옛 측점 그림을 옮기지 않음 · 카드 번호만 열쇠로 이어 받음
|
||||
③ 시설 맥락(포장 · 기울임 · 하강) = 다시 얹은 그림에서 · 짝 = 등록 열쇠(차례 짝 없음 · 엇갈림 0)
|
||||
④ 가게 쓰기 = 지금 가게 값과 견줌(되돌리기 뒤 같은 조작도 씀)
|
||||
TS 셈은 `helper_457_draft_card.cjs`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_457_draft_card.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_457_draft_card.cjs")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def _row(rows: list, key: str) -> dict:
|
||||
return next(r for r in rows if r["key"] == key)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["E", "F", "G", "H"])
|
||||
def test_카드_그림_이름이_새_자리_현황(got, name) -> None:
|
||||
rows = got[name]["rows"]
|
||||
bad = [(r["key"], r["card"], r["truth"]) for r in rows if r["card"] != r["truth"]]
|
||||
assert not bad
|
||||
names = [(r["key"], r["name"], r["truthName"]) for r in rows if r["name"] != r["truthName"]]
|
||||
assert not names
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["E", "F", "G", "H"])
|
||||
def test_카드_번호만_이어_받음(got, name) -> None:
|
||||
assert got[name]["specFields"] == ["station_id"]
|
||||
|
||||
|
||||
def test_E_물넘이포장은_제_측점에만(got) -> None:
|
||||
rows = got["E"]["rows"]
|
||||
fp = _row(rows, "pipe:fp")
|
||||
assert "ford_pavement@" in fp["card"]
|
||||
assert fp["ctx"][:2] == [True, True] # 포장 · 기울임 = 새 자리 그림
|
||||
no6 = _row(rows, "grid:6") # 120 m — 옛 물넘이 범위(119~131) · 새 범위(132~144) 밖
|
||||
assert "ford_pavement" not in no6["card"] and no6["name"] is None
|
||||
assert no6["ctx"] == [False, False, None, 0]
|
||||
pipes = [r for r in rows if "culvert@" in r["card"]]
|
||||
assert [r["key"] for r in pipes] == ["pipe:p1"] # 배관 그림 = 관 측점 하나(No.3 아님)
|
||||
|
||||
|
||||
def test_G_세션에서_놓은_BOX_카드(got) -> None:
|
||||
box = _row(got["G"]["rows"], "pipe:p9")
|
||||
assert "box@" in box["card"] and box["name"]
|
||||
|
||||
|
||||
def test_H_엇갈려_옮긴_관_짝(got) -> None:
|
||||
h = got["H"]
|
||||
p1 = _row(h["rows"], "pipe:p1")
|
||||
p2 = _row(h["rows"], "pipe:p2")
|
||||
assert "culvert@" in p1["card"] and "box@" not in p1["card"]
|
||||
assert "box@" in p2["card"] and "culvert@" not in p2["card"]
|
||||
# 카드 번호 = 같은 관의 화면 상세 번호(등록 열쇠 짝 · 목록이 바뀌어도 처음 본 열쇠)
|
||||
assert h["sourceKeys"]["pipe:p1"] == h["canonIds"]["pipe:p1"]
|
||||
assert h["sourceKeys"]["pipe:p2"] == h["canonIds"]["pipe:p2"]
|
||||
assert p1["id"] == h["canonIds"]["pipe:p1"]
|
||||
|
||||
|
||||
def test_가게_쓰기는_지금_가게_값과_견줌() -> None:
|
||||
page = (ROOT / "A00_Common" / "design" / "design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||
body = page[page.index("function writeFacilities") :]
|
||||
body = body[: body.index("\n }\n")]
|
||||
assert 'same(store.get("pipes"), next.pipes)' in body
|
||||
assert 'same(store.get("structures"), next.structures)' in body
|
||||
assert "written" not in page
|
||||
@@ -0,0 +1,82 @@
|
||||
"""458 노선 바뀜 — B06 갈아끼움 + 지금 현황으로 다시 얹기 · 주인 측점 거리 상한 · 시종점 밖.
|
||||
|
||||
못박는 것
|
||||
- 시점 당김(재현 A 꼴): 관 · 세월교 그림 = 제 측점 하나씩(따라간 누가거리) · 격자를 떠난 관 측점 `grid` 지움.
|
||||
- 가게 초안 없는 채(저장분 현황) 얹어도 옛 자리 세월교가 이웃 일반 측점에 안 붙음(상한 0.51 m · 측점 없음).
|
||||
- 시점 밖으로 밀린 관(재현 D 꼴) = 시점 측점에 안 얹고 「노선 밖」.
|
||||
- 카드 번호 안 겹침 — 격자 자리(80 m)를 떠난 옹벽 끝 측점은 새 번호(그 자리 새 일반 측점과 따로) ·
|
||||
초안 사본 표시(`preview`)는 안 들고 감.
|
||||
|
||||
Node 헬퍼 `helper_458_swap_attach.cjs` 가 진짜 모듈(따라감 · 측점 · 갈아끼움 · 형상 얹기)을 부름.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_458_swap_attach.cjs")],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _shaped(sections: list[dict]) -> dict[float, dict]:
|
||||
return {s["chainage_m"]: s["shapes"] for s in sections if s["shapes"]}
|
||||
|
||||
|
||||
def test_시설마다_제_측점_하나(out):
|
||||
shift = out["shift"]
|
||||
assert shift["pipes"] == [["p1", 47], ["p2", 157]]
|
||||
assert _shaped(shift["now"]) == {47: {"culvert": 47}, 157: {"ford": 157}}
|
||||
assert shift["misses"] == {"outside": [], "far": []}
|
||||
|
||||
|
||||
def test_격자_떠난_시설_측점_grid_지움(out):
|
||||
by_at = {s["chainage_m"]: s for s in out["shift"]["now"]}
|
||||
assert by_at[47]["kind"] == "irregular" and by_at[47]["grid"] is False
|
||||
|
||||
|
||||
def test_저장분_현황이어도_이웃에_안_붙음(out):
|
||||
shift = out["shift"]
|
||||
assert shift["staleMisses"]["far"] == [150]
|
||||
assert all("ford" not in shapes for shapes in _shaped(shift["stale"]).values())
|
||||
|
||||
|
||||
def test_시점_밖_관은_안_얹고_노선_밖(out):
|
||||
outside = out["outside"]
|
||||
assert ["p1", 0] in outside["pipes"]
|
||||
assert outside["misses"] == {"outside": [0], "far": []}
|
||||
assert _shaped(outside["now"]) == {100: {"ford": 100}}
|
||||
assert {s["chainage_m"]: s["grid"] for s in outside["now"]}[20] is True # 격자 자리 옹벽 측점
|
||||
|
||||
|
||||
def test_카드_번호_안_겹침_떠난_끝_측점은_새_번호(out):
|
||||
ids = dict(out["shift"]["ids"])
|
||||
assert len(set(ids.values())) == len(ids)
|
||||
assert ids[87] == "station_000000087000" # 옹벽 끝 80 → 87
|
||||
assert ids[80] == "station_000000080000" # 그 자리 새 일반 측점
|
||||
assert ids[47] == "station_000000047000" # 관도 격자 40 을 떠남
|
||||
assert ids[40] == "station_000000040000"
|
||||
assert ids[157] == "station_000000150000" # 옛 자리가 빈 세월교 측점 = 옛 번호 그대로
|
||||
|
||||
|
||||
def test_초안_사본_표시_안_들고_감(out):
|
||||
assert out["shift"]["preview"] == 0 and out["outside"]["preview"] == 0
|
||||
Reference in New Issue
Block a user