modelstage 0.0.0-stage → 0.5.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +274 -2
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/bin/modelstage.mjs +137 -0
- package/config/diag-upload.example.json +7 -0
- package/config/models/live2d.assetbundls.json +1 -0
- package/config/models/live2d.azurlane.json +1 -0
- package/config/models/live2d.azurlane_bundle.json +1 -0
- package/config/models/live2d.azurlane_spine.json +1 -0
- package/config/models/live2d.bd2.json +1 -0
- package/config/models/live2d.bluearchive.json +1 -0
- package/config/models/live2d.live2d.json +1 -0
- package/config/models/live2d.nikke.json +1 -0
- package/config/models/live2d.nikke_2.json +1 -0
- package/config/models/live2d.npc.json +1 -0
- package/config/models/spine.assetbundls.json +1 -0
- package/config/models/spine.azurlane.json +1 -0
- package/config/models/spine.azurlane_bundle.json +1 -0
- package/config/models/spine.azurlane_spine.json +1 -0
- package/config/models/spine.bd2.json +1 -0
- package/config/models/spine.bluearchive.json +1 -0
- package/config/models/spine.live2d.json +1 -0
- package/config/models/spine.nikke.json +1 -0
- package/config/models/spine.nikke_2.json +1 -0
- package/config/models/spine.npc.json +1 -0
- package/config/names/NIKKE.json +1058 -0
- package/config/names/README.md +47 -0
- package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
- package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
- package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
- package/config/plugins.json +13 -0
- package/config/resources.example.json +40 -0
- package/package.json +63 -4
- package/plugins/unity-live2d-bundle/README.md +98 -0
- package/plugins/unity-live2d-bundle/index.js +891 -0
- package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
- package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
- package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
- package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
- package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
- package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
- package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
- package/plugins/unity-live2d-bundle/lib/png.js +424 -0
- package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
- package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
- package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-live2d-bundle/plugin.json +81 -0
- package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
- package/plugins/unity-spine-bundle/index.js +535 -0
- package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
- package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
- package/plugins/unity-spine-bundle/lib/errors.js +79 -0
- package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
- package/plugins/unity-spine-bundle/lib/extract.js +242 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-spine-bundle/lib/png.js +424 -0
- package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
- package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
- package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-spine-bundle/plugin.json +75 -0
- package/src/server/README.md +69 -0
- package/src/server/assets/browse.mjs +56 -0
- package/src/server/assets/bundle-probe.mjs +94 -0
- package/src/server/assets/probe.mjs +184 -0
- package/src/server/core/config.mjs +173 -0
- package/src/server/core/env-num.mjs +60 -0
- package/src/server/core/env.mjs +49 -0
- package/src/server/core/fs-portable.mjs +37 -0
- package/src/server/core/log.mjs +107 -0
- package/src/server/core/paths.mjs +44 -0
- package/src/server/data/catalog.mjs +1091 -0
- package/src/server/data/entry-states.mjs +499 -0
- package/src/server/data/live2d.mjs +272 -0
- package/src/server/data/manifest-cache.mjs +58 -0
- package/src/server/data/manifest-stamp.mjs +135 -0
- package/src/server/data/model-meta.mjs +114 -0
- package/src/server/data/names.mjs +106 -0
- package/src/server/data/shape-probe-worker.mjs +33 -0
- package/src/server/data/shape-probe.mjs +626 -0
- package/src/server/data/spine.mjs +336 -0
- package/src/server/data/tree.mjs +126 -0
- package/src/server/http/atomic-json.mjs +145 -0
- package/src/server/http/background-solids.mjs +270 -0
- package/src/server/http/backgrounds.mjs +388 -0
- package/src/server/http/diag-upload.mjs +264 -0
- package/src/server/http/diag.mjs +173 -0
- package/src/server/http/ready.mjs +167 -0
- package/src/server/http/static.mjs +312 -0
- package/src/server/http/write.mjs +654 -0
- package/src/server/index.mjs +114 -0
- package/src/server/plugins/builtin-providers.json +30 -0
- package/src/server/plugins/registry.mjs +650 -0
- package/src/server/plugins/slot-catalog.mjs +286 -0
- package/src/server/routes/background-solids.mjs +56 -0
- package/src/server/routes/backgrounds.mjs +77 -0
- package/src/server/routes/config.mjs +38 -0
- package/src/server/routes/data.mjs +313 -0
- package/src/server/routes/diag.mjs +37 -0
- package/src/server/routes/entries.mjs +170 -0
- package/src/server/routes/ready.mjs +122 -0
- package/src/server/serve.mjs +396 -0
- package/src/server/unpack/capacity.mjs +217 -0
- package/src/server/unpack/mem-probe.mjs +27 -0
- package/src/server/unpack/plugin-runner.mjs +529 -0
- package/src/server/unpack/plugin-store.mjs +736 -0
- package/src/server/unpack/probe-store.mjs +73 -0
- package/src/server/unpack/routes.mjs +359 -0
- package/src/server/unpack/runtime/exec.mjs +558 -0
- package/src/server/unpack/runtime/index.mjs +141 -0
- package/src/server/unpack/runtime/node-vm.mjs +111 -0
- package/src/server/unpack/unpack-worker.mjs +105 -0
- package/src/server/unpack/worker-client.mjs +184 -0
- package/src/server/unpack/write-result.mjs +84 -0
- package/src/shared/bundle-shape.mjs +83 -0
- package/src/shared/entry-reasons.mjs +119 -0
- package/src/shared/names.mjs +45 -0
- package/src/shared/type-labels.mjs +31 -0
- package/src/web/README.md +193 -0
- package/src/web/app/anim-list.js +222 -0
- package/src/web/app/background-solids.js +150 -0
- package/src/web/app/backgrounds.js +152 -0
- package/src/web/app/camera-log.js +28 -0
- package/src/web/app/context.js +72 -0
- package/src/web/app/dir-picker.js +242 -0
- package/src/web/app/entry-report.js +71 -0
- package/src/web/app/memory-bars.js +91 -0
- package/src/web/app/memory-budget.js +228 -0
- package/src/web/app/memory-guard.js +204 -0
- package/src/web/app/model-actions.js +682 -0
- package/src/web/app/renderer-setting.js +70 -0
- package/src/web/app/root-manager.js +254 -0
- package/src/web/app/status.js +138 -0
- package/src/web/app/topbar-actions.js +357 -0
- package/src/web/app/transport-actions.js +297 -0
- package/src/web/app/ui-state.js +559 -0
- package/src/web/app/url-params.js +171 -0
- package/src/web/app/view-settings.js +186 -0
- package/src/web/components.css +524 -0
- package/src/web/debug/diag-hud.js +95 -0
- package/src/web/debug/drag.js +369 -0
- package/src/web/debug/log.js +320 -0
- package/src/web/debug/model-cmp.js +292 -0
- package/src/web/debug/panel.css +474 -0
- package/src/web/debug/panel.js +712 -0
- package/src/web/debug/race-probe.js +82 -0
- package/src/web/debug/upload.js +130 -0
- package/src/web/engine/adapters/atlas.js +47 -0
- package/src/web/engine/adapters/index.js +22 -0
- package/src/web/engine/adapters/renderer.js +223 -0
- package/src/web/engine/adapters/skeleton.js +105 -0
- package/src/web/engine/adapters/texture.js +64 -0
- package/src/web/engine/animation.js +181 -0
- package/src/web/engine/appearance.js +136 -0
- package/src/web/engine/camera-adapter.js +212 -0
- package/src/web/engine/effect-chain.js +138 -0
- package/src/web/engine/engine-state.js +120 -0
- package/src/web/engine/filters.js +224 -0
- package/src/web/engine/fit.js +197 -0
- package/src/web/engine/frame-gate.js +33 -0
- package/src/web/engine/gl-driver.js +56 -0
- package/src/web/engine/gl.js +77 -0
- package/src/web/engine/index.js +20 -0
- package/src/web/engine/loader.js +55 -0
- package/src/web/engine/model-info.js +26 -0
- package/src/web/engine/pipeline.js +200 -0
- package/src/web/engine/player.js +427 -0
- package/src/web/engine/pma/blend.js +76 -0
- package/src/web/engine/pma/detect.js +118 -0
- package/src/web/engine/pma/index.js +8 -0
- package/src/web/engine/pma/resolve.js +112 -0
- package/src/web/engine/pma/upload.js +127 -0
- package/src/web/engine/pma.js +5 -0
- package/src/web/engine/render-frame.js +93 -0
- package/src/web/engine/runtime-capabilities.js +209 -0
- package/src/web/engine/runtime.js +37 -0
- package/src/web/engine/skins.js +165 -0
- package/src/web/engine/textures.js +174 -0
- package/src/web/index.html +117 -0
- package/src/web/infra/api.js +531 -0
- package/src/web/infra/diag.js +509 -0
- package/src/web/infra/document.js +269 -0
- package/src/web/infra/dom.js +31 -0
- package/src/web/infra/events.mjs +27 -0
- package/src/web/infra/log.js +153 -0
- package/src/web/infra/mem-stats.js +112 -0
- package/src/web/infra/plugin-host-registration.mjs +285 -0
- package/src/web/infra/plugin-host-report.mjs +32 -0
- package/src/web/infra/plugin-host-unload.mjs +79 -0
- package/src/web/infra/plugin-host-unpack.mjs +899 -0
- package/src/web/infra/plugin-host-warmup.mjs +180 -0
- package/src/web/infra/plugin-host.js +628 -0
- package/src/web/infra/ready.js +461 -0
- package/src/web/infra/schedule.js +51 -0
- package/src/web/infra/shortcuts.js +129 -0
- package/src/web/infra/slot-registry.js +402 -0
- package/src/web/infra/vfs.js +247 -0
- package/src/web/live2d/bridge.js +400 -0
- package/src/web/live2d/model.js +321 -0
- package/src/web/live2d/overlay.js +142 -0
- package/src/web/live2d/pixi-app.js +111 -0
- package/src/web/live2d/playback-probe.js +92 -0
- package/src/web/live2d/probe-geometry.js +329 -0
- package/src/web/live2d/probe.js +119 -0
- package/src/web/live2d/release.js +66 -0
- package/src/web/live2d/runtime-adapter.js +289 -0
- package/src/web/live2d/stage-ctx.js +152 -0
- package/src/web/live2d/stage.js +204 -0
- package/src/web/live2d/vfs-settings.js +174 -0
- package/src/web/live2d/view.js +89 -0
- package/src/web/main.js +629 -0
- package/src/web/panel/background-list.js +373 -0
- package/src/web/panel/bg-source.js +291 -0
- package/src/web/panel/color-picker.css +199 -0
- package/src/web/panel/color-picker.js +278 -0
- package/src/web/panel/dialog.css +116 -0
- package/src/web/panel/dialog.js +173 -0
- package/src/web/panel/dom.js +35 -0
- package/src/web/panel/icons.js +58 -0
- package/src/web/panel/memory-bar.js +195 -0
- package/src/web/panel/panel-settings.js +1263 -0
- package/src/web/panel/panel.css +1304 -0
- package/src/web/panel/render-choice.js +104 -0
- package/src/web/panel/schema.js +784 -0
- package/src/web/panel/settings-io.js +291 -0
- package/src/web/panel/shell-controls.js +274 -0
- package/src/web/panel/slider.js +210 -0
- package/src/web/panel/toolbar-schema.js +111 -0
- package/src/web/panel/toolbar.js +195 -0
- package/src/web/panel/transport-picker.js +375 -0
- package/src/web/panel/transport.js +321 -0
- package/src/web/panel/view.js +553 -0
- package/src/web/plugin/unpack-worker.js +185 -0
- package/src/web/render/camera/constants.js +60 -0
- package/src/web/render/camera/index.js +123 -0
- package/src/web/render/camera/spine.js +124 -0
- package/src/web/render/capture.js +30 -0
- package/src/web/render/interaction.js +527 -0
- package/src/web/render/live2d.js +627 -0
- package/src/web/render/playback.js +47 -0
- package/src/web/render/reset-view.js +29 -0
- package/src/web/render/spine.js +386 -0
- package/src/web/render/surface-switch.js +176 -0
- package/src/web/render/surface.js +571 -0
- package/src/web/render/surfaces.js +70 -0
- package/src/web/roster/filter.js +31 -0
- package/src/web/roster/list.js +276 -0
- package/src/web/roster/persist.js +92 -0
- package/src/web/roster/reveal.js +77 -0
- package/src/web/roster/rows.js +248 -0
- package/src/web/roster/snapshot.js +39 -0
- package/src/web/roster/type-filter.js +97 -0
- package/src/web/roster/view.js +761 -0
- package/src/web/spine/canvas-api-groups.js +267 -0
- package/src/web/spine/canvas-api.js +97 -0
- package/src/web/spine/canvas-document.js +80 -0
- package/src/web/spine/canvas-engine.js +193 -0
- package/src/web/spine/canvas-entry.js +28 -0
- package/src/web/spine/canvas-pixels.js +36 -0
- package/src/web/spine/canvas-runtime.js +127 -0
- package/src/web/spine/canvas-view.js +108 -0
- package/src/web/spine/canvas.js +7 -0
- package/src/web/spine/runtime-hint.js +73 -0
- package/src/web/spine/runtimes.js +212 -0
- package/src/web/style.css +493 -0
- package/tool/lib/bundle-probe-node.mjs +121 -0
- package/tool/lib/paths.mjs +20 -0
- package/tool/lib/runtime-guides.mjs +65 -0
- package/tool/lib/runtime-package.mjs +403 -0
- package/tool/lib/spine-meta.mjs +227 -0
- package/tool/lib/spine-motions.mjs +104 -0
- package/tool/parse-jsonc.mjs +86 -0
- package/tool/resolve-live2d-runtimes.mjs +276 -0
- package/tool/spine-motion-rules.json +101 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
// render/interaction.js —— 舞台相机交互(拖动平移 / 滚轮缩放 / 双指捏合)
|
|
2
|
+
//
|
|
3
|
+
// 交互语义逐条对应如下:
|
|
4
|
+
// · 滚轮 t = (deltaY > 0 ? -1 : 1) → engine.applyZoom(t) 一档 ±10%,锚点=视图中心
|
|
5
|
+
// · 拖动 每次 pointermove 把原始屏幕像素差值交给 engine.applyPan(dx, dy)
|
|
6
|
+
// · 捏合 zoom = 起始userScale × (当前两指距离 / 起始两指距离),中点位移另走 applyPan
|
|
7
|
+
// · 复位 engine.resetCamera()
|
|
8
|
+
//
|
|
9
|
+
// 本文件**不做任何坐标换算**:缩放档位、像素倍率、flip、Live2D 委托全部由引擎处理。
|
|
10
|
+
// 相机能力统一从 stage.camera 取(见 render/camera/spine.js);只有缺失该入口时才回退到
|
|
11
|
+
// stage.getView/setView(且只做加法累计,仍然不做换算)。
|
|
12
|
+
//
|
|
13
|
+
// 日志:所有相机动作都通过 camera.note() 汇入同一路 CAMERA 日志,真机操作即可逐条核对:
|
|
14
|
+
// [CAMERA] wheel deltaY=-120 → zoomStep(+1) (stage-camera 再补 userScale 前后值)
|
|
15
|
+
// [CAMERA] drag dx=+24 dy=0
|
|
16
|
+
// [CAMERA] pinch dist 210→300 ratio=1.429 → zoomTo(1.429)
|
|
17
|
+
// [CAMERA] reset
|
|
18
|
+
|
|
19
|
+
/** 默认参数:只保留"是否允许单指拖动"这个行为开关;缩放范围由引擎 clamp(0.125~16)负责 */
|
|
20
|
+
const DEFAULT_OPTIONS = Object.freeze({
|
|
21
|
+
panWithOnePointer: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 取画布内相对坐标(CSS 像素,原点画布左上角)。
|
|
26
|
+
* @param {HTMLCanvasElement} canvas
|
|
27
|
+
* @param {PointerEvent|WheelEvent} event
|
|
28
|
+
*/
|
|
29
|
+
function toCanvasPoint(canvas, event) {
|
|
30
|
+
const rect = canvas.getBoundingClientRect();
|
|
31
|
+
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 两指距离。
|
|
36
|
+
* @param {{x:number,y:number}} a
|
|
37
|
+
* @param {{x:number,y:number}} b
|
|
38
|
+
*/
|
|
39
|
+
function distance(a, b) {
|
|
40
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 两指中点。
|
|
45
|
+
* @param {{x:number,y:number}} a
|
|
46
|
+
* @param {{x:number,y:number}} b
|
|
47
|
+
*/
|
|
48
|
+
function midpoint(a, b) {
|
|
49
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 相机桥:把交互动作落到 stage.camera(薄转发),缺失时退到 stage 的兼容入口。
|
|
54
|
+
* @param {object} stage
|
|
55
|
+
*/
|
|
56
|
+
function createCameraBridge(stage) {
|
|
57
|
+
const camera = stage && stage.camera;
|
|
58
|
+
|
|
59
|
+
/** 日志:优先走相机入口的 note(与 stage-camera 同一路 CAMERA 日志) */
|
|
60
|
+
function note(message, data) {
|
|
61
|
+
if (camera && typeof camera.note === 'function') {
|
|
62
|
+
try { camera.note(message, data); } catch { /* 日志失败不影响交互 */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
note,
|
|
68
|
+
/** 缩放一档(dir=±1,由引擎处理 ±10% 与 clamp) */
|
|
69
|
+
zoomStep(dir) {
|
|
70
|
+
if (camera && typeof camera.zoomStep === 'function') return camera.zoomStep(dir);
|
|
71
|
+
return null;
|
|
72
|
+
},
|
|
73
|
+
/** 缩放到绝对 userScale(捏合:起始 userScale × 距离比) */
|
|
74
|
+
zoomTo(scale) {
|
|
75
|
+
if (camera && typeof camera.zoomTo === 'function') return camera.zoomTo(scale);
|
|
76
|
+
return null;
|
|
77
|
+
},
|
|
78
|
+
/** 平移:原始屏幕像素差值 */
|
|
79
|
+
panBy(dx, dy) {
|
|
80
|
+
if (camera && typeof camera.panBy === 'function') return camera.panBy(dx, dy);
|
|
81
|
+
// 回退:仅有 setView 的旧契约,本地累计后按绝对量下发(不做任何换算)
|
|
82
|
+
if (typeof stage.setView === 'function' && typeof stage.getView === 'function') {
|
|
83
|
+
const view = stage.getView() || { panX: 0, panY: 0 };
|
|
84
|
+
return stage.setView({ panX: (view.panX || 0) + dx, panY: (view.panY || 0) + dy });
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
},
|
|
88
|
+
/** 复位:交给引擎 resetCamera(重新 fit) */
|
|
89
|
+
reset() {
|
|
90
|
+
if (camera && typeof camera.reset === 'function') return camera.reset();
|
|
91
|
+
if (typeof stage.resetView === 'function') return stage.resetView();
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
/** 当前 userScale(捏合起点用);拿不到时返回 null */
|
|
95
|
+
scale() {
|
|
96
|
+
if (camera && typeof camera.scale === 'function') return camera.scale();
|
|
97
|
+
return null;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 把一帧内累积的平移合并成一次相机调用(拖动风暴时每帧最多一次 applyPan)。
|
|
104
|
+
* @param {(dx:number, dy:number) => void} applyPan
|
|
105
|
+
*/
|
|
106
|
+
function createPanScheduler(applyPan) {
|
|
107
|
+
let shiftX = 0;
|
|
108
|
+
let shiftY = 0;
|
|
109
|
+
let frameId = 0;
|
|
110
|
+
|
|
111
|
+
/** 冲刷本帧累积量 */
|
|
112
|
+
function flush() {
|
|
113
|
+
frameId = 0;
|
|
114
|
+
if (!shiftX && !shiftY) return;
|
|
115
|
+
const dx = shiftX;
|
|
116
|
+
const dy = shiftY;
|
|
117
|
+
shiftX = 0;
|
|
118
|
+
shiftY = 0;
|
|
119
|
+
applyPan(dx, dy);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
queue(dx, dy) {
|
|
124
|
+
if (!dx && !dy) return;
|
|
125
|
+
shiftX += dx;
|
|
126
|
+
shiftY += dy;
|
|
127
|
+
if (frameId) return;
|
|
128
|
+
frameId = requestAnimationFrame(flush);
|
|
129
|
+
},
|
|
130
|
+
flushNow() {
|
|
131
|
+
if (!frameId) return;
|
|
132
|
+
cancelAnimationFrame(frameId);
|
|
133
|
+
flush();
|
|
134
|
+
},
|
|
135
|
+
dispose() {
|
|
136
|
+
if (frameId) cancelAnimationFrame(frameId);
|
|
137
|
+
frameId = 0;
|
|
138
|
+
shiftX = 0;
|
|
139
|
+
shiftY = 0;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 把一帧内的缩放合并成一次相机调用(与 createPanScheduler 同一套结构)。
|
|
146
|
+
*
|
|
147
|
+
* 为什么需要:一次捏合会派发几十上百个 pointermove,逐个调 camera.zoomTo 等于同一帧
|
|
148
|
+
* 反复重算相机(日志里相邻两次 `pinch dist 159→354` / `159→353` 各调一次 zoomTo)。
|
|
149
|
+
* 平移早就有 scheduler,缩放漏了,这里补齐。
|
|
150
|
+
*
|
|
151
|
+
* 两种语义必须分开累积(同一帧里可能既有捏合又有滚轮):
|
|
152
|
+
* · 捏合是**绝对目标值**(pinchStart.scale × 距离比)⇒ 取最后一次,不是相加;
|
|
153
|
+
* · 滚轮是**步进** ⇒ 把 +1/-1 求和,冲刷后清零。
|
|
154
|
+
* 同一帧两者都有时以绝对目标为准(捏合是手上的直接位移,优先级高于滚轮 tick)。
|
|
155
|
+
* 缩放上下限仍由统一相机(camera 层)钳制,这里不另起一套。
|
|
156
|
+
*
|
|
157
|
+
* @param {(target:number) => void} zoomTo 绝对缩放
|
|
158
|
+
* @param {(steps:number) => void} zoomStep 步进缩放
|
|
159
|
+
*/
|
|
160
|
+
function createZoomScheduler(zoomTo, zoomStep) {
|
|
161
|
+
let target = null; // 绝对目标(最后写入者生效)
|
|
162
|
+
let steps = 0; // 步进累积
|
|
163
|
+
let frameId = 0;
|
|
164
|
+
let calls = 0; // 真正落到相机的调用次数(验收要贴的数字)
|
|
165
|
+
let flushes = 0; // 真正产生相机调用的帧数(= calls,但两者分开暴露便于排查)
|
|
166
|
+
|
|
167
|
+
/** 冲刷本帧累积量:每帧最多一次相机调用 */
|
|
168
|
+
function flush() {
|
|
169
|
+
frameId = 0;
|
|
170
|
+
if (target === null && !steps) return;
|
|
171
|
+
const pendingTarget = target;
|
|
172
|
+
const pendingSteps = steps;
|
|
173
|
+
target = null;
|
|
174
|
+
steps = 0;
|
|
175
|
+
if (pendingTarget !== null) {
|
|
176
|
+
zoomTo(pendingTarget);
|
|
177
|
+
calls += 1;
|
|
178
|
+
flushes += 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (pendingSteps) {
|
|
182
|
+
zoomStep(pendingSteps);
|
|
183
|
+
calls += 1;
|
|
184
|
+
flushes += 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function schedule() {
|
|
189
|
+
if (frameId) return;
|
|
190
|
+
frameId = requestAnimationFrame(flush);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
/** 排队一个绝对目标(捏合):只保留最后一次 */
|
|
195
|
+
queueTarget(value) {
|
|
196
|
+
if (!Number.isFinite(value)) return;
|
|
197
|
+
target = value;
|
|
198
|
+
schedule();
|
|
199
|
+
},
|
|
200
|
+
/** 排队一个步进(滚轮):累加 */
|
|
201
|
+
queueStep(dir) {
|
|
202
|
+
if (!dir) return;
|
|
203
|
+
steps += dir;
|
|
204
|
+
schedule();
|
|
205
|
+
},
|
|
206
|
+
/** 立刻冲刷(抬手/取消时调用,保证最后一次捏合不丢) */
|
|
207
|
+
flushNow() {
|
|
208
|
+
if (!frameId) return;
|
|
209
|
+
cancelAnimationFrame(frameId);
|
|
210
|
+
flush();
|
|
211
|
+
},
|
|
212
|
+
dispose() {
|
|
213
|
+
if (frameId) cancelAnimationFrame(frameId);
|
|
214
|
+
frameId = 0;
|
|
215
|
+
target = null;
|
|
216
|
+
steps = 0;
|
|
217
|
+
},
|
|
218
|
+
/** 验收用:累计相机调用次数 / 冲刷帧数 / 是否还有未冲刷的量 */
|
|
219
|
+
stats: () => ({ calls, flushes, pending: target !== null || steps !== 0 }),
|
|
220
|
+
/** 一轮手势结束后清零(汇总条要报"这次手势"的次数,不是进程累计) */
|
|
221
|
+
resetStats() { calls = 0; flushes = 0; },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 指针状态机:单指拖动 / 双指捏合。
|
|
227
|
+
* @param {{canvas: HTMLCanvasElement, camera: object, options: object}} ctx
|
|
228
|
+
*/
|
|
229
|
+
/** 一次拖动的累计位移(抬手时汇总成一条 CAMERA 日志;逐帧记会把面板刷满) */
|
|
230
|
+
const dragAcc = { dx: 0, dy: 0, moves: 0 };
|
|
231
|
+
|
|
232
|
+
/** 清空拖动累计(抬手汇总后调用,避免下一段拖动带上上次的位移) */
|
|
233
|
+
function resetDragAcc() {
|
|
234
|
+
dragAcc.dx = 0;
|
|
235
|
+
dragAcc.dy = 0;
|
|
236
|
+
dragAcc.moves = 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 一次缩放手势的累计量(**抬手时汇总成一条**,与 dragAcc 同一套做法)。
|
|
241
|
+
*
|
|
242
|
+
* 为什么要累积:逐帧记日志不仅刷屏,更糟的是会把"排队值"写成"→ zoomTo(x)",
|
|
243
|
+
* 让人读日志时以为每个 move 都真的调了相机(实测就是这样被误判成"没修好")。
|
|
244
|
+
* 这里只在手势结束打一条,且带上**真实相机调用次数**(来自 scheduler.stats().calls)。
|
|
245
|
+
*/
|
|
246
|
+
/**
|
|
247
|
+
* 缩放手势的**汇总上报器**(照 dragAcc「累积 → 抬手一条」的做法)。
|
|
248
|
+
*
|
|
249
|
+
* 为什么必须累积而不是逐帧打:
|
|
250
|
+
* · 逐帧打会把面板刷满(一次捏合几十上百条);
|
|
251
|
+
* · 更糟的是文案容易写成「→ zoomTo(x)」,而那只是**排队请求**,被人读成"每个 move
|
|
252
|
+
* 都真的调了相机"——实测就因此被判"没修好"。
|
|
253
|
+
* 因此这里只在手势结束打一条,且带上 scheduler 的**真实相机调用次数**。
|
|
254
|
+
*
|
|
255
|
+
* @param {{camera: object}} ctx
|
|
256
|
+
* @param {{queueTarget:Function, queueStep:Function, flushNow:Function, stats:Function, resetStats:Function}} scheduler
|
|
257
|
+
*/
|
|
258
|
+
function createZoomReporter(ctx, scheduler) {
|
|
259
|
+
const acc = { active: false, kind: '', startDist: 0, lastDist: 0, moves: 0, lastTarget: 0, steps: 0, events: 0 };
|
|
260
|
+
let lastReport = null;
|
|
261
|
+
let wheelTimer = 0;
|
|
262
|
+
|
|
263
|
+
function reset() {
|
|
264
|
+
acc.active = false;
|
|
265
|
+
acc.kind = '';
|
|
266
|
+
acc.startDist = 0;
|
|
267
|
+
acc.lastDist = 0;
|
|
268
|
+
acc.moves = 0;
|
|
269
|
+
acc.lastTarget = 0;
|
|
270
|
+
acc.steps = 0;
|
|
271
|
+
acc.events = 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 捏合的一次 move:只累积 + 排队,不打日志 */
|
|
275
|
+
function onPinchMove(pinchStart, dist, ratio) {
|
|
276
|
+
if (!(ratio > 0)) return;
|
|
277
|
+
if (!acc.active || acc.kind !== 'pinch') {
|
|
278
|
+
reset();
|
|
279
|
+
acc.active = true;
|
|
280
|
+
acc.kind = 'pinch';
|
|
281
|
+
acc.startDist = pinchStart.dist;
|
|
282
|
+
}
|
|
283
|
+
acc.lastDist = dist;
|
|
284
|
+
acc.lastTarget = pinchStart.scale * ratio;
|
|
285
|
+
acc.moves += 1;
|
|
286
|
+
scheduler.queueTarget(acc.lastTarget);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 滚轮的一次 tick:只累积 + 排队;停手 200ms 视为一次手势结束 */
|
|
290
|
+
function onWheelTick(dir) {
|
|
291
|
+
if (!acc.active || acc.kind !== 'wheel') {
|
|
292
|
+
reset();
|
|
293
|
+
acc.active = true;
|
|
294
|
+
acc.kind = 'wheel';
|
|
295
|
+
}
|
|
296
|
+
acc.steps += dir;
|
|
297
|
+
acc.events += 1;
|
|
298
|
+
scheduler.queueStep(dir);
|
|
299
|
+
if (wheelTimer) clearTimeout(wheelTimer);
|
|
300
|
+
wheelTimer = setTimeout(() => { wheelTimer = 0; finish('wheel'); }, 200);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 结束一次手势:冲刷末帧 + 打一条汇总(中间过程一条都不打) */
|
|
304
|
+
function finish(kind) {
|
|
305
|
+
if (!acc.active || acc.kind !== kind) return null;
|
|
306
|
+
scheduler.flushNow();
|
|
307
|
+
const calls = scheduler.stats().calls;
|
|
308
|
+
if (kind === 'pinch') {
|
|
309
|
+
ctx.camera.note(
|
|
310
|
+
`pinch ${Math.round(acc.startDist)}→${Math.round(acc.lastDist)}(${acc.moves} moves, ${calls} 帧)`
|
|
311
|
+
+ ` 最终目标 ${acc.lastTarget.toFixed(4)} [camera calls: ${calls}]`,
|
|
312
|
+
{ moves: acc.moves, calls, target: Number(acc.lastTarget.toFixed(4)) },
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
ctx.camera.note(
|
|
316
|
+
`wheel ${acc.events} events → zoomStep(${acc.steps > 0 ? '+' : ''}${acc.steps}) [camera calls: ${calls}]`,
|
|
317
|
+
{ events: acc.events, calls, steps: acc.steps },
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
lastReport = {
|
|
321
|
+
kind,
|
|
322
|
+
moves: acc.moves,
|
|
323
|
+
events: acc.events,
|
|
324
|
+
calls,
|
|
325
|
+
steps: acc.steps,
|
|
326
|
+
target: Number(acc.lastTarget.toFixed(4)),
|
|
327
|
+
startDist: Math.round(acc.startDist),
|
|
328
|
+
lastDist: Math.round(acc.lastDist),
|
|
329
|
+
};
|
|
330
|
+
scheduler.resetStats();
|
|
331
|
+
reset();
|
|
332
|
+
return lastReport;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
onPinchMove,
|
|
337
|
+
onWheelTick,
|
|
338
|
+
finish,
|
|
339
|
+
last: () => lastReport,
|
|
340
|
+
/** 面板关掉/解绑时清掉定时器,避免残留回调 */
|
|
341
|
+
dispose() { if (wheelTimer) clearTimeout(wheelTimer); wheelTimer = 0; },
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** 单指拖动:累积位移 + 排队平移(逐帧不写日志,抬手时由 reportDrag 汇总) */
|
|
346
|
+
function queueDrag(panScheduler, previous, point) {
|
|
347
|
+
const dx = point.x - previous.x;
|
|
348
|
+
const dy = point.y - previous.y;
|
|
349
|
+
if (!dx && !dy) return false;
|
|
350
|
+
dragAcc.dx += dx;
|
|
351
|
+
dragAcc.dy += dy;
|
|
352
|
+
dragAcc.moves += 1;
|
|
353
|
+
panScheduler.queue(dx, dy);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** 抬手时把一次拖动汇总成一条(帧数一并给出,便于判断"轻点"还是"长拖") */
|
|
358
|
+
function reportDrag(ctx) {
|
|
359
|
+
if (dragAcc.moves <= 0) return;
|
|
360
|
+
ctx.camera.note(
|
|
361
|
+
`drag total dx=${Math.round(dragAcc.dx)} dy=${Math.round(dragAcc.dy)}(${dragAcc.moves} 帧)`,
|
|
362
|
+
{ dx: Math.round(dragAcc.dx), dy: Math.round(dragAcc.dy), moves: dragAcc.moves },
|
|
363
|
+
);
|
|
364
|
+
resetDragAcc();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** 建捏合基线:起始 userScale + 两指距离 */
|
|
368
|
+
function makePinchStart(camera, a, b) {
|
|
369
|
+
const scale = camera.scale();
|
|
370
|
+
return { scale: Number.isFinite(scale) ? scale : null, dist: distance(a, b) };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 双指:缩放(起始 scale × 距离比,绝对式)+ 中点位移平移。
|
|
375
|
+
* @returns {{x:number, y:number}} 本次的中点,供下层作为下一帧基线
|
|
376
|
+
*/
|
|
377
|
+
function queuePinch(panScheduler, zoomReporter, pinchStart, pinchMid, a, b) {
|
|
378
|
+
const dist = distance(a, b);
|
|
379
|
+
const mid = midpoint(a, b);
|
|
380
|
+
if (pinchStart && pinchStart.dist > 0 && Number.isFinite(pinchStart.scale)) {
|
|
381
|
+
const ratio = dist / pinchStart.dist;
|
|
382
|
+
if (ratio > 0 && Math.abs(ratio - 1) > 0.001) zoomReporter.onPinchMove(pinchStart, dist, ratio);
|
|
383
|
+
}
|
|
384
|
+
if (pinchMid) {
|
|
385
|
+
const dx = mid.x - pinchMid.x;
|
|
386
|
+
const dy = mid.y - pinchMid.y;
|
|
387
|
+
if (dx || dy) panScheduler.queue(dx, dy);
|
|
388
|
+
}
|
|
389
|
+
return mid;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function createPointerHandler(ctx, panScheduler, zoomScheduler, zoomReporter) {
|
|
393
|
+
const active = new Map(); // pointerId → 最近一次画布内坐标
|
|
394
|
+
/** 捏合基线:起始 userScale 与起始两指距离 */
|
|
395
|
+
let pinchStart = null;
|
|
396
|
+
/** 上一帧两指中点(用于双指平移) */
|
|
397
|
+
let pinchMid = null;
|
|
398
|
+
|
|
399
|
+
/** 重建捏合基线(双指齐全时才有) */
|
|
400
|
+
function rebuildPinch() {
|
|
401
|
+
if (active.size !== 2) { pinchStart = null; pinchMid = null; return; }
|
|
402
|
+
const [a, b] = [...active.values()];
|
|
403
|
+
pinchStart = makePinchStart(ctx.camera, a, b);
|
|
404
|
+
pinchMid = midpoint(a, b);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** 按下:记录起点、锁定指针捕获;第二指落下时建立捏合基线 */
|
|
408
|
+
function onDown(event) {
|
|
409
|
+
if (event.pointerType === 'mouse' && event.button !== 0) return; // 仅鼠标左键
|
|
410
|
+
active.set(event.pointerId, toCanvasPoint(ctx.canvas, event));
|
|
411
|
+
try { ctx.canvas.setPointerCapture(event.pointerId); } catch { /* 合成事件可能不支持 */ }
|
|
412
|
+
rebuildPinch();
|
|
413
|
+
event.preventDefault();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** 移动:单指 → 原始差值平移;双指 → 距离比缩放 + 中点平移 */
|
|
417
|
+
function onMove(event) {
|
|
418
|
+
const previous = active.get(event.pointerId);
|
|
419
|
+
if (!previous) return;
|
|
420
|
+
const point = toCanvasPoint(ctx.canvas, event);
|
|
421
|
+
active.set(event.pointerId, point);
|
|
422
|
+
|
|
423
|
+
if (active.size === 1) {
|
|
424
|
+
if (!ctx.options.panWithOnePointer) return;
|
|
425
|
+
if (queueDrag(panScheduler, previous, point)) event.preventDefault();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (active.size === 2) {
|
|
429
|
+
const [a, b] = [...active.values()];
|
|
430
|
+
pinchMid = queuePinch(panScheduler, zoomReporter, pinchStart, pinchMid, a, b);
|
|
431
|
+
event.preventDefault();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** 抬起/取消:移除指针并按剩余手指数重建基线(双指→单指不跳变) */
|
|
436
|
+
function onUp(event) {
|
|
437
|
+
if (!active.delete(event.pointerId)) return;
|
|
438
|
+
try { ctx.canvas.releasePointerCapture(event.pointerId); } catch { /* 同上 */ }
|
|
439
|
+
rebuildPinch();
|
|
440
|
+
reportDrag(ctx); // 有位移才记一条汇总
|
|
441
|
+
// 缩放手势收尾:末帧必须落地 + 打一条汇总(中间过程一条都不打)
|
|
442
|
+
if (active.size < 2) zoomReporter.finish('pinch');
|
|
443
|
+
if (active.size === 1) {
|
|
444
|
+
const only = [...active.values()][0];
|
|
445
|
+
if (only) pinchMid = { x: only.x, y: only.y };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** 滚轮:只看方向、每次一档;锚点是视图中心 */
|
|
450
|
+
function onWheel(event) {
|
|
451
|
+
event.preventDefault();
|
|
452
|
+
zoomReporter.onWheelTick(event.deltaY > 0 ? -1 : 1);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return { onDown, onMove, onUp, onWheel };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* 把交互挂到画布上。
|
|
460
|
+
* @param {{canvas: HTMLCanvasElement, stage: object, options?: object}} params
|
|
461
|
+
* @returns {() => void} 解绑函数
|
|
462
|
+
*/
|
|
463
|
+
export function attachStageInteraction({ canvas, stage, options } = {}) {
|
|
464
|
+
if (!canvas || typeof canvas.addEventListener !== 'function') {
|
|
465
|
+
throw new Error('attachStageInteraction: 需要 canvas 元素');
|
|
466
|
+
}
|
|
467
|
+
if (!stage || (typeof stage.getView !== 'function' && !stage.camera)) {
|
|
468
|
+
throw new Error('attachStageInteraction: 需要提供 camera 入口或 getView/setView 的 stage');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const camera = createCameraBridge(stage);
|
|
472
|
+
const ctx = {
|
|
473
|
+
canvas,
|
|
474
|
+
camera,
|
|
475
|
+
options: Object.assign({}, DEFAULT_OPTIONS, options || {}),
|
|
476
|
+
};
|
|
477
|
+
const panScheduler = createPanScheduler((dx, dy) => camera.panBy(dx, dy));
|
|
478
|
+
// 缩放同样按帧合并:捏合传绝对目标、滚轮传步进(上限仍由相机层钳制)
|
|
479
|
+
const zoomScheduler = createZoomScheduler(
|
|
480
|
+
(target) => camera.zoomTo(target),
|
|
481
|
+
(steps) => camera.zoomStep(steps),
|
|
482
|
+
);
|
|
483
|
+
// 验收要读"真实相机调用次数":暴露成函数(每次手势后 reset,读到的就是本次手势的数值)
|
|
484
|
+
if (typeof window !== 'undefined') {
|
|
485
|
+
window.__WEB__ = window.__WEB__ || {};
|
|
486
|
+
window.__WEB__.zoomStats = () => zoomScheduler.stats();
|
|
487
|
+
// 最近一次手势的汇总(含真实 calls);stats 每轮手势后清零,验收读这个更稳
|
|
488
|
+
window.__WEB__.zoomLast = () => zoomReporter.last();
|
|
489
|
+
}
|
|
490
|
+
const zoomReporter = createZoomReporter(ctx, zoomScheduler);
|
|
491
|
+
const handler = createPointerHandler(ctx, panScheduler, zoomScheduler, zoomReporter);
|
|
492
|
+
|
|
493
|
+
// 触摸设备上禁用浏览器默认手势,否则拖动会被滚动/缩放抢走
|
|
494
|
+
const previousTouchAction = canvas.style.touchAction;
|
|
495
|
+
canvas.style.touchAction = 'none';
|
|
496
|
+
|
|
497
|
+
canvas.addEventListener('pointerdown', handler.onDown);
|
|
498
|
+
canvas.addEventListener('pointermove', handler.onMove);
|
|
499
|
+
canvas.addEventListener('pointerup', handler.onUp);
|
|
500
|
+
canvas.addEventListener('pointercancel', handler.onUp);
|
|
501
|
+
canvas.addEventListener('wheel', handler.onWheel, { passive: false });
|
|
502
|
+
|
|
503
|
+
/** 解绑:移除监听、还原样式、冲刷尾帧 */
|
|
504
|
+
function detach() {
|
|
505
|
+
panScheduler.flushNow();
|
|
506
|
+
panScheduler.dispose();
|
|
507
|
+
// 缩放也补一次冲刷:最后一次捏合 / 滚轮不能被丢在未执行的 rAF 里
|
|
508
|
+
zoomScheduler.flushNow();
|
|
509
|
+
zoomScheduler.dispose();
|
|
510
|
+
try { zoomReporter.dispose(); } catch { /* 忽略 */ }
|
|
511
|
+
if (typeof window !== 'undefined' && window.__WEB__) {
|
|
512
|
+
delete window.__WEB__.zoomStats;
|
|
513
|
+
delete window.__WEB__.zoomLast;
|
|
514
|
+
}
|
|
515
|
+
canvas.removeEventListener('pointerdown', handler.onDown);
|
|
516
|
+
canvas.removeEventListener('pointermove', handler.onMove);
|
|
517
|
+
canvas.removeEventListener('pointerup', handler.onUp);
|
|
518
|
+
canvas.removeEventListener('pointercancel', handler.onUp);
|
|
519
|
+
canvas.removeEventListener('wheel', handler.onWheel);
|
|
520
|
+
canvas.style.touchAction = previousTouchAction;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return detach;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** 默认参数导出(接线方据此展示/覆盖行为开关) */
|
|
527
|
+
export const STAGE_INTERACTION_DEFAULTS = DEFAULT_OPTIONS;
|