From b1e8d6cb7f5f8a8d4734cd3fdfeddc026ada1d0f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:36:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EC=84=A4=EC=A0=95):=20332-1=20=ED=9C=A0?= =?UTF-8?q?=20=ED=99=95=EB=8C=80=20=EB=B0=A9=ED=96=A5=20=EC=9D=BD=EA=B8=B0?= =?UTF-8?q?=20=EC=B8=B5=20=E2=80=94=20input=5Fprefs=20=C2=B7=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=ED=9C=A0=20=ED=99=94=EB=A9=B4=20=ED=95=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95(=EA=B8=B0=EB=B3=B8=20=EB=8B=B9=EA=B9=80)=20?= =?UTF-8?q?=C2=B7=20=ED=95=80=EC=B9=98=EB=8A=94=20=EB=8A=98=20=EB=B2=8C?= =?UTF-8?q?=EB=A6=AC=EB=A9=B4=20=ED=99=95=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 A00_Common/input_prefs.ts(INPUT_PREFS_KEY · DEFAULT_INPUT_PREFS · inputPrefs · wheelIn · wheelEventIn · 의존 없음) - 등록표 input-prefs(pref · global) 한 줄 - touch_bridge wheelZoomsIn · b_svg_zoom_pan · B04 3D 카메라 · M02 DF 그림 · M02 노무 그림 · 돋보기 · 시트 배선 - M02 그림 · 시트 · 돋보기 기본 방향 = 당김(지금과 반대) - 시험: test_input_prefs 새로 · 돋보기 · 266 · 244-8 헬퍼가 읽기 층을 불러옴 · 돋보기 부호 당김 기본 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy --- A00_Common/b_page_state.ts | 2 + A00_Common/b_svg_zoom_pan.ts | 6 +- A00_Common/input_prefs.ts | 64 ++++++++++ A00_Common/spreadsheet/spreadsheet_grid.ts | 4 +- A00_Common/touch_bridge.ts | 7 +- B04_PreProcess/B04_PreProcess_UI_Camera.ts | 5 +- M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts | 3 +- .../M02_MasterTemplete_Labor_Diagram.ts | 3 +- resources/tester/helper_218_1_loupe.cjs | 32 ++--- resources/tester/helper_266_detail_home.cjs | 1 + resources/tester/helper_input_prefs.cjs | 63 ++++++++++ resources/tester/test_220_1_loupe_wheel.py | 6 +- .../test_244_8_b08_milestone_no_scroll.py | 5 +- resources/tester/test_input_prefs.py | 113 ++++++++++++++++++ ui_template/ui_template_loupe.ts | 5 +- 15 files changed, 288 insertions(+), 31 deletions(-) create mode 100644 A00_Common/input_prefs.ts create mode 100644 resources/tester/helper_input_prefs.cjs create mode 100644 resources/tester/test_input_prefs.py diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index b3745a035..2f14d7bab 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -103,6 +103,8 @@ export const STATE_REGISTRY = { "cross-columns": { bucket: "pref", scope: "global" }, /** B05 · B06 종단 누가토량 알약 자리(309-2) — 그래프 영역 안 비율 `{"x":0~1,"y":0~1}` · 없으면 처음 자리. */ "mass-haul-badge-pos": { bucket: "pref", scope: "global" }, + /** ☰ [설정] 마우스 · 단축키(332) — 읽기는 `input_prefs.ts`(같은 키 글자) · [저장] 때만 씀. */ + "input-prefs": { bucket: "pref", scope: "global" }, /** B05 3D 영역 투영(316-2) `perspective · ortho` · 없으면 원근. */ "b05-3d-projection": { bucket: "pref", scope: "global" }, "section-panel-height": { diff --git a/A00_Common/b_svg_zoom_pan.ts b/A00_Common/b_svg_zoom_pan.ts index 9590e576c..ea40f76d9 100644 --- a/A00_Common/b_svg_zoom_pan.ts +++ b/A00_Common/b_svg_zoom_pan.ts @@ -14,6 +14,8 @@ * 그리는 일은 안 한다 — 넘겨받은 `layer` 에 `transform` 만 적는다. * ========================================================================== */ +import { wheelEventIn } from "./input_prefs"; + /** 줌·팬 상태 — 다시 그릴 때 배율을 되살리는 데 쓴다(2026-08-22 사용자 ③). */ export interface ZoomPanState { scale: number; @@ -213,8 +215,8 @@ export function attachSvgZoomPan(options: SvgZoomPanOptions): ZoomPanHandle { const rect = svg.getBoundingClientRect(); const cx = ((event.clientX - rect.left) / Math.max(rect.width, 1)) * widthPx; const cy = ((event.clientY - rect.top) / Math.max(rect.height, 1)) * heightPx; - // 노선 편집 지도와 같은 방향 — **당기면 확대**(`deltaY > 0`). - zoomAt(event.deltaY > 0 ? WHEEL_IN : WHEEL_OUT, cx, cy); + // ☰ [설정] 휠 방향(332 · 기본 당기면 확대) — 그림 면이 손가락을 받으므로 핀치는 트랙패드뿐. + zoomAt(wheelEventIn(event) ? WHEEL_IN : WHEEL_OUT, cx, cy); }, { passive: false }, ); diff --git a/A00_Common/input_prefs.ts b/A00_Common/input_prefs.ts new file mode 100644 index 000000000..49fff86db --- /dev/null +++ b/A00_Common/input_prefs.ts @@ -0,0 +1,64 @@ +/* ============================================================================= + * input_prefs.ts + * 마우스 · 단축키 설정 읽기 층(PLAN 332) — ☰ [설정] 창이 [저장] 때 계정 pref 로 씀. + * + * - 값 = `aislo:pref:input-prefs`(등록표 `input-prefs`) JSON 한 벌 · 부를 때마다 읽음(새로고침 없이 바로 따름) + * - 없음 · 깨짐 · Node(저장소 없음) = 기본값(= 지금 동작) · 의존 없음 · 불러올 때 window 안 만짐(CAD 도 import) + * - 휠 확대 방향 하나로 모든 화면 일체 — 당김 = 휠을 몸 쪽으로(deltaY > 0) 굴리면 확대 + * - 핀치(트랙패드 · 손가락)는 설정과 상관없이 늘 벌리면 확대(deltaY < 0) + * ========================================================================== */ + +export type WheelZoom = "pull" | "push"; + +export interface InputPrefs { + /** 휠 확대 방향 — pull = 당기면 확대 · push = 밀면 확대 */ + wheelZoom: WheelZoom; + /** CAD 내 별칭 `{명령: [별칭]}`(대문자 · acad.pgp 식 — 명령 이름 > 내 별칭 > 기본 별칭) */ + cadAliases: Record; +} + +/** `stateKey("input-prefs")` 와 같은 키(시험이 맞춰 봄) — 등록표를 불러오면 window 가 딸려 와서 글자로 둠 */ +export const INPUT_PREFS_KEY = "aislo:pref:input-prefs"; + +export const DEFAULT_INPUT_PREFS: InputPrefs = { wheelZoom: "pull", cadAliases: {} }; + +const cleanAliases = (raw: unknown): Record => { + const out: Record = {}; + if (!raw || typeof raw !== "object") return out; + for (const [id, list] of Object.entries(raw as Record)) { + if (!Array.isArray(list)) continue; + const aliases = list + .filter((a): a is string => typeof a === "string") + .map((a) => a.toUpperCase()); + if (aliases.length) out[id.toUpperCase()] = aliases; + } + return out; +}; + +/** 지금 설정 — 부를 때마다 저장소에서 읽음 */ +export function inputPrefs(): InputPrefs { + try { + const raw = globalThis.localStorage?.getItem(INPUT_PREFS_KEY); + if (!raw) return { ...DEFAULT_INPUT_PREFS, cadAliases: {} }; + const value = JSON.parse(raw) as Partial | null; + return { + wheelZoom: value?.wheelZoom === "push" ? "push" : "pull", + cadAliases: cleanAliases(value?.cadAliases), + }; + } catch { + return { ...DEFAULT_INPUT_PREFS, cadAliases: {} }; + } +} + +/** 휠 한 번이 확대인가 — 설정 방향을 따름 */ +export const wheelIn = (deltaY: number): boolean => + inputPrefs().wheelZoom === "push" ? deltaY < 0 : deltaY > 0; + +/** 트랙패드 핀치 — 브라우저가 Ctrl+휠 · 잘게 쪼갠 값으로 냄 + * ponytail: |deltaY| < 50 로 마우스 Ctrl+휠(한 칸 ≥ 약 100)과 가름 · 잘게 도는 마우스는 핀치로 봄 */ +export const isTrackpadPinch = (e: Pick): boolean => + e.ctrlKey && Math.abs(e.deltaY) < 50; + +/** 휠 사건이 확대인가 — 핀치(`pinch` 손가락 벌림 · 트랙패드)는 늘 벌리면 확대 · 나머지는 설정 방향 */ +export const wheelEventIn = (e: Pick, pinch = false): boolean => + pinch || isTrackpadPinch(e) ? e.deltaY < 0 : wheelIn(e.deltaY); diff --git a/A00_Common/spreadsheet/spreadsheet_grid.ts b/A00_Common/spreadsheet/spreadsheet_grid.ts index da403b2d4..9e0b97db7 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.ts +++ b/A00_Common/spreadsheet/spreadsheet_grid.ts @@ -13,6 +13,7 @@ import "./spreadsheet_grid.css"; import { colIndex, toA1 } from "./spreadsheet_address"; +import { wheelZoomsIn } from "../touch_bridge"; import { buildColGeometry, buildOutlineRuns, @@ -271,7 +272,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle { if (!ev.ctrlKey) return; ev.preventDefault(); const before = zoom; - const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom * Math.exp(-ev.deltaY * 0.0015))); + const step = (wheelZoomsIn(ev) ? 1 : -1) * Math.abs(ev.deltaY) * 0.0015; // ☰ [설정] 방향(332) + const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom * Math.exp(step))); if (next === before) return; zoom = next; colGeo = scaledGeometry(baseColGeo, zoom); diff --git a/A00_Common/touch_bridge.ts b/A00_Common/touch_bridge.ts index 6c83aa700..6fc8a810d 100644 --- a/A00_Common/touch_bridge.ts +++ b/A00_Common/touch_bridge.ts @@ -11,6 +11,8 @@ * 마우스 · 펜 이벤트는 건드리지 않음. * ========================================================================== */ +import { wheelEventIn } from "./input_prefs"; + const LONG_MS = 500; const SLOP_PX = 10; const DOUBLE_MS = 300; @@ -31,9 +33,8 @@ let contactPx = 0; /** 고름 반경(px) — 손가락으로 눌렀으면 접촉 크기만큼(최소 24px) · 마우스 · 펜은 그대로(기기대응 4-4) */ export const pickPx = (px: number): number => (contactPx ? Math.max(px, contactPx) : px); -/** 「당기면 확대」 휠 화면(B04 · B05 지도)의 확대 판정 — 손가락 벌림은 거꾸로라 뒤집음 */ -export const wheelZoomsIn = (e: WheelEvent): boolean => - isPinchWheel(e) ? e.deltaY < 0 : e.deltaY > 0; +/** 휠 화면(B04 · B05 지도 · M02 그림 · 시트)의 확대 판정 — ☰ [설정] 방향(332) · 손가락 벌림은 늘 벌리면 확대 */ +export const wheelZoomsIn = (e: WheelEvent): boolean => wheelEventIn(e, isPinchWheel(e)); const ownsTouch = (t: EventTarget | null): boolean => t instanceof Element && !!t.closest("[data-touch-own]"); diff --git a/B04_PreProcess/B04_PreProcess_UI_Camera.ts b/B04_PreProcess/B04_PreProcess_UI_Camera.ts index d5adca1d9..8bf42cc44 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Camera.ts +++ b/B04_PreProcess/B04_PreProcess_UI_Camera.ts @@ -1,6 +1,7 @@ import * as THREE from "three"; import type { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import type { SurfaceBounds } from "./B04_PreProcess_Api_Fetch"; +import { wheelEventIn } from "../A00_Common/input_prefs"; export const SURFACE_CAMERA_FOV = 50; const LIGHT_VIEWER_BACKGROUND = 0xf5f7f9; @@ -246,11 +247,11 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void } /** 휠 줌 — 커서가 가리키는 지점을 축으로 삼아 그 점이 화면에 고정된 채 멀어지고 가까워진다. - * 휠을 위로 올리면 멀어진다(사용자 지시). */ + * 방향 = ☰ [설정](332 · 기본 당기면 가까워짐 · 위로 올리면 멀어짐). */ function onWheel(event: WheelEvent): void { if (!controls.enabled || options.blocked?.()) return; event.preventDefault(); - zoomAround(event, event.deltaY < 0 ? 1 / ZOOM_STEP : ZOOM_STEP); + zoomAround(event, wheelEventIn(event) ? ZOOM_STEP : 1 / ZOOM_STEP); } /** 그 자리를 축으로 멀어지고(factor > 1) 가까워짐 — 휠 · 두 손가락 벌림이 같이 씀 */ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts index 76d4b028c..0623a007e 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts @@ -7,6 +7,7 @@ * 그리기만(바꾸는 것은 부른 쪽이 Model 로) · 순수 부분은 `M02_DF_Diagram_Parts.ts`. * ========================================================================== */ +import { wheelZoomsIn } from "../../A00_Common/touch_bridge"; import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements"; import type { CfBox, CfKind, CfLogic, CfPill, CfPort } from "./M02_DF_Model"; import { @@ -454,7 +455,7 @@ export function renderDiagram( "wheel", (ev) => { ev.preventDefault(); - setZoom(st.zoom * (ev.deltaY < 0 ? 1.1 : 1 / 1.1), ev.clientX, ev.clientY); + setZoom(st.zoom * (wheelZoomsIn(ev) ? 1.1 : 1 / 1.1), ev.clientX, ev.clientY); // ☰ [설정] 방향(332) }, { passive: false }, ); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts index b966ba85c..1d653f7a2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts @@ -7,6 +7,7 @@ * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; +import { wheelZoomsIn } from "../A00_Common/touch_bridge"; import { createButton, el } from "@ui/ui_template_elements"; import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money"; import "./M02_MasterTemplete_Labor_Diagram.css"; @@ -313,7 +314,7 @@ function mountShell(host: HTMLElement): State { if (!e.ctrlKey) return; e.preventDefault(); const r = viewport.getBoundingClientRect(); - zoomAt(st, e.deltaY < 0 ? 1.1 : 1 / 1.1, e.clientX - r.left, e.clientY - r.top); + zoomAt(st, wheelZoomsIn(e) ? 1.1 : 1 / 1.1, e.clientX - r.left, e.clientY - r.top); // ☰ [설정] 방향(332) }, { passive: false }, ); diff --git a/resources/tester/helper_218_1_loupe.cjs b/resources/tester/helper_218_1_loupe.cjs index ac649dbf7..031fa6f4d 100644 --- a/resources/tester/helper_218_1_loupe.cjs +++ b/resources/tester/helper_218_1_loupe.cjs @@ -15,7 +15,9 @@ function load(file) { ? {} : p.endsWith("key_shortcut") // 공용 키 판정(214-2)도 그 자리에서 트랜스파일 ? load(path.join(ROOT, "A00_Common", "key_shortcut.ts")) - : require(p); + : p.endsWith("input_prefs") // 휠 방향(332 · Node = 기본 당김) + ? load(path.join(ROOT, "A00_Common", "input_prefs.ts")) + : require(p); new Function("exports", "module", "require", js)(box.exports, box, req); return box.exports; } @@ -100,30 +102,30 @@ r.consts = { r.listeners = win.calls.map((c) => [c.type, c.capture]); } { - // 220-1 휠 — 켜 있는 동안만 배율 · 스크롤 막음 · 켤 때마다 2배 + // 220-1 휠 — 켜 있는 동안만 배율 · 스크롤 막음 · 켤 때마다 2배 · 확대 = 당김(deltaY > 0 · 332 기본) const { win, view } = setup(); const wheel = (deltaY) => win.fire("wheel", { deltaY }); const zoomLog = () => view.log.filter((l) => l[0] === "zoom").map((l) => l[1]); - const offEv = wheel(-100); + const offEv = wheel(100); const offState = { prevented: offEv.prevented, stopped: offEv.stopped, log: view.log.length }; win.fire("keydown", { code: "KeyZ" }); - const up = wheel(-100); + const up = wheel(100); const upState = { prevented: up.prevented, stopped: up.stopped, zoom: zoomLog().at(-1) }; - wheel(100); - wheel(100); + wheel(-100); + wheel(-100); const down = zoomLog().at(-1); wheel(0); // 가로 굴림 — 배율 그대로 · 스크롤은 막음 const flat = zoomLog().at(-1); - for (let i = 0; i < 30; i++) wheel(-50); + for (let i = 0; i < 30; i++) wheel(50); const max = zoomLog().at(-1); - for (let i = 0; i < 30; i++) wheel(120); + for (let i = 0; i < 30; i++) wheel(-120); const min = zoomLog().at(-1); win.fire("keyup", { code: "KeyZ" }); const n = zoomLog().length; - const afterOff = wheel(-100); + const afterOff = wheel(100); const afterOffZoomed = zoomLog().length !== n; win.fire("keydown", { code: "KeyZ" }); - wheel(-100); + wheel(100); r.wheel = { off: offState, up: upState, @@ -134,17 +136,17 @@ r.consts = { afterOff: { prevented: afterOff.prevented, stopped: afterOff.stopped, zoomed: afterOffZoomed }, reopen: zoomLog().at(-1), // 다시 켜면 2배에서 한 칸 pure: [ - m.nextLoupeZoom(2, -1), m.nextLoupeZoom(2, 1), - m.nextLoupeZoom(6, -1), - m.nextLoupeZoom(1.5, 1), + m.nextLoupeZoom(2, -1), + m.nextLoupeZoom(6, 1), + m.nextLoupeZoom(1.5, -1), ], }; // 길게 누름으로 켠 동안도 같음 const b = setup(); b.win.fire("mousedown", { button: 2, buttons: 2 }); b.win.tick(400); - const press = b.win.fire("wheel", { deltaY: -1 }); + const press = b.win.fire("wheel", { deltaY: 1 }); r.wheel.press = { prevented: press.prevented, zoom: b.view.log.at(-1) }; } { @@ -298,7 +300,7 @@ r.origin = { // 켜진 채 CAD 위로(mouseover · mousemove 둘 다) — 숨김 · 휠 안 막음 · 나오면 배율 그대로 보임 const { win, view } = setup(); win.fire("keydown", { code: "KeyZ" }); - win.fire("wheel", { deltaY: -100 }); // 2.25 + win.fire("wheel", { deltaY: 100 }); // 2.25 win.fire("mouseover", { target: cad }); const hidden = last(view); const n = view.log.length; diff --git a/resources/tester/helper_266_detail_home.cjs b/resources/tester/helper_266_detail_home.cjs index a5dfdbdbf..9b02f78cb 100644 --- a/resources/tester/helper_266_detail_home.cjs +++ b/resources/tester/helper_266_detail_home.cjs @@ -28,6 +28,7 @@ function loadTs(full) { new Function("exports", "module", "require", js)(box.exports, box, localRequire); return box.exports; } +STUBS["./input_prefs"] = loadTs(path.join(ROOT, "A00_Common", "input_prefs.ts")); // 휠 방향(332 · Node = 기본 당김) const metrics = loadTs(path.join(B06, "B06_Section_UI_Cross_View_Metrics.ts")); const { attachSvgZoomPan } = loadTs(path.join(ROOT, "A00_Common", "b_svg_zoom_pan.ts")); diff --git a/resources/tester/helper_input_prefs.cjs b/resources/tester/helper_input_prefs.cjs new file mode 100644 index 000000000..860064bb5 --- /dev/null +++ b/resources/tester/helper_input_prefs.cjs @@ -0,0 +1,63 @@ +/* PLAN 332-1 단위검증 헬퍼 — 휠 방향 읽기 층(`A00_Common/input_prefs.ts`) · 손가락 다리(`touch_bridge.ts`) + * · 등록표 키(`b_page_state.ts`)를 그 자리에서 트랜스파일해 Node 로 돌림. 가짜 localStorage 로 설정을 바꿔 봄. + * 결과 = 표준출력 JSON 한 벌. 짝 = `test_input_prefs.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 A00 = path.join(ROOT, "A00_Common"); +function load(file) { + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const req = (p) => (p === "./input_prefs" ? prefs : {}); + new Function("exports", "module", "require", js)(box.exports, box, req); + return box.exports; +} + +const out = {}; +// ① Node(저장소 없음) — 불러올 때 window 를 안 만지고 기본값 +const prefs = load(path.join(A00, "input_prefs.ts")); +const bridge = load(path.join(A00, "touch_bridge.ts")); +const page = load(path.join(A00, "b_page_state.ts")); +const sample = () => ({ + prefs: prefs.inputPrefs(), + pull: prefs.wheelIn(100), + push: prefs.wheelIn(-100), + mouseCtrl: prefs.wheelEventIn({ ctrlKey: true, deltaY: 100 }), + padSpread: prefs.wheelEventIn({ ctrlKey: true, deltaY: -3.5 }), + padPinch: prefs.wheelEventIn({ ctrlKey: true, deltaY: 3.5 }), + fingerSpread: prefs.wheelEventIn({ ctrlKey: true, deltaY: -9.5 }, true), + bridgePull: bridge.wheelZoomsIn({ ctrlKey: false, deltaY: 100 }), +}); +out.node = { key: prefs.INPUT_PREFS_KEY, default: prefs.DEFAULT_INPUT_PREFS, ...sample() }; +out.registryKey = page.stateKey("input-prefs"); +out.registryEntry = page.STATE_REGISTRY["input-prefs"]; + +// ② 브라우저 저장소 흉내 — 값을 바꾸면 다음 부름부터 바로 따름 +const store = new Map(); +globalThis.localStorage = { + getItem: (k) => (store.has(k) ? store.get(k) : null), + setItem: (k, v) => store.set(k, String(v)), + removeItem: (k) => store.delete(k), +}; +const set = (v) => store.set(prefs.INPUT_PREFS_KEY, v); +set(JSON.stringify({ wheelZoom: "push", cadAliases: { line: ["ll", 3], ARC: [] } })); +out.push = sample(); +set("{깨짐"); +out.broken = sample(); +set(JSON.stringify({ wheelZoom: "sideways" })); +out.unknown = sample(); +store.delete(prefs.INPUT_PREFS_KEY); +out.empty = sample(); +// ③ 불러오는 동안 저장소가 던지는 브라우저(사생활 보호) — 기본값 +globalThis.localStorage = { + getItem: () => { + throw new Error("blocked"); + }, +}; +out.blocked = sample(); + +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_220_1_loupe_wheel.py b/resources/tester/test_220_1_loupe_wheel.py index a1c5bda7d..e70b9c4da 100644 --- a/resources/tester/test_220_1_loupe_wheel.py +++ b/resources/tester/test_220_1_loupe_wheel.py @@ -34,10 +34,10 @@ def test_지름_360_배율_한도(r): assert (c["zoom"], c["zoomMin"], c["zoomMax"], c["zoomStep"]) == (2, 1.5, 6, 0.25) -def test_휠_위_크게_아래_작게_한도(r): +def test_휠_당김_크게_밀기_작게_한도(r): # 332 기본 = 당김 확대 w = r["wheel"] - assert w["up"]["zoom"] == 2.25 # 위로 한 칸 - assert w["down"] == 1.75 # 아래로 두 칸 + assert w["up"]["zoom"] == 2.25 # 당겨 한 칸 + assert w["down"] == 1.75 # 밀어 두 칸 assert w["flat"] == 1.75 # 가로 굴림은 그대로 assert w["max"] == 6 and w["min"] == 1.5 assert w["pure"] == [2.25, 1.75, 6, 1.5] diff --git a/resources/tester/test_244_8_b08_milestone_no_scroll.py b/resources/tester/test_244_8_b08_milestone_no_scroll.py index 17c980d2a..e3c7ecce2 100644 --- a/resources/tester/test_244_8_b08_milestone_no_scroll.py +++ b/resources/tester/test_244_8_b08_milestone_no_scroll.py @@ -20,7 +20,10 @@ def _read(rel): def _node(script): - src = IMPORTS.sub("", _read("A00_Common/b_svg_zoom_pan.ts")) + # 휠 방향 읽기 층(332) 을 앞에 붙임 — Node 엔 저장소가 없어 기본(당기면 확대) + src = _read("A00_Common/input_prefs.ts") + IMPORTS.sub( + "", _read("A00_Common/b_svg_zoom_pan.ts") + ) result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", src + "\n" + script], diff --git a/resources/tester/test_input_prefs.py b/resources/tester/test_input_prefs.py new file mode 100644 index 000000000..c9c2cd0b7 --- /dev/null +++ b/resources/tester/test_input_prefs.py @@ -0,0 +1,113 @@ +"""PLAN 332-1 — 휠 확대 방향 한 설정(`A00_Common/input_prefs.ts`) · 모든 휠 화면이 그 판정을 거침. + +· 기본 = 당김(휠을 몸 쪽으로 = 확대) · 밀기 저장 뒤 뒤집힘 · 깨짐 · 모르는 값 · 막힌 저장소 · Node = 기본 +· 핀치(트랙패드 · 손가락)는 설정과 상관없이 늘 벌리면 확대 +· 키 = 등록표 `input-prefs`(pref · global) 와 같은 글자 +짝: `helper_input_prefs.cjs`(실제 TS 를 Node 로 돌림). +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess + +import pytest + +HERE = os.path.dirname(__file__) +ROOT = os.path.join(HERE, "..", "..") + +pytestmark = pytest.mark.skipif( + not os.path.isdir(os.path.join(ROOT, "config", "node_modules")), + reason="프론트엔드 의존성 미설치", +) + + +def _read(rel: str) -> str: + with open(os.path.join(ROOT, rel), encoding="utf-8") as f: + return f.read() + + +@pytest.fixture(scope="module") +def r() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", os.path.join(HERE, "helper_input_prefs.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +PULL = {"pull": True, "push": False, "mouseCtrl": True, "bridgePull": True} +PINCH = {"padSpread": True, "padPinch": False, "fingerSpread": True} + + +def _pick(case: dict, keys: dict) -> dict: + return {k: case[k] for k in keys} + + +def test_Node_기본_당김(r): + n = r["node"] + assert n["default"] == {"wheelZoom": "pull", "cadAliases": {}} + assert n["prefs"] == n["default"] + assert _pick(n, PULL) == PULL and _pick(n, PINCH) == PINCH + + +def test_밀기_저장_뒤_뒤집힘_핀치는_그대로(r): + p = r["push"] + assert p["prefs"]["wheelZoom"] == "push" + assert _pick(p, PULL) == {"pull": False, "push": True, "mouseCtrl": False, "bridgePull": False} + assert _pick(p, PINCH) == PINCH # 벌리면 확대는 설정과 상관없음 + # 별칭 = 대문자 · 글자 아닌 것 · 빈 목록 버림 + assert p["prefs"]["cadAliases"] == {"LINE": ["LL"]} + + +@pytest.mark.parametrize("case", ["broken", "unknown", "empty", "blocked"]) +def test_깨짐_모르는_값_없음_막힘_기본(r, case): + c = r[case] + assert c["prefs"] == {"wheelZoom": "pull", "cadAliases": {}} + assert _pick(c, PULL) == PULL and _pick(c, PINCH) == PINCH + + +def test_등록표_키(r): + assert r["node"]["key"] == r["registryKey"] == "aislo:pref:input-prefs" + assert r["registryEntry"] == {"bucket": "pref", "scope": "global"} + + +def test_읽기_층은_의존_없음_불러올_때_window_안_만짐(): + src = _read("A00_Common/input_prefs.ts") + assert not re.search(r"^import ", src, re.M) + assert "window." not in src + + +# 휠 줌 자리 — 직접 박은 방향(`deltaY < 0 ?` · `deltaY > 0 ?` · `-ev.deltaY`)이 없고 공용 판정을 부름 +WHEEL_SITES = { + "A00_Common/touch_bridge.ts": "wheelEventIn(e, isPinchWheel(e))", + "A00_Common/b_svg_zoom_pan.ts": "wheelEventIn(event)", + "B04_PreProcess/B04_PreProcess_UI_Camera.ts": "wheelEventIn(event)", + "M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts": "wheelZoomsIn(ev)", + "M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts": "wheelZoomsIn(e)", + "ui_template/ui_template_loupe.ts": "wheelIn(deltaY)", + "A00_Common/spreadsheet/spreadsheet_grid.ts": "wheelZoomsIn(ev)", + # wheelZoomsIn 을 거쳐 따라오는 셋 + "B04_PreProcess/B04_PreProcess_UI_MapViewer.ts": "wheelZoomsIn(event)", + "B05_Profile/B05_Profile_UI_Drainage_Interact.ts": "wheelZoomsIn(event)", + "B05_Profile/B05_Profile_UI_RouteEdit_Input.ts": "wheelZoomsIn(event)", +} + + +@pytest.mark.parametrize("rel", sorted(WHEEL_SITES)) +def test_휠_자리_공용_판정(rel): + src = _read(rel) + assert WHEEL_SITES[rel] in src + assert not re.search(r"deltaY [<>] 0 \?", src), rel + assert "-ev.deltaY" not in src and "Math.sign(-deltaY)" not in src + + +def test_700줄_안(): + for rel in WHEEL_SITES: + assert _read(rel).count("\n") <= 700, rel diff --git a/ui_template/ui_template_loupe.ts b/ui_template/ui_template_loupe.ts index 16479963a..68a0cfee4 100644 --- a/ui_template/ui_template_loupe.ts +++ b/ui_template/ui_template_loupe.ts @@ -14,6 +14,7 @@ import "./ui_template_loupe.css"; import { keyLetter } from "../A00_Common/key_shortcut"; +import { wheelIn } from "../A00_Common/input_prefs"; /** 원 지름(px) */ export const LOUPE_DIAMETER = 360; @@ -86,9 +87,9 @@ export function isCadTarget(target: unknown): boolean { return !!(target as Partial | null)?.closest?.(LOUPE_SKIP); } -/** 휠 한 번 뒤 배율 — 위로 굴림(deltaY < 0) = 크게 · 한도 안 */ +/** 휠 한 번 뒤 배율 — ☰ [설정] 방향(332 · 기본 당김 = deltaY > 0 = 크게) · 한도 안 */ export function nextLoupeZoom(zoom: number, deltaY: number): number { - const step = Math.sign(-deltaY) * LOUPE_ZOOM_STEP; + const step = deltaY ? (wheelIn(deltaY) ? 1 : -1) * LOUPE_ZOOM_STEP : 0; return Math.max(LOUPE_ZOOM_MIN, Math.min(LOUPE_ZOOM_MAX, zoom + step)); }