- 알약 · 목록이 연 범위 첫 카드를 다시 누르면 「재누름 = 해제」로 노면공까지 풀리던 것 고침 - 고른 노면공 범위 안 카드는 재누름에도 그대로 · 풂은 빈 자리 · 목록 · 알약 · 범위 밖은 옛 규칙 - 시험: test_165_pavement_flow 에 재누름 단계 · 범위 밖 재누름 해제 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
349 lines
13 KiB
JavaScript
349 lines
13 KiB
JavaScript
/* 165 노면공 고름 일관성 헬퍼 — 진짜 `createB06StructuresPanel`(SPN)에 폼 · 횡단 카드 · 종단 알약을 흉내 내 붙이고
|
|
* 조사 K 재현 차례 6단계를 돌려 단계마다 {폼 · 알약 · 포장층 · 고른 카드 · 세션 sid} 를 표준출력 JSON 한 벌로.
|
|
* 카드 고름 리스너는 design_view_cross.ts 의 `setStationSelectListener` 본문을, 카드 · 알약 흉내는 B06 Section_View ·
|
|
* Section_View_Profile 의 `selectStation` · `focusStation` · `selectMark` · `showCard` 를 그대로 옮김(TS 는 그 자리 트랜스파일).
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
|
|
function loadTs(file, stubs = {}) {
|
|
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
const localRequire = (request) => {
|
|
if (stubs[request]) return stubs[request];
|
|
throw new Error(`stub 없음: ${request}`);
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
|
|
// 세션(structure-pick) — 메모리 한 벌
|
|
const store = new Map();
|
|
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
|
|
"../b_page_state": {
|
|
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
|
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
|
},
|
|
});
|
|
|
|
const PID = "p165";
|
|
const TYPES = [
|
|
{ type_id: "pavement_concrete", group: "G" },
|
|
{ type_id: "hazard_sign", group: "E" },
|
|
{ type_id: "pipe", group: "A", managed_by: "pipe_points" },
|
|
];
|
|
const PAVE = {
|
|
structure_id: "pave",
|
|
type_id: "pavement_concrete",
|
|
placement: "interval",
|
|
chainage_m: 30,
|
|
start_m: 30,
|
|
end_m: 250,
|
|
};
|
|
const PAVE2 = {
|
|
structure_id: "pave2",
|
|
type_id: "pavement_concrete",
|
|
placement: "interval",
|
|
chainage_m: 100,
|
|
start_m: 100,
|
|
end_m: 200,
|
|
};
|
|
const PIPES = [{ chainage_m: 80.17, facility: "pipe", source: "spacing", options: {} }];
|
|
// 카드 = 20 m 측점 + 관 측점(비정규)
|
|
const CARDS = [...Array.from({ length: 16 }, (_, i) => i * 20), 80.17]
|
|
.sort((a, b) => a - b)
|
|
.map((at) => ({ station_id: `s${at}`, chainage_m: at }));
|
|
const cardAt = (at) => CARDS.find((card) => Math.abs(card.chainage_m - at) < 0.01);
|
|
|
|
/* ── 폼 흉내(B05 패널 · loadForm / loadPipeForm / resetForm 의 알림 차례) ── */
|
|
let binding = null;
|
|
let structures = [];
|
|
const form = { id: null, pipe: null };
|
|
const loadForm = (target) => {
|
|
form.id = target?.structure_id ?? null;
|
|
form.pipe = null;
|
|
binding.onSelect(target);
|
|
};
|
|
const loadPipeForm = (chainageM) => {
|
|
const hadStructure = form.id !== null;
|
|
form.id = null;
|
|
form.pipe = chainageM;
|
|
if (hadStructure) binding.onSelect(null);
|
|
};
|
|
const resetForm = () => {
|
|
const hadPipe = form.pipe !== null;
|
|
form.pipe = null;
|
|
loadForm(null);
|
|
if (hadPipe) binding.onPipeSelect(null);
|
|
};
|
|
const node = { addEventListener() {}, querySelector: () => ({ append() {} }) };
|
|
const section = {
|
|
root: node,
|
|
listRoot: node,
|
|
facility: { root: node, setRevetSideLabels() {}, recount() {} },
|
|
setTypes() {},
|
|
setStructures(next) {
|
|
structures = next;
|
|
},
|
|
setPipeFacilities() {},
|
|
hasSelection: () => form.id !== null || form.pipe !== null,
|
|
selectById(id) {
|
|
if (id === null) {
|
|
if (form.id !== null || form.pipe !== null) resetForm();
|
|
return;
|
|
}
|
|
loadForm(structures.find((entry) => entry.structure_id === id) ?? null);
|
|
},
|
|
selectPipeByChainage(chainageM) {
|
|
if (chainageM === null) {
|
|
if (form.pipe !== null) resetForm();
|
|
return;
|
|
}
|
|
loadPipeForm(chainageM);
|
|
},
|
|
};
|
|
|
|
const panelModule = loadTs(
|
|
path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structures_Panel.ts"),
|
|
{
|
|
"../A00_Common/design/design_shared": {
|
|
bindDesignView: (_shared, _view, parts) => {
|
|
binding = parts.structures;
|
|
},
|
|
sharedStructures: () => section,
|
|
},
|
|
"../B05_Profile/B05_Profile_Api_Structures": {
|
|
fetchStructureTypes: async () => TYPES,
|
|
fetchStructures: async () => ({ structures: [] }),
|
|
readPendingStructures: () => current.structures,
|
|
pipesToStructureMarks: (pipes) =>
|
|
pipes.map((pipe) => ({
|
|
structure_id: `pipe-${pipe.chainage_m.toFixed(2)}`,
|
|
type_id: pipe.facility,
|
|
chainage_m: pipe.chainage_m,
|
|
start_m: null,
|
|
end_m: null,
|
|
})),
|
|
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
|
|
writePendingStructures() {},
|
|
},
|
|
"../B05_Profile/B05_Profile_Api_Pipes_Draft": {
|
|
readPendingPipes: () => null,
|
|
writePendingPipes() {},
|
|
},
|
|
"../B05_Profile/B05_Profile_UI_Drainage_Facility_Merge": { mergeFacilityOptions: (x) => x },
|
|
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
|
fetchDetailPipePoints: async () => ({ pipe_points: PIPES, basins: [] }),
|
|
},
|
|
"../common_util/common_util_summary_item": {
|
|
linkFields: () => ({}),
|
|
readGroupItems: (x) => x ?? {},
|
|
readSummaryItem: (x) => x ?? null,
|
|
},
|
|
"../B05_Profile/B05_Profile_UI_Structure_Pick_Session": { readStructurePick: () => null },
|
|
"../A00_Common/design/design_selection": selection,
|
|
"@ui/ui_template_elements": { showToast() {} },
|
|
"./B06_Section_UI_Adjust_Dock": {
|
|
adjustDockRoot: () => ({}),
|
|
refreshAdjustSlots() {},
|
|
resetAdjustDock() {},
|
|
setAdjustSideSlots() {},
|
|
},
|
|
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
|
|
},
|
|
);
|
|
|
|
/* ── 종단 알약 흉내(Section_View_Profile) ── */
|
|
let marks = [];
|
|
const profile = {
|
|
markedId: null,
|
|
pill: null,
|
|
selectMark(id) {
|
|
this.markedId = id;
|
|
this.pill = id;
|
|
},
|
|
showCard(card) {
|
|
if (!card) return;
|
|
const pipe = PIPES.find((entry) => Math.abs(entry.chainage_m - card.chainage_m) < 0.51);
|
|
// 관 측점 = 그 측점선 짝 알약 · 일반 측점 = 꺼짐 — 고른 구조물이 덮으면 지킴(53-1)
|
|
this.pill = pipe ? `pipe-${pipe.chainage_m.toFixed(2)}` : null;
|
|
const marked = marks.find((entry) => entry.structure_id === this.markedId);
|
|
if (!pipe && marked && selection.coversChainage(marked, card.chainage_m))
|
|
this.pill = this.markedId;
|
|
},
|
|
};
|
|
|
|
/* ── 횡단 카드 흉내(Section_View) ── */
|
|
let markTypes = TYPES;
|
|
const view = {
|
|
selected: null,
|
|
spanMarkId: null,
|
|
listener: null,
|
|
isPavement(id) {
|
|
const mark = marks.find((entry) => entry.structure_id === id);
|
|
return !!mark && markTypes.find((type) => type.type_id === mark.type_id)?.group === "G";
|
|
},
|
|
selectStation(id) {
|
|
// 고른 노면공 범위 안 카드 재누름 = 그대로(167-3 · Section_View `selectStation`)
|
|
const card = CARDS.find((entry) => entry.station_id === id);
|
|
const mark = marks.find((entry) => entry.structure_id === this.spanMarkId);
|
|
const held =
|
|
this.isPavement(this.spanMarkId) &&
|
|
card.chainage_m >= Math.min(mark.start_m, mark.end_m) - 0.01 &&
|
|
card.chainage_m <= Math.max(mark.start_m, mark.end_m) + 0.01;
|
|
if (this.selected === id && held) return;
|
|
const previous = this.selected;
|
|
this.selected = this.selected === id ? null : id;
|
|
profile.showCard(CARDS.find((card) => card.station_id === this.selected) ?? null);
|
|
if (this.selected !== previous) this.listener(this.selected);
|
|
},
|
|
focusStation(id) {
|
|
if (id === null) this.selected = null;
|
|
else if (this.selected !== id) this.selectStation(id);
|
|
else profile.showCard(CARDS.find((card) => card.station_id === id));
|
|
},
|
|
selectMark(id, span = true) {
|
|
this.spanMarkId = span || this.isPavement(id) ? id : null;
|
|
profile.selectMark(id);
|
|
},
|
|
};
|
|
|
|
let wallSelecting = false;
|
|
const focusStationAt = (at) => {
|
|
const nearest = CARDS.reduce((best, card) =>
|
|
Math.abs(card.chainage_m - at) < Math.abs(best.chainage_m - at) ? card : best,
|
|
);
|
|
view.focusStation(nearest.station_id);
|
|
};
|
|
|
|
const current = { structures: [PAVE] };
|
|
let panel = null;
|
|
|
|
/** design_view_cross.ts 카드 고름 리스너 본문(벽 → 소유 측점) — 그대로 옮김. */
|
|
const ownerOf = (card) => (wallOwner && card.chainage_m === wallOwner.card ? wallOwner : null);
|
|
let wallOwner = null;
|
|
view.listener = (stationId) => {
|
|
const target = stationId ? CARDS.find((card) => card.station_id === stationId) : null;
|
|
if (!target) {
|
|
panel.showAtChainage(null);
|
|
selection.selectDesign(PID, selection.NO_SELECTION, { from: "b06" });
|
|
return;
|
|
}
|
|
const owner = wallSelecting ? ownerOf(target) : null;
|
|
const opened = panel.showAtChainage(owner?.chainage_m ?? target.chainage_m);
|
|
if (!wallSelecting) selection.selectDesign(PID, opened, { from: "b06" });
|
|
const pipeAt = panel.currentChainage();
|
|
const mark = opened.structureId ?? (pipeAt === null ? null : `pipe-${pipeAt.toFixed(2)}`);
|
|
view.selectMark(mark, false);
|
|
};
|
|
|
|
async function build(list) {
|
|
current.structures = list;
|
|
store.clear();
|
|
form.id = null;
|
|
form.pipe = null;
|
|
view.selected = null;
|
|
view.spanMarkId = null;
|
|
profile.markedId = null;
|
|
profile.pill = null;
|
|
panel = panelModule.createB06StructuresPanel({
|
|
projectId: PID,
|
|
shared: { view: () => "cross" },
|
|
detail: () => ({ cross_sections: CARDS }),
|
|
stationInterval: () => 20,
|
|
focusChainage: (at) => {
|
|
if (!wallSelecting) focusStationAt(at);
|
|
},
|
|
markStructure: (id) => view.selectMark(id),
|
|
onMarks: (next, types) => {
|
|
marks = next;
|
|
markTypes = types;
|
|
},
|
|
});
|
|
await panel.load();
|
|
}
|
|
|
|
/** 지금 상태 한 줄 — 포장층 = 고른 노면공 범위 안 카드 수(포장층 is-active 와 같은 판정). */
|
|
function snap() {
|
|
const mark = marks.find((entry) => entry.structure_id === view.spanMarkId);
|
|
const pave = mark && view.isPavement(mark.structure_id);
|
|
const inSpan = (at) =>
|
|
at >= Math.min(mark.start_m, mark.end_m) - 0.01 &&
|
|
at <= Math.max(mark.start_m, mark.end_m) + 0.01;
|
|
const card = CARDS.find((entry) => entry.station_id === view.selected);
|
|
return {
|
|
form: form.id ?? (form.pipe === null ? null : `pipe-${form.pipe.toFixed(2)}`),
|
|
pill: profile.pill,
|
|
paveLayer: pave ? mark.structure_id : null,
|
|
paveCards: pave ? CARDS.filter((entry) => inSpan(entry.chainage_m)).length : 0,
|
|
card: card ? card.chainage_m : null,
|
|
sid: selection.readSelection(PID)?.structureId ?? null,
|
|
};
|
|
}
|
|
|
|
/** 좌측 목록 / 종단 알약 = 폼에 그 구조물(B05 `loadForm` · SVP `edit.selectStructure`). */
|
|
const listPick = (id) => loadForm(structures.find((entry) => entry.structure_id === id));
|
|
const pillPick = (id) => {
|
|
profile.pill = id; // 종단 그림이 먼저 그 알약을 켬
|
|
panel.selectStructure(id);
|
|
};
|
|
const cardPick = (at) => view.selectStation(cardAt(at).station_id);
|
|
/** 벽 누름 — SPS `revetOffset.select` 감싸개(소유 측점 폼) 뒤 카드 고름(벽 고르는 턴). */
|
|
const wallPick = (cardAtM, ownerM) => {
|
|
wallOwner = { card: cardAtM, chainage_m: ownerM };
|
|
wallSelecting = true;
|
|
selection.selectDesign(PID, panel.selectionAt(ownerM), { partKey: "inlet", from: "b06" });
|
|
panel.showAtChainage(ownerM);
|
|
if (view.selected !== cardAt(cardAtM).station_id) cardPick(cardAtM);
|
|
wallSelecting = false;
|
|
};
|
|
|
|
(async () => {
|
|
const out = {};
|
|
await build([PAVE, { structure_id: "sign", type_id: "hazard_sign", chainage_m: 127.86 }]);
|
|
listPick("pave");
|
|
out.step1 = snap();
|
|
cardPick(40); // 고름이 이미 연 범위 첫 카드 재누름(브레인 ORCA ②)
|
|
out.step2re = snap();
|
|
cardPick(140);
|
|
out.step2 = snap();
|
|
cardPick(100);
|
|
out.step2b = snap();
|
|
wallPick(100, 80.17); // 고른 카드의 벽 = 카드 고름 리스너 안 돎
|
|
out.step3 = snap();
|
|
pillPick("pave");
|
|
out.step4 = snap();
|
|
cardPick(80); // 관 자리 카드 = 관(지금처럼)
|
|
out.pipeCard = snap();
|
|
pillPick("pave");
|
|
cardPick(280); // 범위 밖 = 모두 풂
|
|
out.outside = snap();
|
|
cardPick(280); // 범위 밖 카드 재누름 = 카드 풂(옛 규칙 그대로)
|
|
out.outsideRe = snap();
|
|
|
|
// 5. B05 알약 → B06 복귀 — 넘김값 {범위 첫 카드 · sid} · `restoreStructurePick` 의 focusStation 차례
|
|
await build([PAVE]);
|
|
selection.selectDesign(PID, { chainageM: 40, structureId: "pave" }, { from: "b05" });
|
|
panel.prefer("pave");
|
|
view.focusStation(cardAt(40).station_id);
|
|
panel.prefer(null);
|
|
panel.selectStructure("pave");
|
|
out.step5 = snap();
|
|
|
|
// 6. 같은 측점에 노면공 둘 — 목록 둘째 → 그 층만 · 범위 안 카드 눌러도 둘째 그대로
|
|
await build([PAVE, PAVE2]);
|
|
listPick("pave2");
|
|
out.step6 = snap();
|
|
cardPick(160);
|
|
out.step6b = snap();
|
|
process.stdout.write(JSON.stringify(out));
|
|
})().catch((error) => {
|
|
process.stderr.write(String(error?.stack ?? error));
|
|
process.exit(1);
|
|
});
|