Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
305 lines
12 KiB
JavaScript
305 lines
12 KiB
JavaScript
/* PLAN 294-7 · 295-4 단위검증 헬퍼 — 물넘이 콘크리트 누름(얇은 판정면 · 자기 물넘이 고름) ·
|
|
* 보라 선 첫 누름이 남은 부재 고름을 안 되살림. 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림.
|
|
* 짝 = `test_294_7_ford_slab_pick.py`.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const B06 = path.join(ROOT, "B06_Section");
|
|
|
|
class FakeStorage {
|
|
getItem(key) {
|
|
return Object.prototype.hasOwnProperty.call(this, key) ? this[key] : null;
|
|
}
|
|
setItem(key, value) {
|
|
this[key] = String(value);
|
|
}
|
|
removeItem(key) {
|
|
delete this[key];
|
|
}
|
|
}
|
|
globalThis.window = { sessionStorage: new FakeStorage(), localStorage: new FakeStorage() };
|
|
globalThis.document = { addEventListener: () => {}, removeEventListener: () => {} };
|
|
globalThis.requestAnimationFrame = (fn) => fn();
|
|
|
|
const STUBS = {
|
|
"./B06_Section_UI_Adjust_Dock": { refreshAdjustSlots: () => {} },
|
|
"./B06_Section_UI_Cross_Design_Pick": { setBermPickHost: () => {} },
|
|
"./B06_Section_UI_Page_Persist": { BERM_TYPE_ID: "berm" },
|
|
};
|
|
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
|
|
function loadTs(full) {
|
|
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
const localRequire = (request) => {
|
|
if (STUBS[request]) return STUBS[request];
|
|
if (request.endsWith(".css")) return {};
|
|
return loadTs(resolve(path.join(path.dirname(full), request)));
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
const SP = loadTs(path.join(B06, "B06_Section_UI_Cross_Shape_Pick.ts"));
|
|
const PO = loadTs(path.join(B06, "B06_Section_UI_Cross_Pick_Order.ts"));
|
|
const SEL = loadTs(path.join(B06, "B06_Section_UI_Page_Structures_Selection.ts"));
|
|
const DS = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"));
|
|
const out = {};
|
|
|
|
/* ── ① 얇은 콘크리트 판정면 — 기본 배율 4 px · 노면선 y=100 ─────────────── */
|
|
const slab = "100,100 200,96 200,100 100,104"; // 기운 노면 · 두께 4
|
|
const thick = SP.thickenDown(slab);
|
|
const pts = (s) => s.split(" ").map((p) => p.split(",").map(Number));
|
|
const inside = (p, ring) => {
|
|
let inn = false;
|
|
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
const [ax, ay] = ring[i];
|
|
const [bx, by] = ring[j];
|
|
if (ay > p[1] !== by > p[1] && p[0] < ((bx - ax) * (p[1] - ay)) / (by - ay) + ax) inn = !inn;
|
|
}
|
|
return inn;
|
|
};
|
|
const gap = (p, [a, b]) => {
|
|
const d = [b[0] - a[0], b[1] - a[1]];
|
|
const t = Math.max(
|
|
0,
|
|
Math.min(1, ((p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1]) / (d[0] ** 2 + d[1] ** 2)),
|
|
);
|
|
return Math.hypot(p[0] - a[0] - t * d[0], p[1] - a[1] - t * d[1]);
|
|
};
|
|
// 카드 층 흉내 — 보라 노면선 판정(굵기 10 = 위아래 5) · 형상 판정면 · 중심 십자 없음
|
|
const node = (name, classes, points) => ({
|
|
name,
|
|
classList: { contains: (c) => classes.includes(c) },
|
|
getAttribute: (a) => (a === "points" ? points : null),
|
|
});
|
|
function stacked(shapePoints) {
|
|
const kids = [];
|
|
const layer = {
|
|
children: kids,
|
|
append: (...nodes) =>
|
|
nodes.forEach((n) => {
|
|
const at = kids.indexOf(n);
|
|
if (at >= 0) kids.splice(at, 1);
|
|
kids.push(n);
|
|
}),
|
|
addEventListener: () => {},
|
|
};
|
|
layer.append(
|
|
node("road", ["b06-chart__design-hit"], "100,100 200,96"),
|
|
node("slab", ["b06-chart__shape-hit"], shapePoints),
|
|
);
|
|
PO.stackPickTargets(layer);
|
|
return kids;
|
|
}
|
|
const topAt = (p, kids) =>
|
|
[...kids]
|
|
.reverse()
|
|
.find((n) =>
|
|
n.classList.contains("b06-chart__design-hit")
|
|
? gap(p, pts(n.getAttribute("points"))) <= 5
|
|
: inside(p, pts(n.getAttribute("points"))),
|
|
)?.name ?? null;
|
|
const now = stacked(thick);
|
|
const old = stacked(slab);
|
|
out.slab = {
|
|
thick,
|
|
bottomAt150: (pts(thick)[2][1] + pts(thick)[3][1]) / 2, // 아랫변 가운데
|
|
justBelow: topAt([150, 106], now), // 노면선 6 px 아래
|
|
justBelowOld: topAt([150, 106], old),
|
|
onLine: topAt([150, 99], now), // 노면선 위 = 보라 선(끌기 · 185)
|
|
insideOld: topAt([150, 101], old), // 옛 판정면 안쪽도 보라 띠에 덮임
|
|
order: now.map((n) => n.name),
|
|
tallUnchanged: SP.thickenDown("0,0 30,0 30,20 0,20") === "0,0 30,0 30,20 0,20",
|
|
};
|
|
|
|
/* ── ② 형상 누름 길 — 물넘이 id = 그 누가거리 · 맹암거 = 알림 ─────────────── */
|
|
const routed = [];
|
|
const from = { dispatchEvent: (event) => routed.push(["event", event.detail]) };
|
|
globalThis.CustomEvent = class {
|
|
constructor(type, init) {
|
|
this.type = type;
|
|
this.detail = init?.detail;
|
|
}
|
|
};
|
|
const route = SP.shapePickRoute(from, (at) => routed.push(["pipe", at]));
|
|
route("pipe-90.00");
|
|
route("ud-1");
|
|
out.routed = routed;
|
|
|
|
/* ── ③ 폼에 올릴 시설(주인 측점) — 연동 관 95 m 가 걸친 물넘이 90 m 카드 ──── */
|
|
const sections = [
|
|
{ chainage_m: 80, station_id: "s80", culvert: { c: 80 }, ford_pavement: { chainage_m: 90 } },
|
|
{ chainage_m: 90, station_id: "s90", ford_pavement: { chainage_m: 90 } },
|
|
{ chainage_m: 95, station_id: "s95", culvert: { c: 95 } },
|
|
{ chainage_m: 110, station_id: "s110", culvert: { c: 110 } },
|
|
];
|
|
const log = [];
|
|
const stationControls = {
|
|
revetOffset: { select: (at, key) => log.push(["select", at, key]), highlightFor: () => null },
|
|
ford: { select: () => {} },
|
|
box: { select: () => {} },
|
|
structureSpan: {
|
|
// 연동 관 95 m 의 연장(유출 흙막이 10 m)이 90 m 카드까지 옴
|
|
ownerOf: (s) =>
|
|
s.culvert ? s : s.chainage_m >= 85 && s.chainage_m <= 105 ? sections[2] : null,
|
|
},
|
|
inletStructure: { set: () => {}, valueFor: () => "auto" },
|
|
};
|
|
const panel = {
|
|
root: { addEventListener: () => {}, querySelectorAll: () => [], contains: () => false },
|
|
facility: { onInletStructureChange: () => {}, setInletStructure: () => {} },
|
|
currentChainage: () => null,
|
|
showAtChainage: (at) => log.push(["form", at]),
|
|
releasePipe: () => log.push(["release"]),
|
|
getStructures: () => [],
|
|
selectStructure: () => {},
|
|
};
|
|
SEL.wireStructureSelection(
|
|
stationControls,
|
|
() => ({ cross_sections: sections }),
|
|
panel,
|
|
(at, key) => log.push(["remember", at, key]),
|
|
);
|
|
const run = (...args) => {
|
|
log.length = 0;
|
|
stationControls.revetOffset.select(...args);
|
|
return log.filter(([kind]) => kind !== "select");
|
|
};
|
|
out.owner = {
|
|
slabOwnCard: run(90, "pipe"), // 형상 누름 · 되살림 = 그 물넘이 90(옛 = 연동 관 95)
|
|
slabHint: run(90, "pipe", 90),
|
|
slabLinkedCard: run(80, "pipe", 90), // 자기 관 있는 이웃 카드의 연동 콘크리트 = 물넘이
|
|
pipeBody: run(80, "pipe"), // 관 몸통 = 자기 관 그대로
|
|
chute: run(90, "chute"),
|
|
};
|
|
|
|
/* ── ④ 보라 선 첫 누름(295-4) — 남은 부재키(도수로 띠)를 카드 고름이 지키지 않게 ── */
|
|
const P = "p1";
|
|
const seed = () => {
|
|
DS.selectDesign(P, { chainageM: null, structureId: null });
|
|
DS.selectDesign(P, { chainageM: 110, structureId: null }, { partKey: "chute" });
|
|
};
|
|
seed();
|
|
DS.selectDesign(P, { chainageM: 110, structureId: null }, { from: "b06" }); // 옛 길 = 카드 고름만
|
|
const oldKey = DS.readSelection(P)?.partKey ?? null;
|
|
seed();
|
|
DS.selectDesign(P, { chainageM: 110, structureId: null }, { partKey: "", from: "b06" }); // 선 첫 누름 = 풂 먼저
|
|
DS.selectDesign(P, { chainageM: 110, structureId: null }, { from: "b06" }); // 그다음 카드 고름
|
|
out.firstLine = { oldKey, newKey: DS.readSelection(P)?.partKey ?? null };
|
|
|
|
/* ── ⑤ 안 골라진 카드 첫 누름(294-8 · 295-4) — 페이지 흐름: 부재 고름 → 카드 고름 리스너 → 폼 엶 ──
|
|
* 폼은 열 때 두 번 그림(종류 칸 = 기본값 → 시설 값) — 둘째 그림에 도수로 상자가 섬. */
|
|
const tick = () => new Promise((done) => setTimeout(done, 0));
|
|
const flowSections = [
|
|
{ chainage_m: 90, station_id: "s90", ford_pavement: { chainage_m: 90, chute: {} } },
|
|
{ chainage_m: 95, station_id: "s95", culvert: { c: 95, chute: {} } },
|
|
{ chainage_m: 110, station_id: "s110", ford_pavement: { chainage_m: 110, chute: {} } },
|
|
];
|
|
const flowLog = [];
|
|
const box = (id) => {
|
|
const el = {
|
|
dataset: { specContainer: id, specIndex: "0" },
|
|
closest: (sel) => (sel.startsWith("fieldset") ? el : null),
|
|
};
|
|
return el;
|
|
};
|
|
let shown = null;
|
|
let boxes = [];
|
|
const handlers = {};
|
|
let flowPicked = null;
|
|
const flowControls = {
|
|
revetOffset: {
|
|
select: (at, key) => {
|
|
flowLog.push(["select", at, key]);
|
|
flowPicked = key ? { at, key } : null;
|
|
},
|
|
highlightFor: (s) => (flowPicked?.at === s.chainage_m ? flowPicked.key : null),
|
|
},
|
|
ford: { select: () => {} },
|
|
box: { select: () => {} },
|
|
structureSpan: {
|
|
// 연동 관 95 m 의 유출 흙막이가 4+10(90 m) 카드까지 옴(299)
|
|
ownerOf: (s) => (s.culvert ? s : s.chainage_m === 90 ? flowSections[1] : null),
|
|
},
|
|
inletStructure: { set: () => {}, valueFor: () => "auto" },
|
|
};
|
|
const paint = (ids) => {
|
|
boxes = ids.map(box);
|
|
handlers.specpaint?.forEach((fn) => fn());
|
|
};
|
|
const flowPanel = {
|
|
root: {
|
|
addEventListener: (type, fn) => (handlers[type] ??= []).push(fn),
|
|
querySelectorAll: () => boxes,
|
|
contains: () => false,
|
|
},
|
|
facility: { onInletStructureChange: () => {}, setInletStructure: () => {} },
|
|
currentChainage: () => shown,
|
|
showAtChainage: (at) => {
|
|
flowLog.push(["form", at]);
|
|
shown = at;
|
|
if (at === null) return { chainageM: null, structureId: null };
|
|
paint(at === 95 ? ["pipe"] : ["slope"]); // 종류 칸 = 기본값
|
|
paint(at === 95 ? ["pipe", "chute_add"] : ["slope", "chute"]); // 시설 값 = 도수로 상자 섬
|
|
return { chainageM: at, structureId: null };
|
|
},
|
|
releasePipe: () => flowLog.push(["release"]),
|
|
getStructures: () => [],
|
|
selectStructure: () => {},
|
|
};
|
|
const flowWired = SEL.wireStructureSelection(
|
|
flowControls,
|
|
() => ({ cross_sections: flowSections }),
|
|
flowPanel,
|
|
(at, key) => flowLog.push(["remember", at, key]),
|
|
);
|
|
// 카드 고름 리스너(`design_view_cross` setStationSelectListener) — 폼 자리는 배선이 돌려준 함수 그대로
|
|
const cardSelected = (target) => flowPanel.showAtChainage(flowWired.formChainageOf(target));
|
|
const esc = () => {
|
|
flowPicked = null;
|
|
shown = null;
|
|
boxes = [];
|
|
};
|
|
const flow = async (before, press) => {
|
|
esc();
|
|
before?.();
|
|
await tick();
|
|
flowLog.length = 0;
|
|
press();
|
|
const got = flowLog.map((entry) => [...entry]);
|
|
await tick();
|
|
return got;
|
|
};
|
|
// 형상 누름 = toggleRevet("pipe", 90) — 안 골라진 카드면 부재 고름 → onSelect
|
|
const slabPress = () => {
|
|
flowControls.revetOffset.select(90, "pipe", 90);
|
|
cardSelected(flowSections[0]);
|
|
};
|
|
// 보라 선 첫 누름 = pickLine — 남은 부재 풂 → onSelect
|
|
const linePress = () => {
|
|
flowControls.revetOffset.select(110, null);
|
|
cardSelected(flowSections[2]);
|
|
};
|
|
|
|
(async () => {
|
|
// 다시 누름 = 풂 — 고름 표시(마이크로태스크)가 풀린 뒤
|
|
await Promise.resolve();
|
|
out.owner.release = run(90, null, 90);
|
|
out.first = {
|
|
slabAfterEsc: await flow(null, slabPress),
|
|
slabAfterOther: await flow(() => flowPanel.showAtChainage(95), slabPress),
|
|
lineAfterEsc: await flow(null, linePress),
|
|
lineAfterOther: await flow(() => flowPanel.showAtChainage(95), linePress),
|
|
};
|
|
// 폼 안 [추가] · 단 늘림 = 그 상자 고름 그대로(54-30)
|
|
flowPanel.showAtChainage(95);
|
|
flowLog.length = 0;
|
|
paint(["pipe", "chute_add", "soil_add"]);
|
|
out.first.addBox = flowLog.map((entry) => [...entry]);
|
|
process.stdout.write(JSON.stringify(out));
|
|
})();
|