- 초안 상세(카드)도 캐시와 같은 attachShapes 를 follow 뒤 현황으로 부름 · atChainage 지움 · 이름 = 측점 계획 제목 - cross 입력 넘김 = 사용자 칸 · 카드 번호만 · 포장 · 기울임 · 하강 = 새 자리 그림에서(crossWithShapes · 점 창도) - 초안 자리 시설 측점 짝 = 등록 열쇠(B06 측점 열쇠 목록 · 처음 본 열쇠) · 차례 짝 지움 - writeFacilities = 지금 가게 값과 견줌(되돌리기 뒤 같은 조작도 씀) - 시험 test_457_draft_card(재현 E · F · G · H) · 365 · 433 · 440 도우미 새 규칙으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
276 lines
9.2 KiB
JavaScript
276 lines
9.2 KiB
JavaScript
/* 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));
|