Files
Aislo/resources/tester/helper_457_draft_card.cjs
T
eomsangdonandClaude Opus 5.5 4a749584e6 fix(457 노선 바뀜 초안 카드): 그림 · 이름 · 횡단 맥락 = 지금 현황을 새 측점에 다시 얹음 · 등록 열쇠 짝 · 가게 쓰기 = 지금 가게 값과 견줌
- 초안 상세(카드)도 캐시와 같은 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>
2026-10-18 22:43:05 +09:00

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));