Files
Aislo/resources/tester/helper_232_1_3d_press.cjs
T
eomsangdonandClaude Opus 5.5 cb4bab7bf2 feat(구조물표): 323-5 고름 한 칸 — 관 세션 id(옮겨도 그대로 · 저장 안 함) · 고름 강조 한 규칙(pickLights · 카드 형상 · 3D 같은 함수) · 그린 것마다 신원표(data-structure-id + data-part) · 같은 자리 거리 = PIPE_MATCH_M 하나 · 322-0 이 깬 옛 시험 도움 파일 · 줄 수 고침
- `design_pipe_ids.ts` — 장부(세션 pipe-ids) · pipe-s{차례} · 자리 → id(찾는 거리 안 가장 가까운 것) · followMovedPipe 가 id 를 새 자리로(고름 id 그대로 · 순환 import 없게 design_selection 에서 옮김) · 설계 페이지가 프로젝트를 정함(usePipeIds) · 자리 이름 만들기 열 곳 · 되읽기 둘 걷음 · 측점 계획 주인 = 세션 id(withPipeIds)
- `design_pick_match.ts` — pickLights(① id ② 같은 자리 ③ 고른 부분) · paintPicked(카드 통째 형상 · 부분 · 관 계열 · 포장층은 제 손잡이) · tagShape — 관 세트 벽 · 집수정 · 관 · 세월교 측벽 · 몸체 · BOX 끝 · C군 벽 · 물넘이 콘크리트에 신원표 · 형상 판정면은 통째 형상 · 콘크리트만
- 3D — 부재 맞춤 = pickLights(옛 0.01 걷음) · C군 벽 솔리드 = 구조물 id(덮는 측점 어디서 골라도 한 번 스윕한 벽이 켜짐)
- 322-0 이 깬 것 고침 — 노면공 흐름 도움 파일(165 · 231-1) 정의 판 확인 흉내 · design_view_profile 700줄(322-0 이 704)
- 시험: test_323_5_pick(세션 id · 옮김 · 강조 규칙 · 칠하기) · 옛 시험 도움 파일 흉내 · 기대 글 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-14 18:06:13 +09:00

219 lines
7.8 KiB
JavaScript

