- 종단 보기 구독이 배수유역도 관 마커 · 유역 · 마름모까지 알림 없이 칠함 · 재계산 뒤 다시 얹음 - 지도 고름 · 목록 관 고름 · 목록 다시 누름도 고름 한 칸에 적고 칠하기는 구독 몫 - 목록 관 줄 폭 PIPE_MATCH_M · 조금 옮겨진 관도 고름 따라감 · 저장 뒤 목록 주입이 관 폼 안 닫음 - design_pick_escape — Esc = 고름 한 칸 비움 → 종단 · 3D · 지도 · 폼 · 횡단 카드 · 범위 · 형상 모두 풂 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
320 lines
11 KiB
JavaScript
320 lines
11 KiB
JavaScript
/* 233-2 3D · 횡단 카드 · [상세] 강조 = 고름 한 칸에서 파생 — 진짜 TS 를 Node 로 돌림.
|
|
* ① 3D: `bindProfilePick`(종단 보기 구독) + 진짜 마커(MK3) · 3D 부재 고름(SPK) · three.
|
|
* ② 횡단: `bindCrossPick` · `releaseCrossHighlights`(B06_Section_UI_Page_Pick_Sync) + 카드 · 형상 흉내.
|
|
* ③ [상세]: `attachDetailSelect` 다시 그림(창 크기 바뀜) 뒤 고른 선 조각 유지.
|
|
* 표준출력 JSON 한 벌 · 짝 = test_233_2_view_pick.py.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const NM = path.join(ROOT, "config", "node_modules");
|
|
const ts = require(path.join(NM, "typescript"));
|
|
const THREE = require(path.join(NM, "three", "build", "three.cjs"));
|
|
|
|
function loadTs(file, stubs = {}) {
|
|
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, file), "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
const localRequire = (request) => {
|
|
if (request === "three") return THREE;
|
|
if (stubs[request]) return stubs[request];
|
|
throw new Error(`stub 없음: ${request}`);
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
|
|
/* ── 가짜 DOM(라벨 캔버스 · [상세] 값 칸 · 조각) ── */
|
|
const node = (tag) => {
|
|
const self = {
|
|
tag,
|
|
hidden: false,
|
|
textContent: "",
|
|
attrs: {},
|
|
classes: new Set(),
|
|
parentElement: null,
|
|
setAttribute: (k, v) => {
|
|
self.attrs[k] = String(v);
|
|
if (k === "class")
|
|
String(v)
|
|
.split(" ")
|
|
.forEach((c) => self.classes.add(c));
|
|
},
|
|
getAttribute: (k) => self.attrs[k] ?? null,
|
|
addEventListener() {},
|
|
classList: {
|
|
toggle: (c, on) => (on ? self.classes.add(c) : self.classes.delete(c)),
|
|
contains: (c) => self.classes.has(c),
|
|
},
|
|
closest: (sel) => (self.classes.has(sel.slice(1)) ? self : null),
|
|
getContext: () => null,
|
|
};
|
|
return self;
|
|
};
|
|
globalThis.document = {
|
|
createElement: node,
|
|
createElementNS: (_ns, tag) => node(tag),
|
|
addEventListener() {},
|
|
removeEventListener() {},
|
|
};
|
|
globalThis.window = { addEventListener() {}, removeEventListener() {} };
|
|
|
|
const store = new Map();
|
|
const DSel = loadTs("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 ESC = loadTs("A00_Common/design/design_pick_escape.ts", { "./design_selection": DSel });
|
|
const IRR = {
|
|
irregularStationId: (id) => `irregular:${id}`,
|
|
isPipeStation: (s) => s.origin === "pipe",
|
|
};
|
|
const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", {
|
|
"../A00_Common/design/design_selection": DSel,
|
|
"./B05_Profile_UI_IrregularStations": IRR,
|
|
});
|
|
const PP = loadTs("A00_Common/design/design_pick_profile.ts", {
|
|
"./design_selection": DSel,
|
|
"./design_pick_escape": ESC,
|
|
"../../B05_Profile/B05_Profile_UI_Selection": SY,
|
|
"../../B05_Profile/B05_Profile_UI_IrregularStations": IRR,
|
|
});
|
|
const MK3 = loadTs("B05_Profile/B05_Profile_UI_Markers.ts", {
|
|
"@util/common_util_svg": { inferStationInterval: () => 20, stationLabel: (c) => `0+${c}` },
|
|
});
|
|
const SPK = loadTs("B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts");
|
|
const SYNC = loadTs("B06_Section/B06_Section_UI_Page_Pick_Sync.ts", {
|
|
"../A00_Common/design/design_selection": DSel,
|
|
"../A00_Common/design/design_pick_escape": ESC,
|
|
});
|
|
const CROSS_PAD = Function(
|
|
`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(fs.readFileSync(path.join(ROOT, "B06_Section", "B06_Section_UI_Section_Common.ts"), "utf8"))[1]}`,
|
|
)();
|
|
// [상세] 조각 모듈은 helper_209_1 과 같은 꼴로 딸린 모듈까지 진짜로 읽음(카드 공용만 흉내)
|
|
const DEEP_STUBS = {
|
|
"@ui/ui_template_context_menu": { createMapContextMenu: () => ({}) },
|
|
"./B06_Section_UI_Section_Common": { CROSS_PAD },
|
|
};
|
|
const resolveTs = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
|
|
function loadDeep(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 (DEEP_STUBS[request]) return DEEP_STUBS[request];
|
|
if (request.endsWith(".css")) return {};
|
|
if (request.startsWith("@util/"))
|
|
return loadDeep(resolveTs(path.join(ROOT, "common_util", request.slice(6))));
|
|
return loadDeep(resolveTs(path.join(path.dirname(full), request)));
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
const DTS = loadDeep(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Detail_Select.ts"));
|
|
|
|
const out = {};
|
|
const PID = "p233b";
|
|
const sel = (chainageM, structureId = null, options = {}) =>
|
|
DSel.selectDesign(
|
|
PID,
|
|
chainageM === null ? DSel.NO_SELECTION : { chainageM, structureId },
|
|
options,
|
|
);
|
|
|
|
/* ── ① 3D ── */
|
|
const bounds = { x: [0, 200], y: [0, 200], z: [0, 10] };
|
|
const scene = new THREE.Scene();
|
|
const markers = MK3.createRouteMarkers(scene, () => bounds);
|
|
const STATIONS = [
|
|
...[100, 120].map((at) => ({ station_id: `s${at}`, chainage_m: at, center_x: at })),
|
|
{ station_id: "irregular:sign", chainage_m: 127.86, center_x: 127.86 },
|
|
].map((s) => ({ ...s, center_y: 100, center_z: 5, frame: { left_xy: [0, 1] }, kind: "regular" }));
|
|
markers.renderStationLines(STATIONS, 3);
|
|
let notified = 0;
|
|
markers.onStationSelectionChange(() => (notified += 1));
|
|
const corridor = new THREE.Group();
|
|
const member = new THREE.Mesh(new THREE.BoxGeometry(4, 2, 4), new THREE.MeshLambertMaterial());
|
|
member.userData = { chainageM: 160, kind: "pipe", key: "inlet" };
|
|
const wall = new THREE.Mesh(new THREE.BoxGeometry(4, 2, 4), new THREE.MeshLambertMaterial());
|
|
wall.userData = { chainageM: 120, kind: "wall", key: "own" };
|
|
corridor.add(member, wall);
|
|
const structurePick = SPK.bindStructurePick({
|
|
canvas: { addEventListener() {}, removeEventListener() {} },
|
|
camera: () => null,
|
|
group: () => corridor,
|
|
blocked: () => false,
|
|
});
|
|
const profile = { station: null, structure: null };
|
|
let b05Visible = true;
|
|
const disposers = [];
|
|
PP.bindProfilePick(
|
|
PID,
|
|
{
|
|
profile: {
|
|
setSelectedStation: (id) => Object.assign(profile, { station: id, structure: null }),
|
|
setSelectedStructure: (id) => Object.assign(profile, { station: null, structure: id }),
|
|
},
|
|
viewer: { markers, structurePick },
|
|
},
|
|
{ visible: () => b05Visible, onDispose: (fn) => disposers.push(fn) },
|
|
{
|
|
irregular: () => [{ id: "sign", chainage_m: 127.86, origin: "user" }],
|
|
pipes: () => [],
|
|
sections: () => STATIONS.filter((s) => s.station_id.startsWith("s")),
|
|
},
|
|
);
|
|
const bar = (id) => markers.group.children[1].children.find((o) => o.userData.stationId === id);
|
|
const pin = () => markers.group.children.find((g) => g.renderOrder === 9);
|
|
const lit = (mesh) => mesh.material.emissive.getHex() !== 0;
|
|
const view3d = () => ({
|
|
red: STATIONS.map((s) => s.station_id).filter(
|
|
(id) => bar(id).material.color.getHexString() === "ef4444",
|
|
),
|
|
pin: pin().visible,
|
|
member: lit(member),
|
|
wall: lit(wall),
|
|
profile: { ...profile },
|
|
});
|
|
sel(120, null, { from: "b06" }); // 횡단 카드 120 고름 → 종단 보기도 값을 받음
|
|
out.card120 = view3d();
|
|
sel(160, null, { partKey: "inlet", from: "b06" });
|
|
out.part160 = view3d();
|
|
sel(127.86, "sign", { from: "b05" }); // 목록 표지 — 그 자리 구조물 측점 띠
|
|
out.sign = view3d();
|
|
sel(null);
|
|
out.released3d = view3d();
|
|
out.notified = notified; // 칠하기만 — 마커 알림으로 사슬이 다시 돌지 않음
|
|
disposers.forEach((fn) => fn());
|
|
|
|
/* ── ② 횡단 카드 · 범위 · 형상 ── */
|
|
const calls = [];
|
|
const sections = [100, 120].map((at) => ({ station_id: `s${at}`, chainage_m: at }));
|
|
const revetLit = new Set([120]);
|
|
let cardSelected = "s120";
|
|
let crossVisible = true;
|
|
let releaseCross;
|
|
const deps = {
|
|
sections: () => sections,
|
|
controls: {
|
|
revetOffset: {
|
|
highlightFor: (s) => revetLit.has(s.chainage_m),
|
|
selectedFor: (s) => revetLit.has(s.chainage_m),
|
|
select: (at) => revetLit.delete(at),
|
|
},
|
|
ford: { selectedFor: () => false, select() {} },
|
|
box: { selectedFor: () => false, select() {} },
|
|
},
|
|
releaseForm: () => calls.push("releaseForm"),
|
|
view: {
|
|
focusStation: (id) => {
|
|
calls.push(`focus:${id}`);
|
|
if (id === null && cardSelected !== null) {
|
|
cardSelected = null;
|
|
releaseCross(); // 카드 리스너(null) → 풀기 한 길 다시 부름 — 되돌아온 부름은 건너뜀
|
|
}
|
|
},
|
|
refreshCards: (list) => calls.push(`refresh:${list.join(",")}`),
|
|
selectMark: (id) => calls.push(`mark:${id}`),
|
|
},
|
|
reapply: () => calls.push("reapply"),
|
|
};
|
|
const crossDisposers = [];
|
|
const crossPick = SYNC.bindCrossPick(
|
|
PID,
|
|
{ visible: () => crossVisible, onDispose: (fn) => crossDisposers.push(fn) },
|
|
deps,
|
|
);
|
|
releaseCross = () => {
|
|
crossPick.release();
|
|
sel(null, null, { from: "b06" });
|
|
};
|
|
sel(120, null, { from: "b06" });
|
|
out.ownPick = calls.splice(0);
|
|
sel(140, null, { from: "esc" });
|
|
out.otherPick = calls.splice(0);
|
|
releaseCross(); // 카드 다시 누름 · 목록 다시 누름 · 보기 오감
|
|
out.release = calls.splice(0);
|
|
out.revetAfter = [...revetLit];
|
|
revetLit.add(100);
|
|
cardSelected = "s100";
|
|
sel(null, null, { from: "esc" }); // 다른 곳 풀기(Esc)
|
|
out.otherRelease = calls.splice(0);
|
|
crossVisible = false;
|
|
sel(100, null, { from: "esc" });
|
|
out.hidden = calls.splice(0);
|
|
crossDisposers.forEach((fn) => fn());
|
|
|
|
/* ── ③ [상세] 선 조각 — 다시 그린 뒤 고름 유지 ── */
|
|
let axes = {
|
|
maxOffset: 10,
|
|
pixelsPerMeter: 10,
|
|
displayMax: 110,
|
|
elevationMid: 100,
|
|
exaggeration: 1,
|
|
};
|
|
const groundM = [
|
|
[
|
|
[-5, 100],
|
|
[0, 101],
|
|
],
|
|
[
|
|
[0, 101],
|
|
[5, 99],
|
|
],
|
|
];
|
|
const toPx = ([o, e]) =>
|
|
[
|
|
CROSS_PAD.left + (axes.maxOffset - o) * axes.pixelsPerMeter,
|
|
CROSS_PAD.top + (axes.displayMax - e) * axes.pixelsPerMeter,
|
|
].join(",");
|
|
const layerChildren = [];
|
|
const layer = {
|
|
querySelectorAll: () => [...layerChildren],
|
|
append: (hit) => layerChildren.push(hit),
|
|
};
|
|
const drawGround = () => {
|
|
layerChildren.length = 0;
|
|
for (const pts of groundM) {
|
|
const el = node("polyline");
|
|
el.tagName = "polyline";
|
|
el.setAttribute("class", "b06-chart__cross-profile");
|
|
el.setAttribute("points", pts.map(toPx).join(" "));
|
|
el.hasAttribute = () => false;
|
|
layerChildren.push(el);
|
|
}
|
|
};
|
|
let click = null;
|
|
const cad = {
|
|
addEventListener: (_type, fn) => (click = fn),
|
|
querySelector: (s) => (s === "g.b06-chart__plot-layer" ? layer : null),
|
|
};
|
|
const select = DTS.attachDetailSelect({
|
|
cad,
|
|
design: () => undefined,
|
|
axes: () => axes,
|
|
picked() {},
|
|
commit() {},
|
|
});
|
|
drawGround();
|
|
select.redraw();
|
|
const hit = layerChildren.find((el) => el.classes.has(DTS.PIECE_HIT_CLASS));
|
|
click({ target: hit, shiftKey: false, ctrlKey: false, metaKey: false, stopPropagation() {} });
|
|
out.detailPicked = select.source("length")?.pieces.length ?? 0;
|
|
axes = { ...axes, pixelsPerMeter: 23 }; // 창 크기 바뀜 → 다시 그림
|
|
drawGround();
|
|
select.redraw();
|
|
out.detailAfterRedraw = select.source("length")?.pieces.length ?? 0;
|
|
groundM.splice(0, 1, [
|
|
[-6, 100],
|
|
[0, 102],
|
|
]); // 그 조각 자리가 바뀌면 고름도 빠짐
|
|
drawGround();
|
|
select.redraw();
|
|
out.detailAfterChange = select.source("length")?.pieces.length ?? 0;
|
|
|
|
process.stdout.write(JSON.stringify(out));
|