- 관 세트 주인 측점 = 0.51 m 안 가장 가까운 측점(TS attachCulvertSets ↔ 파이썬 attach_culvert_specs) · 넘으면 연동 이웃에도 안 얹고 missed - 형상 얹기 한 벌(attachShapes ↔ attach_shapes) — 시종점 측점 자리인데 좌표가 노선 진행 반대쪽 0.51 m 넘게 바깥 = 「노선 밖」 · 안 얹음 · 안 얹은 것 돌려줌(서버 = 로그 경고) - 측점 갈아끼움 — 격자 아닌 자리면 앞 횡단 grid 표시 지움 · 갈아끼움 뒤 가게 초안 없어도 늘 다시 얹음 · 카드 · 종단 측점 이름 = 계획 제목 · 안 얹은 시설 알림(바뀐 때 한 번) - 시험: test_458_swap_attach(재현 A · D) · test_323_4 거울 moved 경우(상한 · 시종점 밖 · 연동 폭) · test_299 같은 키 견본 = 주인 0.51 m 안으로 · 273_2 흉내 돌려줌 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
6.3 KiB
JavaScript
193 lines
6.3 KiB
JavaScript
/* 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);
|
|
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 {
|
|
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));
|