/* 232-1 3D 누름 길 통째 — 진짜 마커(MK3) · 마커 입력(MI) · 3D 부재 고름(SPK) · 선택 동기화(SY) ·
* 넘김(PKS) · 고름 한 칸(DSel) 을 three + 가짜 캔버스로 한 누름(창 캡처 → 캔버스 듣개 차례)씩 돌림.
* 배선은 `B05_Profile_UI_Viewer.ts` · `design_view_profile.ts` 와 같은 꼴. 표준출력 JSON 한 벌.
*/
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;
}
/* ── 창 · 캔버스 흉내(듣개 차례 = 창 캡처 → 캔버스 등록 차례) ── */
const windowListeners = [];
globalThis.window = {
addEventListener: (type, fn) => windowListeners.push({ type, fn }),
removeEventListener: (type, fn) => {
const i = windowListeners.findIndex((entry) => entry.type === type && entry.fn === fn);
if (i >= 0) windowListeners.splice(i, 1);
},
};
globalThis.document = { createElement: () => ({ getContext: () => null, width: 0, height: 0 }) };
const canvasListeners = [];
const canvas = {
addEventListener: (type, fn) => canvasListeners.push({ type, fn }),
removeEventListener() {},
getBoundingClientRect: () => ({ left: 0, top: 0, width: 400, height: 400 }),
setPointerCapture() {},
hasPointerCapture: () => false,
releasePointerCapture() {},
};
function fire(type, clientX, clientY) {
const event = {
type,
button: 0,
pointerId: 1,
clientX,
clientY,
preventDefault() {},
stopPropagation() {},
};
for (const entry of windowListeners.filter((e) => e.type === type)) entry.fn(event);
for (const entry of canvasListeners.filter((e) => e.type === type)) entry.fn(event);
}
/* ── 진짜 모듈 ── */
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 MK3 = loadTs("B05_Profile/B05_Profile_UI_Markers.ts", {
"@util/common_util_svg": {
inferStationInterval: () => 20,
stationLabel: (chainage) => `0+${chainage}`,
},
});
const MI = loadTs("B05_Profile/B05_Profile_UI_Viewer_Marker_Input.ts", {
"./B05_Profile_UI_Markers": MK3,
});
// 고름 강조 한 규칙(323-5) — 진짜 `design_pick_match`(같은 자리 = 고름 모듈 거리)
const PM = loadTs("A00_Common/design/design_pick_match.ts", { "./design_selection": DSel });
const SPK = loadTs("B05_Profile/B05_Profile_UI_Viewer_Structure_Pick.ts", {
"../A00_Common/design/design_pick_match": PM,
"../A00_Common/design/design_selection": DSel,
});
const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", {
"../A00_Common/design/design_selection": DSel,
"./B05_Profile_UI_IrregularStations": {
irregularStationId: (id) => `irregular:${id}`,
isPipeStation: (s) => s.origin === "pipe",
},
});
const PKS = loadTs("B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts", {
"../A00_Common/design/design_selection": DSel,
});
/* ── 장면: 지형 범위 · 위에서 내려다보는 직교 카메라 ── */
const PID = "p232";
const bounds = { x: [0, 200], y: [0, 200], z: [0, 10] };
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 500);
camera.position.set(0, 200, 0);
camera.up.set(0, 0, -1);
camera.lookAt(0, 0, 0);
camera.updateProjectionMatrix();
camera.updateMatrixWorld(true);
const clientOf = (x, y, z = 5) => {
const p = MK3.modelToScene({ x, y, z }, bounds).project(camera);
return [((p.x + 1) / 2) * 400, ((1 - p.y) / 2) * 400];
};
const markers = MK3.createRouteMarkers(scene, () => bounds);
const STATIONS = [100, 120].map((at) => ({
station_id: `s${at}`,
chainage_m: at,
center_x: at,
center_y: 100,
center_z: 5,
frame: { left_xy: [0, 1] },
kind: "regular",
}));
markers.renderStationLines(STATIONS, 3);
// 코리도 — 리본(신원표 없음 · 「빈 곳」) + 부재 하나(측점 160 · inlet)
const corridor = new THREE.Group();
const ribbon = new THREE.Mesh(
new THREE.BoxGeometry(400, 0.1, 400),
new THREE.MeshLambertMaterial(),
);
ribbon.position.y = -0.5;
const member = new THREE.Mesh(new THREE.BoxGeometry(4, 2, 4), new THREE.MeshLambertMaterial());
member.position.copy(MK3.modelToScene({ x: 160, y: 160, z: 6 }, bounds));
member.userData = { chainageM: 160, kind: "pipe", key: "inlet" };
corridor.add(ribbon, member);
scene.add(corridor);
scene.updateMatrixWorld(true);
const markerInput = MI.bindMarkerPointerControls({
canvas,
camera: () => camera,
controls: { enabled: true },
markers,
getTerrain: () => null,
getBounds: () => bounds,
});
const structurePick = SPK.bindStructurePick({
canvas,
camera: () => camera,
group: () => corridor,
blocked: () => markerInput.blocked(),
pressedMarker: () => markerInput.pressedMarker(),
});
/* ── 종단 보기 배선 흉내(design_view_profile) ── */
let graph = null;
let form = null;
let syncing = false;
const sync = SY.createSelectionSync({
stations: () => [],
isSyncing: () => syncing,
setSyncing: (v) => (syncing = v),
selectMarker: (id) => markers.selectStation(id),
selectGraph: (id) => (graph = id),
selectSidebar: (at) => (form = at),
openAtStation: (at) => (form = at),
selectBasin() {},
markStation() {},
selectStructure3D: (at) => structurePick.selectAtChainage(at),
regularChainage: (id) => STATIONS.find((s) => s.station_id === id)?.chainage_m,
remember: (at) =>
DSel.selectDesign(PID, at === null ? DSel.NO_SELECTION : { chainageM: at, structureId: null }),
});
markers.onStationSelectionChange((id) => {
graph = id;
sync.syncIrregularSelection(id);
});
PKS.wireStructurePick(
structurePick,
PID,
{
resolveAt: () => ({ structureId: null }),
hasSelection: () => form !== null,
selectAt: (sel) => (form = sel.chainageM),
},
sync.selectStationOfPipe,
);
/* ── 읽기 ── */
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 state = () => ({
s100: "#" + bar("s100").material.color.getHexString(),
s120: "#" + bar("s120").material.color.getHexString(),
pin: pin().visible,
session: DSel.readSelection(PID),
graph,
member: member.material.emissive.getHex() !== 0,
});
const press = (xy) => {
fire("pointerdown", ...xy);
const down = state();
fire("pointerup", ...xy);
return { down, up: state() };
};
const out = {};
const edge120 = clientOf(120, 102); // 띠 끝쪽(가운데 핀 · 라벨 자리 밖)
const center120 = clientOf(120, 100);
const empty = clientOf(40, 40); // 리본만
out.bar = press(edge120);
out.empty = press(empty);
out.again = press(edge120); // 같은 띠 다시 = 풂
out.center = press(center120); // 숨은 핀 · 라벨이 있던 가운데
out.memberPick = press(clientOf(160, 160));
out.memberEmpty = press(empty);
out.memberAgain = press(clientOf(160, 160)); // 같은 부재 다시 = 풂
press(edge120);
markers.setStationLinesVisible(false);
out.hiddenLines = press(clientOf(100, 102)); // 꺼 둔 측점선은 누름을 안 가로챔
out.hiddenPressedMarker = markerInput.pressedMarker();
process.stdout.write(JSON.stringify(out));