- 부품 상자 common_util_structure_parts.json(갈래마다 부품 · 읽는 칸 · 자리꼴 · 정본 · 42종 처음값) · 표 읽기 common_util_structure_catalog.ts / .py 짝(정의 반영 ⊕ 처음값 ⊕ 부품 끔) · 거울 시험 test_322_1_catalog_mirror - 목록: pickGroups 군 차례 = 구조물군 열쇠 · 페이지 거르기 listPick(B05 both · B06 both + b06) · HIDDEN_PICK_GROUPS · HIDDEN_PICK_TYPES · pickable 걷음(옛 대피 셋 asRoad 그대로) - 측점: structureStationMarks · pipeStationMarks · is_station_planting_type · resolve_extra_stations = 표 횡단도(anchor · span3 · none) - 카드 이름: cardTitle / card_title 한 공식(빈칸 = 그 종류 정의 기본값) · 표시 이름이 정의 관종도 앎 - 형상: 벽 = shape wall(형태 = 형상칸.form · 물빼기 = 형상칸.weep) · 포장 = shape pavement(「사용 중」 거르기 없앰) · 맹암거 · 사토 · 관 지점 시설 세트 = shape - 3D 몸체 = 그림 자료 주인 종류의 표 3D 부품 · 알약 띠 = 표 자리꼴 구간(관 지점 시설은 알약만) - ★1 노출형 · 개거 횡단도 none · ★2 쇄석 화면에서도 포장층 · ★3 기슭막이 표 자리꼴 interval(정의 파일 그대로) · ★4 코드 숨김 10종 list hidden · ★5 관 빈칸 D800 → 정의 기본 D1000 - 옛 시험: 도움 파일 로더 27개가 표 모듈 공용 한 벌(helper_322_catalog_module) 씀 · 기대값 고침(272_1 · b05_extra_stations · roundtrip · 314 · 52_7 · walls 거울 · wall_drain_pipe) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
459 lines
16 KiB
JavaScript
459 lines
16 KiB
JavaScript
/* 231-1 노면공 범위 고름 — 「측점만 남은 고름」이 B05↔B06 오감 · 다시 세움에서 노면공 범위를 켜지 않는지.
|
|
* 진짜 `createB06StructuresPanel` · `applyStructurePick` · `wireStructurePick` · `readStructurePick` 를 Node 로 돌리고
|
|
* 폼 · 카드 · 알약 흉내는 helper_165 와 같은 꼴(공용 폼 `selectAt` = B05 패널 본문 그대로). 표준출력 JSON 한 벌.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
|
|
|
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 (CATALOG.isCatalogRequest(request)) return CATALOG;
|
|
if (stubs[request]) return stubs[request];
|
|
throw new Error(`stub 없음: ${request}`);
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
|
|
global.requestAnimationFrame = (fn) => fn();
|
|
|
|
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 pickSession = loadTs(
|
|
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts"),
|
|
{ "../A00_Common/design/design_selection": selection },
|
|
);
|
|
const structurePick = loadTs(
|
|
path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structure_Pick.ts"),
|
|
{ "../A00_Common/design/design_selection": selection },
|
|
);
|
|
|
|
const PID = "p231";
|
|
const TYPES = [
|
|
{ type_id: "pavement_concrete", group: "G" },
|
|
{ type_id: "retaining_wall", group: "C", placement: "interval" }, // 실제 등록부 = C 구간형(측점 심음)
|
|
{ type_id: "hazard_sign", group: "E" },
|
|
{ type_id: "pipe", group: "A", managed_by: "pipe_points" },
|
|
];
|
|
const groupOf = (typeId) => TYPES.find((type) => type.type_id === typeId)?.group;
|
|
const atStation = (entry) => selection.coversAtStation(groupOf(entry.type_id));
|
|
const PAVE = {
|
|
structure_id: "pave",
|
|
type_id: "pavement_concrete",
|
|
placement: "interval",
|
|
chainage_m: 30,
|
|
start_m: 30,
|
|
end_m: 250,
|
|
};
|
|
const WALL = {
|
|
structure_id: "wall",
|
|
type_id: "retaining_wall",
|
|
placement: "interval",
|
|
chainage_m: 100,
|
|
start_m: 100,
|
|
end_m: 160,
|
|
};
|
|
const SIGN = { structure_id: "sign", type_id: "hazard_sign", chainage_m: 127.86 };
|
|
const PIPES = [{ chainage_m: 80.17, facility: "pipe", source: "spacing", options: {} }];
|
|
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 · selectAt 본문) ── */
|
|
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);
|
|
},
|
|
resolveAt: (sel, coverable) => selection.resolveSelection(sel, PIPES, structures, coverable),
|
|
selectAt(sel, coverable) {
|
|
const hit = selection.resolveSelection(sel, PIPES, structures, coverable);
|
|
if (hit.pipeChainageM !== null) this.selectPipeByChainage(hit.pipeChainageM);
|
|
else this.selectById(hit.structureId);
|
|
return hit;
|
|
},
|
|
};
|
|
|
|
const current = { structures: [] };
|
|
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 },
|
|
"../B05_Profile/B05_Profile_UI_Drainage_Ford_Fill": {
|
|
fordWaitsOf: () => new Set(),
|
|
waitsFordHeight: () => false,
|
|
},
|
|
"../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": pickSession,
|
|
"../A00_Common/design/design_selection": selection,
|
|
"@ui/ui_template_elements": { showToast() {} },
|
|
// 관 기준 측점 정수 맞춤(234-1) — 일반 측점 없으면 정수 m 만
|
|
"../common_util/common_util_structure_station": {
|
|
snapStructureStation: (value) => Math.floor(value + 0.5),
|
|
},
|
|
"./B06_Section_UI_Adjust_Dock": {
|
|
adjustDockRoot: () => ({}),
|
|
refreshAdjustSlots() {},
|
|
resetAdjustDock() {},
|
|
setAdjustSideSlots() {},
|
|
},
|
|
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
|
|
// 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌)
|
|
"../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs(
|
|
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"),
|
|
{
|
|
"../A00_Common/touch_bridge": { pickPx: (px) => px }, // 기기대응 4-4 — 마우스 반경 그대로
|
|
"@ui/ui_template_context_menu": { createMapContextMenu() {} },
|
|
"./B05_Profile_UI_IrregularStations": {
|
|
irregularStationId: (id) => `irregular:${id}`,
|
|
isPipeStation: () => false,
|
|
},
|
|
"./B05_Profile_Api_Structures": {
|
|
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
|
|
},
|
|
"../A00_Common/design/design_selection": selection,
|
|
"../common_util/common_util_structure_station": loadTs(
|
|
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
|
|
),
|
|
},
|
|
),
|
|
},
|
|
);
|
|
|
|
/* ── 종단 알약 · 횡단 카드 흉내(helper_165 와 같음) ── */
|
|
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);
|
|
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;
|
|
},
|
|
};
|
|
const isPavement = (id) => {
|
|
const mark = marks.find((entry) => entry.structure_id === id);
|
|
return !!mark && groupOf(mark.type_id) === "G";
|
|
};
|
|
const view = {
|
|
selected: null,
|
|
spanMarkId: null,
|
|
listener: null,
|
|
selectStation(id) {
|
|
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);
|
|
},
|
|
revealStation() {
|
|
this.selected = null;
|
|
profile.showCard(null);
|
|
},
|
|
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 || isPavement(id) ? id : null;
|
|
profile.selectMark(id);
|
|
},
|
|
};
|
|
const focusStationAt = (at, revealOnly = false) => {
|
|
const nearest = CARDS.reduce((best, card) =>
|
|
Math.abs(card.chainage_m - at) < Math.abs(best.chainage_m - at) ? card : best,
|
|
);
|
|
if (revealOnly) view.revealStation(nearest.station_id);
|
|
else view.focusStation(nearest.station_id);
|
|
};
|
|
|
|
let panel = null;
|
|
/** design_view_cross.ts 카드 고름 리스너 본문(벽 누름 없음). */
|
|
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 preferred = panel.preferred() !== null;
|
|
const opened = panel.showAtChainage(target.chainage_m);
|
|
const picked = preferred ? opened : { chainageM: opened.chainageM, structureId: null };
|
|
selection.selectDesign(PID, picked, { from: "b06" });
|
|
const pipeAt = panel.currentChainage();
|
|
const mark = opened.structureId ?? (pipeAt === null ? null : `pipe-${pipeAt.toFixed(2)}`);
|
|
view.selectMark(mark, false);
|
|
};
|
|
|
|
/** 새로 세움 — keep = 새로 고침(F5 · 세션은 남고 화면 · 폼은 처음부터). */
|
|
async function build(list, keep = false) {
|
|
current.structures = list;
|
|
if (!keep) 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, revealOnly) => focusStationAt(at, revealOnly),
|
|
markStructure: (id, span) => view.selectMark(id, span),
|
|
onMarks: (next) => {
|
|
marks = next;
|
|
},
|
|
});
|
|
await panel.load();
|
|
}
|
|
|
|
/** design_view_cross.ts `restoreStructurePick` 의 focusStation 차례 그대로. */
|
|
function restore() {
|
|
const handoff = pickSession.readStructurePick(PID);
|
|
structurePick.applyStructurePick(
|
|
handoff,
|
|
{ cross_sections: CARDS },
|
|
{
|
|
focusStation: (stationId) => {
|
|
panel.prefer(handoff?.structureId ?? null);
|
|
view.focusStation(stationId);
|
|
panel.prefer(null);
|
|
const sid = handoff?.structureId;
|
|
if (sid && structures.some((entry) => entry.structure_id === sid))
|
|
panel.selectStructure(sid);
|
|
},
|
|
refreshCard() {},
|
|
revetSelect() {},
|
|
fordSelect() {},
|
|
boxSelect() {},
|
|
},
|
|
structures,
|
|
);
|
|
}
|
|
|
|
/** B05 로 갔다 B06 으로 돌아옴 — B05 `restorePick`(폼 비었을 때만 · id 길) → B06 onShow(load → 되얹기). */
|
|
async function roundTrip() {
|
|
const pick = pickSession.readStructurePick(PID);
|
|
if (!section.hasSelection() && pick?.structureId)
|
|
section.selectAt({ chainageM: pick.at, structureId: pick.structureId });
|
|
await panel.load();
|
|
restore();
|
|
}
|
|
|
|
/** 새로 고침(F5) — 첫 마운트 `load().then(restoreStructurePick)` 차례(231-2). */
|
|
async function refresh() {
|
|
await build(current.structures, true);
|
|
restore();
|
|
}
|
|
|
|
function snap() {
|
|
const mark = marks.find((entry) => entry.structure_id === view.spanMarkId);
|
|
const pave = mark && isPavement(mark.structure_id);
|
|
const card = CARDS.find((entry) => entry.station_id === view.selected);
|
|
return {
|
|
form: form.id ?? (form.pipe === null ? null : `pipe-${form.pipe.toFixed(2)}`),
|
|
paveLayer: pave ? mark.structure_id : null,
|
|
span: view.spanMarkId,
|
|
card: card ? card.chainage_m : null,
|
|
sid: selection.readSelection(PID)?.structureId ?? null,
|
|
};
|
|
}
|
|
|
|
const cardPick = (at) => view.selectStation(cardAt(at).station_id);
|
|
const listPick = (id) => loadForm(structures.find((entry) => entry.structure_id === id));
|
|
/** 좌측 목록 관 줄 — B05 패널 `onSelectPipe`(loadPipeForm → onPipeSelect). */
|
|
const listPipe = (at) => {
|
|
loadPipeForm(at);
|
|
binding.onPipeSelect(at);
|
|
};
|
|
|
|
/** B05 3D 부재 고름 — 진짜 `wireStructurePick` 에 B05 와 같은 거름(design_view_profile). */
|
|
function pick3d(chainageM) {
|
|
const controls = {};
|
|
pickSession.wireStructurePick(controls, PID, section, () => undefined, atStation);
|
|
controls.onPick({ chainageM, kind: "revet", key: "own" });
|
|
}
|
|
|
|
(async () => {
|
|
const out = {};
|
|
// A. 노면공 범위 안 빈 카드 → 오감
|
|
await build([PAVE, SIGN]);
|
|
cardPick(120);
|
|
out.cardBefore = snap();
|
|
await roundTrip();
|
|
out.cardAfter = snap();
|
|
await roundTrip();
|
|
out.cardAfter2 = snap();
|
|
|
|
// B. 같은 카드에서 목록 표지 → 오감(세션 = 측점만 그대로)
|
|
await build([PAVE, SIGN]);
|
|
cardPick(120);
|
|
listPick("sign");
|
|
out.signPick = snap();
|
|
await roundTrip();
|
|
out.signAfter = snap();
|
|
|
|
// C. 목록 노면공 → 오감 = 그대로 범위 강조(id 길)
|
|
await build([PAVE, SIGN]);
|
|
listPick("pave");
|
|
out.paveBefore = snap();
|
|
await roundTrip();
|
|
out.paveAfter = snap();
|
|
|
|
// D. 옹벽 범위 안 카드 = 그 카드 그대로(기준 카드 100 으로 안 옮김) · 오감 뒤도
|
|
await build([PAVE, WALL]);
|
|
cardPick(120);
|
|
out.wallCard = snap();
|
|
await roundTrip();
|
|
out.wallAfter = snap();
|
|
listPick("wall"); // 목록 고름은 기준 카드로 감(그대로)
|
|
out.wallList = snap();
|
|
|
|
// E. 관 80.17 넘김(측점만) = 카드 80.17(이웃 80 아님)
|
|
await build([PAVE]);
|
|
selection.selectDesign(PID, { chainageM: 80.17, structureId: null }, { from: "b05" });
|
|
restore();
|
|
out.pipeHandoff = snap();
|
|
|
|
// F. B05 3D 부재 고름 — 노면공 범위 안이라도 노면공 id 를 적지 않음
|
|
await build([PAVE, WALL]);
|
|
pick3d(120);
|
|
out.pick3dWall = { sid: snap().sid, form: snap().form };
|
|
await build([PAVE]);
|
|
pick3d(200);
|
|
out.pick3dBare = { sid: snap().sid, form: snap().form };
|
|
await roundTrip();
|
|
out.pick3dAfter = snap();
|
|
|
|
// 231-2 목록 고름도 늘 고름 기록 — 카드 120 → 목록 관 · 표지 · 옹벽 → 오감 · 새로 고침
|
|
await build([PAVE, WALL, SIGN]);
|
|
cardPick(120);
|
|
listPipe(80.17);
|
|
out.listPipe = snap();
|
|
await roundTrip();
|
|
out.listPipeTrip = snap();
|
|
await refresh();
|
|
out.listPipeF5 = snap();
|
|
|
|
await build([PAVE, SIGN]);
|
|
cardPick(120);
|
|
listPick("sign"); // 같은 카드 120 — 카드 리스너 안 돎
|
|
out.listSign = snap();
|
|
await roundTrip();
|
|
out.listSignTrip = snap();
|
|
await refresh();
|
|
out.listSignF5 = snap();
|
|
|
|
await build([PAVE, WALL]);
|
|
cardPick(120);
|
|
listPick("wall");
|
|
out.listWall = snap();
|
|
await roundTrip();
|
|
out.listWallTrip = snap();
|
|
|
|
await build([PAVE, SIGN]);
|
|
cardPick(120);
|
|
listPick("pave");
|
|
await refresh();
|
|
out.listPaveF5 = snap();
|
|
process.stdout.write(JSON.stringify(out));
|
|
})().catch((error) => {
|
|
process.stderr.write(String(error?.stack ?? error));
|
|
process.exit(1);
|
|
});
|