- 고르기: 누름 = 더함 · Shift = 뺌 · Ctrl = 넣고 빼기 · 상자 = 앞 고름 ∪ 상자 · Shift 상자 = 뺌(H4 · H5) · 명령 안 고르기 단계도 누를 때 첫 점 · 스침 강조(ic/selecting.ts) - 그립: 쉴 때만 집음(N7) · 누르고 떼면 대기 → 다음 누름에 놓음 · 끌어 놓기(H3) · 거리 숫자(N8) · 확정 · 읽기 전용 도면은 그립 · Delete 막음(H2) - 직교: F8 = 기준점에서 가로 · 세로 강제 · Shift 누른 동안 뒤집음 · 객체 스냅 점 · 좌표 · 각도 잠금은 안 덮음 · 클릭 사건 snapped 채움(ic/ortho.ts · H8) · 가이드는 F3 와 뗌 · 가이드 잡는 거리 = 화면 픽셀(H9) - 휠: 기본 밂(앞으로 굴리면 확대 · 오토캐드 · 모든 화면 · ☰ 설정 뒤집기 그대로) · 가운데 단추 두 번 = 범위 줌 · ZOOM · ⛶ 앞 화면 기억(ic/zoom-prompt.ts) - 시험: mouse-acad 새로 · 휠 방향 시험 손질(input-wheel · input-prefs-cad · input_prefs · app_settings · 164 · 220_1 · 221_1 · 244_8 기대값 뒤집음 · 266 pull 박음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
202 lines
8.7 KiB
JavaScript
202 lines
8.7 KiB
JavaScript
/* PLAN 164-1 단위검증 헬퍼 — [상세] 그림 줌 · 팬(`B06_Section_UI_Cross_Detail.ts` `attachDetailZoom`).
|
|
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 가짜 svg 에 휠 · 가운데 단추 · 더블클릭을 쏨.
|
|
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_164_detail_zoom.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");
|
|
const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8");
|
|
// 카드 여백은 원본에서 읽음(화면과 같은 값)
|
|
const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)();
|
|
// 축 눈금 간격도 원본 함수 그대로(179-3 · 축 다시 그리기)
|
|
const niceSrc = /export function niceTickStep[\s\S]*?\n\}/.exec(common.replace(/\r/g, ""))[0];
|
|
const niceTickStep = ts
|
|
.transpileModule(niceSrc.replace("export ", ""), { compilerOptions: {} })
|
|
.outputText.concat("\nreturn niceTickStep;");
|
|
const svgElement = (tag, attrs = {}) => {
|
|
const node = el(tag);
|
|
for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value);
|
|
return node;
|
|
};
|
|
const svgText = (text, attrs) => Object.assign(svgElement("text", attrs), { textContent: text });
|
|
const STUBS = {
|
|
"@ui/ui_template_modal": { openModal: () => {} },
|
|
"@ui/ui_template_context_menu": { createMapContextMenu: () => ({}) },
|
|
"../A00_Common/b_page_state": {},
|
|
"./B06_Section_Api_Fetch": {},
|
|
"./B06_Section_UI_Section_Common": {
|
|
CROSS_PAD,
|
|
L: (key) => key,
|
|
niceTickStep: Function(niceTickStep)(),
|
|
svgElement,
|
|
svgText,
|
|
},
|
|
"./B06_Section_UI_Cross_Detail_Pick": { DETAIL_TARGETS: {} },
|
|
// 선 조각 고르기(209-1)는 줌과 무관 — 따로 시험(`test_209_1_detail_select.py`)
|
|
"./B06_Section_UI_Cross_Detail_Select": { attachDetailSelect: () => ({}), PIECE_HIT_CLASS: "" },
|
|
// 표 위 CAD 칸(260)도 줌과 무관 — 따로 시험(`test_260_note_cad.py`)
|
|
"./B06_Section_UI_Cross_Detail_Cad": { mountNoteCad: () => {}, stationNoLabel: () => "" },
|
|
};
|
|
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 (request.endsWith(".css")) return {};
|
|
if (STUBS[request]) return STUBS[request];
|
|
return loadTs(resolve(path.join(path.dirname(full), request)));
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
|
|
function el(tag) {
|
|
const attrs = {};
|
|
const listeners = {};
|
|
const self = {
|
|
tag,
|
|
parent: null,
|
|
childNodes: [],
|
|
attrs,
|
|
style: { setProperty: (name, value) => (attrs[`style:${name}`] = value) },
|
|
classList: { add: () => {}, remove: () => {} },
|
|
setAttribute: (name, value) => (attrs[name] = String(value)),
|
|
getAttribute: (name) => attrs[name] ?? null,
|
|
// 진짜 DOM 처럼 옛 부모에서 옮겨 옴
|
|
append: (...nodes) =>
|
|
nodes.forEach((node) => {
|
|
if (node.parent) node.parent.childNodes.splice(node.parent.childNodes.indexOf(node), 1);
|
|
node.parent = self;
|
|
self.childNodes.push(node);
|
|
}),
|
|
replaceChildren: (...nodes) => {
|
|
self.childNodes.splice(0).forEach((node) => (node.parent = null));
|
|
self.append(...nodes);
|
|
},
|
|
// 「tag.class」 · 「tag」 만 — 깊이 우선 첫 것
|
|
querySelector: (selector) => {
|
|
const [tagName, cls] = selector.split(".");
|
|
const walk = (node) => {
|
|
for (const child of node.childNodes) {
|
|
const classes = (child.attrs.class ?? "").split(" ");
|
|
if (child.tag === tagName && (!cls || classes.includes(cls))) return child;
|
|
const found = walk(child);
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
};
|
|
return walk(self);
|
|
},
|
|
// 첫 보기 꼭짓점(295-2) — 이 그림엔 꺾은선 · 다각형 없음
|
|
querySelectorAll: () => [],
|
|
addEventListener: (type, fn) => (listeners[type] ??= []).push(fn),
|
|
fire: (type, event) => {
|
|
const full = { preventDefault() {}, stopPropagation() {}, pointerId: 1, ...event };
|
|
(listeners[type] ?? []).forEach((fn) => fn(full));
|
|
return full;
|
|
},
|
|
setPointerCapture: () => {},
|
|
releasePointerCapture: () => {},
|
|
// 칸 = viewBox 의 2 배(비율 같음)
|
|
getBoundingClientRect: () => ({ left: 0, top: 0, width: 1200, height: 600 }),
|
|
};
|
|
return self;
|
|
}
|
|
globalThis.document = { createElementNS: (_ns, tag) => el(tag), createElement: el };
|
|
|
|
const { attachDetailZoom } = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail.ts"));
|
|
|
|
// 카드 svg 꼴 — 바탕 · 축 층 · clip 그룹 > 도형 층 > 십자(중심 319,142 = 칸 가운데 · 295-2 첫 보기 = 1배)
|
|
const AXES = {
|
|
widthPx: 600,
|
|
heightPx: 300,
|
|
pixelsPerMeter: 10,
|
|
maxOffset: 24,
|
|
displayMax: 120,
|
|
elevationMid: 110,
|
|
exaggeration: 1,
|
|
};
|
|
function chart() {
|
|
const svg = el("svg");
|
|
svg.setAttribute("viewBox", "0 0 600 300");
|
|
const axes = svgElement("g", { class: "b06-chart__axes" });
|
|
const clipGroup = svgElement("g", { "clip-path": "url(#c)" });
|
|
const plot = svgElement("g", { class: "b06-chart__plot-layer" });
|
|
const center = svgElement("g", { class: "b06-chart__center" });
|
|
center.append(svgElement("line", { x1: 319, y1: 124, x2: 319, y2: 160 }));
|
|
plot.append(center);
|
|
clipGroup.append(plot);
|
|
svg.append(svgElement("rect"), axes, clipGroup, svgElement("line", { class: "b06-chart__axis" }));
|
|
return { svg, axes, plot, center };
|
|
}
|
|
const ticks = (axes) =>
|
|
axes.childNodes.filter((node) => node.tag === "text").map((node) => node.textContent);
|
|
const { svg, axes, plot: layer, center } = chart();
|
|
let saved;
|
|
const handle = attachDetailZoom(svg, AXES, undefined, (next) => (saved = next));
|
|
const ticksBefore = ticks(axes);
|
|
const out = {
|
|
// 179-3 — 감싸지 않음 · 자식 넷 그대로
|
|
wrapped: svg.childNodes.length === 1 && svg.childNodes[0].tag === "g",
|
|
children: svg.childNodes.length,
|
|
ratio: svg.attrs["style:--ratio"],
|
|
before: layer.getAttribute("transform"),
|
|
ticksBefore: ticksBefore.length,
|
|
};
|
|
// 휠 밂(deltaY < 0 · 기본 337) = 확대 · 커서(화면 600,300 = svg 300,150) 고정
|
|
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: -100 });
|
|
out.wheelIn = { ...saved };
|
|
out.cursorFixed = saved.tx + saved.scale * 300;
|
|
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: 100 });
|
|
out.wheelOut = saved.scale;
|
|
// 원배율에서 좌클릭 끌기 = 팬 아님
|
|
handle.reset();
|
|
svg.fire("pointerdown", { button: 0, clientX: 100, clientY: 100 });
|
|
svg.fire("pointermove", { clientX: 300, clientY: 100 });
|
|
svg.fire("pointerup", {});
|
|
out.leftDrag = { ...saved };
|
|
// 가운데 단추 끌기 = 팬(1배에서도 · 화면 200px = svg 100)
|
|
svg.fire("pointerdown", { button: 1, clientX: 100, clientY: 100 });
|
|
svg.fire("pointermove", { clientX: 300, clientY: 140 });
|
|
svg.fire("pointerup", {});
|
|
out.middlePan = { ...saved };
|
|
// 팬 뒤 — 도형 층만 transform · 축 층 · 바깥 그룹 transform 없음 · 눈금 글 바뀜
|
|
out.plotTransform = layer.getAttribute("transform");
|
|
out.axesTransform = axes.getAttribute("transform");
|
|
out.clipTransform = svg.childNodes[2].getAttribute("transform");
|
|
out.ticksChanged = JSON.stringify(ticks(axes)) !== JSON.stringify(ticksBefore);
|
|
out.ticksAfter = ticks(axes).length;
|
|
out.panClickStopped = svg.fire("click", {
|
|
stopPropagation() {
|
|
this.stopped = true;
|
|
},
|
|
}).stopped;
|
|
// 다시 떠도 배율 이어 감
|
|
const again = chart();
|
|
attachDetailZoom(again.svg, AXES, saved, () => {});
|
|
out.restored = again.plot.getAttribute("transform");
|
|
// 다시 떠도 축은 이어 간 범위로(1배 원래 눈금과 다름)
|
|
out.restoredTicksShifted = JSON.stringify(ticks(again.axes)) !== JSON.stringify(ticksBefore);
|
|
// 축 옵션 없음 · 도형 층 없음 — 줌은 걸고 축은 그대로 / 줌 없음
|
|
const bare = chart();
|
|
out.noAxesHandle = attachDetailZoom(bare.svg, null, undefined, () => {}) !== null;
|
|
const flat = el("svg");
|
|
flat.setAttribute("viewBox", "0 0 600 300");
|
|
out.noLayer = attachDetailZoom(flat, AXES, undefined, () => {});
|
|
// 더블클릭 원복
|
|
svg.fire("dblclick", {});
|
|
out.reset = layer.getAttribute("transform");
|
|
// 확대 — 십자는 중심에서 배율을 되돌림(크기 그대로)
|
|
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: -100 });
|
|
out.centerTransform = center.getAttribute("transform");
|
|
out.max = (() => {
|
|
for (let i = 0; i < 60; i++) svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: -100 });
|
|
return saved.scale;
|
|
})();
|
|
process.stdout.write(JSON.stringify(out));
|