@jshookmcp/jshook 0.2.7 → 0.2.9
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/README.md +36 -5
- package/README.zh.md +36 -5
- package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-BNk-EoBt.mjs} +3 -3
- package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-Cq8q01kp.mjs} +5 -5
- package/dist/ConsoleMonitor-CPVQW1Y-.mjs +2201 -0
- package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-BNPxu0RH.mjs} +1 -1
- package/dist/DetailedDataManager-BQQcxh64.mjs +217 -0
- package/dist/EventBus-DgPmwpeu.mjs +141 -0
- package/dist/EvidenceGraphBridge-SFesNera.mjs +153 -0
- package/dist/{ExtensionManager-CZ6IveoV.mjs → ExtensionManager-CWYgw0YW.mjs} +13 -6
- package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-gzWtkKuf.mjs} +1 -1
- package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-B9gZCdFP.mjs} +3 -3
- package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-BLDH0dCv.mjs} +4 -4
- package/dist/HookGeneratorBuilders.core.generators.storage-CtcdK78Q.mjs +639 -0
- package/dist/InstrumentationSession-CvPC7Jwy.mjs +244 -0
- package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CbVdCIJF.mjs} +3 -3
- package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-BsDZbLYm.mjs} +81 -78
- package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-Bcpml6II.mjs} +44 -18
- package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-dZtA1ZGn.mjs} +14 -53
- package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-B-FjA2mJ.mjs} +1 -1
- package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-D1lzJ_VG.mjs} +2 -2
- package/dist/PageController-Bqm2kZ_X.mjs +417 -0
- package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-BOhyVsjx.mjs} +4 -4
- package/dist/PrerequisiteError-Dl33Svkz.mjs +20 -0
- package/dist/ResponseBuilder-D3iFYx2N.mjs +143 -0
- package/dist/ReverseEvidenceGraph-Dlsk94LC.mjs +269 -0
- package/dist/ScriptManager-aHHq0X7U.mjs +3000 -0
- package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-CqdIFlQl.mjs} +2 -2
- package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-DhFaPvRO.mjs} +3 -3
- package/dist/ToolCatalog-C0JGZoOm.mjs +582 -0
- package/dist/ToolError-jh9whhMd.mjs +15 -0
- package/dist/ToolProbe-oC7aPrkv.mjs +45 -0
- package/dist/ToolRegistry-BjaF4oNz.mjs +131 -0
- package/dist/ToolRouter.policy-BWV67ZK-.mjs +304 -0
- package/dist/TraceRecorder-DgxyVbdQ.mjs +519 -0
- package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-CePkipZY.mjs} +1 -1
- package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-BvKs-gxc.mjs} +2 -2
- package/dist/WorkflowEngine-CuvkZtWu.mjs +598 -0
- package/dist/analysis-CL9uACt9.mjs +463 -0
- package/dist/antidebug-CqDTB_uk.mjs +1081 -0
- package/dist/artifactRetention-CFEprwPw.mjs +591 -0
- package/dist/artifacts-Bk2-_uPq.mjs +59 -0
- package/dist/betterSqlite3-0pqusHHH.mjs +74 -0
- package/dist/binary-instrument-CXfpx6fT.mjs +979 -0
- package/dist/bind-helpers-xFfRF-qm.mjs +22 -0
- package/dist/boringssl-inspector-BH2D3VKc.mjs +180 -0
- package/dist/browser-BpOr5PEx.mjs +4082 -0
- package/dist/concurrency-Bt0yv1kJ.mjs +41 -0
- package/dist/{constants-CCvsN80K.mjs → constants-B0OANIBL.mjs} +88 -46
- package/dist/coordination-qUbyF8KU.mjs +259 -0
- package/dist/debugger-gnKxRSN0.mjs +1271 -0
- package/dist/definitions-6M-eejaT.mjs +53 -0
- package/dist/definitions-B18eyf0B.mjs +18 -0
- package/dist/definitions-B3QdlrHv.mjs +34 -0
- package/dist/definitions-B4rAvHNZ.mjs +63 -0
- package/dist/definitions-BB_4jnmy.mjs +37 -0
- package/dist/definitions-BMfYXoNC.mjs +43 -0
- package/dist/definitions-Beid2EB3.mjs +27 -0
- package/dist/definitions-C1UvM5Iy.mjs +126 -0
- package/dist/definitions-CXEI7QC72.mjs +216 -0
- package/dist/definitions-C_4r7Fo-2.mjs +14 -0
- package/dist/definitions-CkFDALoa.mjs +26 -0
- package/dist/definitions-Cke7zEb8.mjs +94 -0
- package/dist/definitions-ClJLzsJQ.mjs +25 -0
- package/dist/definitions-Cq-zroAU.mjs +28 -0
- package/dist/definitions-Cy3Sl6gV.mjs +34 -0
- package/dist/definitions-D3VsGcvz.mjs +47 -0
- package/dist/definitions-DVGfrn7y.mjs +96 -0
- package/dist/definitions-LKpC3-nL.mjs +9 -0
- package/dist/definitions-bAhHQJq9.mjs +359 -0
- package/dist/encoding-Bvz5jLRv.mjs +1065 -0
- package/dist/evidence-graph-bridge-C_fv9PuC.mjs +135 -0
- package/dist/{factory-CibqTNC8.mjs → factory-DxlGh9Xf.mjs} +37 -52
- package/dist/graphql-DYWzJ29s.mjs +1026 -0
- package/dist/handlers-9sAbfIg-.mjs +2552 -0
- package/dist/handlers-Bl8zkwz1.mjs +2716 -0
- package/dist/handlers-C67ktuRN.mjs +710 -0
- package/dist/handlers-C87g8oCe.mjs +276 -0
- package/dist/handlers-CTsDAO6p.mjs +681 -0
- package/dist/handlers-Cgyg6c0U.mjs +645 -0
- package/dist/handlers-D6j6yka7.mjs +2124 -0
- package/dist/handlers-DdFzXLvF.mjs +446 -0
- package/dist/handlers-DeLOCd5m.mjs +799 -0
- package/dist/handlers-DlCJN4Td.mjs +757 -0
- package/dist/handlers-DxGIq15_2.mjs +917 -0
- package/dist/handlers-U6L4xhuF.mjs +585 -0
- package/dist/handlers-tB9Mp9ZK.mjs +84 -0
- package/dist/handlers-tiy7EIBp.mjs +572 -0
- package/dist/handlers.impl-DS0d9fUw.mjs +761 -0
- package/dist/hooks-CzCWByww.mjs +898 -0
- package/dist/index.mjs +384 -155
- package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
- package/dist/maintenance-P7ePRXQC.mjs +830 -0
- package/dist/manifest-2ToTpjv8.mjs +106 -0
- package/dist/manifest-3g71z6Bg.mjs +79 -0
- package/dist/manifest-82baTv4U.mjs +45 -0
- package/dist/manifest-B3QVVeBS.mjs +82 -0
- package/dist/manifest-BB2J8IMJ.mjs +149 -0
- package/dist/manifest-BKbgbSiY.mjs +60 -0
- package/dist/manifest-Bcf-TJzH.mjs +848 -0
- package/dist/manifest-BmtZzQiQ2.mjs +45 -0
- package/dist/manifest-Bnd7kqEY.mjs +55 -0
- package/dist/manifest-BqQX6OQC2.mjs +65 -0
- package/dist/manifest-BqrQ4Tpj.mjs +81 -0
- package/dist/manifest-Br4RPFt5.mjs +370 -0
- package/dist/manifest-C5qDjysN.mjs +107 -0
- package/dist/manifest-C9RT5nk32.mjs +34 -0
- package/dist/manifest-CAhOuvSl.mjs +204 -0
- package/dist/manifest-CBYWCUBJ.mjs +51 -0
- package/dist/manifest-CFADCRa1.mjs +37 -0
- package/dist/manifest-CQVhavRF.mjs +114 -0
- package/dist/manifest-CT7zZBV1.mjs +48 -0
- package/dist/manifest-CV12bcrF.mjs +121 -0
- package/dist/manifest-CXsRWjjI.mjs +224 -0
- package/dist/manifest-CZLUCfG02.mjs +95 -0
- package/dist/manifest-D6phHKFd.mjs +131 -0
- package/dist/manifest-DCyjf4n2.mjs +294 -0
- package/dist/manifest-DHsnKgP6.mjs +60 -0
- package/dist/manifest-Df_dliIe.mjs +55 -0
- package/dist/manifest-Dh8WBmEW.mjs +129 -0
- package/dist/manifest-DhKRAT8_.mjs +92 -0
- package/dist/manifest-DlpTj4ic2.mjs +193 -0
- package/dist/manifest-DrbmZcFl2.mjs +253 -0
- package/dist/manifest-DuwHjUa5.mjs +70 -0
- package/dist/manifest-DzwvxPJX.mjs +38 -0
- package/dist/manifest-NXctwWQq.mjs +68 -0
- package/dist/manifest-Sc_0JQ13.mjs +418 -0
- package/dist/manifest-gZ4s_UtG.mjs +96 -0
- package/dist/manifest-qSleDqdO.mjs +1023 -0
- package/dist/modules-C184v-S9.mjs +11365 -0
- package/dist/mojo-ipc-B_H61Afw.mjs +525 -0
- package/dist/network-671Cw6hV.mjs +3346 -0
- package/dist/{artifacts-BbdOMET5.mjs → outputPaths-B1uGmrWZ.mjs} +219 -212
- package/dist/parse-args-BlRjqlkL.mjs +39 -0
- package/dist/platform-WmNn8Sxb.mjs +2070 -0
- package/dist/process-QcbIy5Zq.mjs +1401 -0
- package/dist/proxy-DqNs0bAd.mjs +170 -0
- package/dist/registry-D-6e18lB.mjs +34 -0
- package/dist/response-BQVP-xUn.mjs +28 -0
- package/dist/server/plugin-api.mjs +2 -2
- package/dist/shared-state-board-DV-dpHFJ.mjs +586 -0
- package/dist/sourcemap-Dq8ez8vS.mjs +650 -0
- package/dist/ssrf-policy-ZaUfvhq7.mjs +166 -0
- package/dist/streaming-BUQ0VJsg.mjs +725 -0
- package/dist/tool-builder-DCbIC5Eo.mjs +186 -0
- package/dist/transform-CiYJfNX0.mjs +1007 -0
- package/dist/types-Bx92KJfT.mjs +4 -0
- package/dist/wasm-DQTnHDs4.mjs +531 -0
- package/dist/workflow-f3xJOcjx.mjs +725 -0
- package/package.json +48 -78
- package/dist/ExtensionManager-DqUSOamB.mjs +0 -2
- package/dist/ToolCatalog-CnwmMIw3.mjs +0 -61483
- package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-CDe5WPSV.mjs} +0 -0
- package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-Bo4T3bz8.mjs} +0 -0
- package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-CwVLVdDM.mjs} +0 -0
- package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-DVkj9kpI.mjs} +0 -0
- package/dist/{types-BBjOqye-.mjs → types-CPhOReNX.mjs} +1 -1
|
@@ -0,0 +1,2716 @@
|
|
|
1
|
+
import { n as asJsonResponse } from "./response-BQVP-xUn.mjs";
|
|
2
|
+
import { r as argNumber } from "./parse-args-BlRjqlkL.mjs";
|
|
3
|
+
//#region src/server/domains/canvas/adapters/cocos-adapter.ts
|
|
4
|
+
/**
|
|
5
|
+
* Generates a self-contained JS string that traverses the Cocos Creator scene graph
|
|
6
|
+
* via cc.director.getScene() using DFS and returns a serialisable scene tree.
|
|
7
|
+
*
|
|
8
|
+
* Supports both v2 (cc.Node._children) and v3 (cc.Node.children) APIs.
|
|
9
|
+
* World bounds are computed from worldPosition + contentSize (v3) or getBoundingBox() (v2).
|
|
10
|
+
*
|
|
11
|
+
* @param opts - Dump options (maxDepth, onlyInteractive, onlyVisible)
|
|
12
|
+
*/
|
|
13
|
+
function buildCocosSceneTreeDumpPayload(opts) {
|
|
14
|
+
const maxDepth = opts.maxDepth ?? 20;
|
|
15
|
+
const onlyInteractive = opts.onlyInteractive ?? false;
|
|
16
|
+
return `(function() {
|
|
17
|
+
function getChildren(node) {
|
|
18
|
+
if (!node) return [];
|
|
19
|
+
// v3 uses node.children, v2 uses node._children
|
|
20
|
+
if (node.children && Array.isArray(node.children)) return node.children;
|
|
21
|
+
if (node._children && Array.isArray(node._children)) return node._children;
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getNumChildren(node) {
|
|
26
|
+
if (!node) return 0;
|
|
27
|
+
if (node.children && typeof node.children.length === 'number') return node.children.length;
|
|
28
|
+
if (node._children && typeof node._children.length === 'number') return node._children.length;
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Determine if we are on Cocos v3 (has cc.Scene) vs v2
|
|
33
|
+
var isV3 = !!(window.cc && window.cc.Scene);
|
|
34
|
+
|
|
35
|
+
// Cocos v3 node properties
|
|
36
|
+
function getNodeId(node, idx) {
|
|
37
|
+
try {
|
|
38
|
+
if (node.uuid !== undefined && node.uuid !== null && node.uuid !== '') return String(node.uuid);
|
|
39
|
+
} catch(e) {}
|
|
40
|
+
try {
|
|
41
|
+
if (node._uuid !== undefined && node._uuid !== null && node._uuid !== '') return String(node._uuid);
|
|
42
|
+
} catch(e) {}
|
|
43
|
+
return (node.constructor ? node.constructor.name : 'Node') + '_' + idx;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function safeProp(node, key, fallback) {
|
|
47
|
+
try { var v = node[key]; return v === undefined || v === null ? fallback : v; } catch(e) { return fallback; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getWorldBounds(node) {
|
|
51
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
52
|
+
try {
|
|
53
|
+
if (isV3) {
|
|
54
|
+
// v3: use worldPosition + contentSize
|
|
55
|
+
var wp = node.worldPosition;
|
|
56
|
+
var cs = node.contentSize;
|
|
57
|
+
if (wp && cs) {
|
|
58
|
+
return {
|
|
59
|
+
x: wp.x - (cs.width / 2),
|
|
60
|
+
y: wp.y - (cs.height / 2),
|
|
61
|
+
width: cs.width,
|
|
62
|
+
height: cs.height
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// fallback: use position + scale + contentSize
|
|
66
|
+
var pos = node.position;
|
|
67
|
+
var sx = safeProp(node, 'scaleX', 1);
|
|
68
|
+
var sy = safeProp(node, 'scaleY', 1);
|
|
69
|
+
var w = cs ? cs.width : safeProp(node, 'width', 0);
|
|
70
|
+
var h = cs ? cs.height : safeProp(node, 'height', 0);
|
|
71
|
+
return {
|
|
72
|
+
x: pos ? pos.x : safeProp(node, 'x', 0),
|
|
73
|
+
y: pos ? pos.y : safeProp(node, 'y', 0),
|
|
74
|
+
width: Math.abs(w * sx),
|
|
75
|
+
height: Math.abs(h * sy)
|
|
76
|
+
};
|
|
77
|
+
} else {
|
|
78
|
+
// v2: use getBoundingBox()
|
|
79
|
+
if (typeof node.getBoundingBox === 'function') {
|
|
80
|
+
var bb = node.getBoundingBox();
|
|
81
|
+
if (bb) return { x: bb.x, y: bb.y, width: bb.width, height: bb.height };
|
|
82
|
+
}
|
|
83
|
+
// v2 fallback
|
|
84
|
+
var px = safeProp(node, 'x', 0) || 0;
|
|
85
|
+
var py = safeProp(node, 'y', 0) || 0;
|
|
86
|
+
var sx2 = safeProp(node, 'scaleX', 1);
|
|
87
|
+
var sy2 = safeProp(node, 'scaleY', 1);
|
|
88
|
+
var w2 = safeProp(node, '_contentSize') && safeProp(node._contentSize, 'width', 0) || safeProp(node, 'width', 0);
|
|
89
|
+
var h2 = safeProp(node, '_contentSize') && safeProp(node._contentSize, 'height', 0) || safeProp(node, 'height', 0);
|
|
90
|
+
return {
|
|
91
|
+
x: px, y: py,
|
|
92
|
+
width: Math.abs(w2 * sx2),
|
|
93
|
+
height: Math.abs(h2 * sy2)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
} catch(e) {
|
|
97
|
+
return {
|
|
98
|
+
x: safeProp(node, 'x', 0),
|
|
99
|
+
y: safeProp(node, 'y', 0),
|
|
100
|
+
width: safeProp(node, 'width', 0) || 0,
|
|
101
|
+
height: safeProp(node, 'height', 0) || 0
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isInteractive(node) {
|
|
107
|
+
try {
|
|
108
|
+
// v3: check for Button component or _eventMask
|
|
109
|
+
if (isV3) {
|
|
110
|
+
var comps = node.components;
|
|
111
|
+
if (comps && Array.isArray(comps)) {
|
|
112
|
+
for (var ci = 0; ci < comps.length; ci++) {
|
|
113
|
+
var c = comps[ci];
|
|
114
|
+
if (c && c.constructor && (c.constructor.name === 'Button' || c.constructor.name === 'UICanvas' || c.constructor.name === 'Sprite')) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (node._eventMask !== undefined) return !!(node._eventMask & 1);
|
|
120
|
+
} else {
|
|
121
|
+
// v2: check mouseEnabled
|
|
122
|
+
return !!(safeProp(node, 'mouseEnabled', true));
|
|
123
|
+
}
|
|
124
|
+
} catch(e) {}
|
|
125
|
+
return !!(safeProp(node, 'mouseEnabled', true));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getActive(node) {
|
|
129
|
+
if (isV3) {
|
|
130
|
+
return !!(safeProp(node, 'active', true));
|
|
131
|
+
} else {
|
|
132
|
+
return !!(safeProp(node, '_active', true));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getName(node) {
|
|
137
|
+
if (isV3) {
|
|
138
|
+
return safeProp(node, 'name', undefined);
|
|
139
|
+
} else {
|
|
140
|
+
return safeProp(node, '_name', undefined);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getPosition(node) {
|
|
145
|
+
if (isV3) {
|
|
146
|
+
var p = node.position;
|
|
147
|
+
if (p && typeof p.x === 'number') return { x: p.x, y: p.y };
|
|
148
|
+
return { x: safeProp(node, 'x', 0), y: safeProp(node, 'y', 0) };
|
|
149
|
+
} else {
|
|
150
|
+
return { x: safeProp(node, 'x', 0), y: safeProp(node, 'y', 0) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getScale(node) {
|
|
155
|
+
return {
|
|
156
|
+
x: safeProp(node, 'scaleX', 1),
|
|
157
|
+
y: safeProp(node, 'scaleY', 1)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getRotation(node) {
|
|
162
|
+
if (isV3) {
|
|
163
|
+
return {
|
|
164
|
+
x: safeProp(node, 'rotationX', 0),
|
|
165
|
+
y: safeProp(node, 'rotationY', 0),
|
|
166
|
+
z: safeProp(node, 'rotation', 0)
|
|
167
|
+
};
|
|
168
|
+
} else {
|
|
169
|
+
return {
|
|
170
|
+
x: safeProp(node, 'rotationX', 0),
|
|
171
|
+
y: safeProp(node, 'rotationY', 0),
|
|
172
|
+
z: safeProp(node, 'rotation', 0)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getType(node) {
|
|
178
|
+
if (isV3) {
|
|
179
|
+
return node.constructor ? node.constructor.name : 'Node';
|
|
180
|
+
} else {
|
|
181
|
+
// v2: use _className if available
|
|
182
|
+
return safeProp(node, '_className', node.constructor ? node.constructor.name : 'Node');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
var totalNodes = 0;
|
|
187
|
+
|
|
188
|
+
function traverse(node, depth, path) {
|
|
189
|
+
if (!node || depth > ${maxDepth}) return null;
|
|
190
|
+
totalNodes++;
|
|
191
|
+
|
|
192
|
+
var active = getActive(node);
|
|
193
|
+
var interactive = isInteractive(node);
|
|
194
|
+
var visible = active;
|
|
195
|
+
|
|
196
|
+
if (${opts.onlyVisible ?? false} && !visible) return null;
|
|
197
|
+
if (${onlyInteractive} && !interactive) return null;
|
|
198
|
+
|
|
199
|
+
var wb = getWorldBounds(node);
|
|
200
|
+
var pos = getPosition(node);
|
|
201
|
+
var scale = getScale(node);
|
|
202
|
+
var rot = getRotation(node);
|
|
203
|
+
var numC = getNumChildren(node);
|
|
204
|
+
var children = null;
|
|
205
|
+
|
|
206
|
+
if (numC > 0) {
|
|
207
|
+
children = [];
|
|
208
|
+
var nodeChildren = getChildren(node);
|
|
209
|
+
for (var i = 0; i < nodeChildren.length; i++) {
|
|
210
|
+
var cn = nodeChildren[i];
|
|
211
|
+
if (!cn) continue;
|
|
212
|
+
var childPath = path ? path + '/' + getNodeId(cn, i) : getNodeId(cn, i);
|
|
213
|
+
var sub = traverse(cn, depth + 1, childPath);
|
|
214
|
+
if (sub) children.push(sub);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
var alpha = safeProp(node, 'opacity', 1);
|
|
219
|
+
if (alpha === undefined || alpha === null) alpha = safeProp(node, 'alpha', 1);
|
|
220
|
+
|
|
221
|
+
var width = safeProp(node, 'width', 0) || 0;
|
|
222
|
+
var height = safeProp(node, 'height', 0) || 0;
|
|
223
|
+
if (isV3 && node.contentSize) {
|
|
224
|
+
width = safeProp(node.contentSize, 'width', 0) || width;
|
|
225
|
+
height = safeProp(node.contentSize, 'height', 0) || height;
|
|
226
|
+
} else if (!isV3 && node._contentSize) {
|
|
227
|
+
width = safeProp(node._contentSize, 'width', 0) || width;
|
|
228
|
+
height = safeProp(node._contentSize, 'height', 0) || height;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
var result = {
|
|
232
|
+
id: getNodeId(node, 0),
|
|
233
|
+
type: getType(node),
|
|
234
|
+
name: getName(node),
|
|
235
|
+
visible: visible,
|
|
236
|
+
interactive: interactive,
|
|
237
|
+
alpha: alpha,
|
|
238
|
+
x: pos.x,
|
|
239
|
+
y: pos.y,
|
|
240
|
+
width: width,
|
|
241
|
+
height: height,
|
|
242
|
+
worldBounds: wb,
|
|
243
|
+
path: path || getNodeId(node, 0),
|
|
244
|
+
customData: {
|
|
245
|
+
scaleX: scale.x,
|
|
246
|
+
scaleY: scale.y,
|
|
247
|
+
rotationX: rot.x,
|
|
248
|
+
rotationY: rot.y,
|
|
249
|
+
rotation: rot.z,
|
|
250
|
+
active: active,
|
|
251
|
+
parent: node.parent ? getNodeId(node.parent, -1) : null,
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (children && children.length > 0) result.children = children;
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!window.cc || !window.cc.director) {
|
|
260
|
+
return {
|
|
261
|
+
engine: 'CocosCreator',
|
|
262
|
+
version: window.cc && window.cc.ENGINE_VERSION ? window.cc.ENGINE_VERSION : undefined,
|
|
263
|
+
canvas: { width: 0, height: 0, dpr: 1, contextType: 'unknown' },
|
|
264
|
+
sceneTree: null,
|
|
265
|
+
totalNodes: 0,
|
|
266
|
+
completeness: 'partial',
|
|
267
|
+
error: 'cc.director not found'
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var scene = window.cc.director.getScene ? window.cc.director.getScene() : null;
|
|
272
|
+
|
|
273
|
+
var canvasEl = document.querySelector('canvas');
|
|
274
|
+
var cocosVersion = window.cc.ENGINE_VERSION || (isV3 ? '3.x' : '2.x');
|
|
275
|
+
var canvasInfo = {
|
|
276
|
+
width: canvasEl ? canvasEl.width : safeProp(scene, 'width', 0) || 0,
|
|
277
|
+
height: canvasEl ? canvasEl.height : safeProp(scene, 'height', 0) || 0,
|
|
278
|
+
dpr: window.devicePixelRatio || 1,
|
|
279
|
+
contextType: 'unknown'
|
|
280
|
+
};
|
|
281
|
+
if (canvasEl) {
|
|
282
|
+
var gl = canvasEl.getContext('webgl2') || canvasEl.getContext('webgl');
|
|
283
|
+
canvasInfo.contextType = gl ? (gl instanceof WebGL2RenderingContext ? 'webgl2' : 'webgl') : '2d';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
var sceneTree = scene ? traverse(scene, 0, 'cc.director.getScene()') : null;
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
engine: 'CocosCreator',
|
|
290
|
+
version: cocosVersion,
|
|
291
|
+
canvas: canvasInfo,
|
|
292
|
+
sceneTree: sceneTree,
|
|
293
|
+
totalNodes: totalNodes,
|
|
294
|
+
completeness: sceneTree ? 'full' : 'partial',
|
|
295
|
+
_meta: { isV3: isV3 }
|
|
296
|
+
};
|
|
297
|
+
})()`;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Generates a self-contained JS string that:
|
|
301
|
+
* 1. Transforms screen coordinates → canvas coordinates
|
|
302
|
+
* 2. Flips Y axis (Cocos uses bottom-left origin, browser uses top-left)
|
|
303
|
+
* 3. Runs hit test via engine (v3) or recursive DFS bounds check (v2 / fallback)
|
|
304
|
+
* 4. Returns all candidates sorted by depth (topmost first)
|
|
305
|
+
*
|
|
306
|
+
* @param opts - Pick options (x, y, canvasId)
|
|
307
|
+
*/
|
|
308
|
+
function buildCocosHitTestPayload(opts) {
|
|
309
|
+
const x = opts.x;
|
|
310
|
+
const y = opts.y;
|
|
311
|
+
const canvasId = opts.canvasId;
|
|
312
|
+
return `(function() {
|
|
313
|
+
function getChildren(node) {
|
|
314
|
+
if (!node) return [];
|
|
315
|
+
if (node.children && Array.isArray(node.children)) return node.children;
|
|
316
|
+
if (node._children && Array.isArray(node._children)) return node._children;
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getNumChildren(node) {
|
|
321
|
+
if (!node) return 0;
|
|
322
|
+
if (node.children && typeof node.children.length === 'number') return node.children.length;
|
|
323
|
+
if (node._children && typeof node._children.length === 'number') return node._children.length;
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getNodeId(node, idx) {
|
|
328
|
+
try {
|
|
329
|
+
if (node.uuid !== undefined && node.uuid !== null && node.uuid !== '') return String(node.uuid);
|
|
330
|
+
} catch(e) {}
|
|
331
|
+
try {
|
|
332
|
+
if (node._uuid !== undefined && node._uuid !== null && node._uuid !== '') return String(node._uuid);
|
|
333
|
+
} catch(e) {}
|
|
334
|
+
return (node.constructor ? node.constructor.name : 'Node') + '_' + idx;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function safeProp(node, key, fallback) {
|
|
338
|
+
try { var v = node[key]; return v === undefined || v === null ? fallback : v; } catch(e) { return fallback; }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getWorldBounds(node) {
|
|
342
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
343
|
+
try {
|
|
344
|
+
var isV3 = !!(window.cc && window.cc.Scene);
|
|
345
|
+
if (isV3) {
|
|
346
|
+
var wp = node.worldPosition;
|
|
347
|
+
var cs = node.contentSize;
|
|
348
|
+
if (wp && cs) {
|
|
349
|
+
return {
|
|
350
|
+
x: wp.x - (cs.width / 2),
|
|
351
|
+
y: wp.y - (cs.height / 2),
|
|
352
|
+
width: cs.width,
|
|
353
|
+
height: cs.height
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
var pos = node.position;
|
|
357
|
+
var sx = safeProp(node, 'scaleX', 1);
|
|
358
|
+
var sy = safeProp(node, 'scaleY', 1);
|
|
359
|
+
var w = cs ? cs.width : safeProp(node, 'width', 0);
|
|
360
|
+
var h = cs ? cs.height : safeProp(node, 'height', 0);
|
|
361
|
+
return {
|
|
362
|
+
x: pos ? pos.x : safeProp(node, 'x', 0),
|
|
363
|
+
y: pos ? pos.y : safeProp(node, 'y', 0),
|
|
364
|
+
width: Math.abs(w * sx),
|
|
365
|
+
height: Math.abs(h * sy)
|
|
366
|
+
};
|
|
367
|
+
} else {
|
|
368
|
+
if (typeof node.getBoundingBox === 'function') {
|
|
369
|
+
var bb = node.getBoundingBox();
|
|
370
|
+
if (bb) return { x: bb.x, y: bb.y, width: bb.width, height: bb.height };
|
|
371
|
+
}
|
|
372
|
+
var px = safeProp(node, 'x', 0) || 0;
|
|
373
|
+
var py = safeProp(node, 'y', 0) || 0;
|
|
374
|
+
var sx2 = safeProp(node, 'scaleX', 1);
|
|
375
|
+
var sy2 = safeProp(node, 'scaleY', 1);
|
|
376
|
+
var w2 = safeProp(node, '_contentSize') ? safeProp(node._contentSize, 'width', 0) : safeProp(node, 'width', 0);
|
|
377
|
+
var h2 = safeProp(node, '_contentSize') ? safeProp(node._contentSize, 'height', 0) : safeProp(node, 'height', 0);
|
|
378
|
+
return {
|
|
379
|
+
x: px, y: py,
|
|
380
|
+
width: Math.abs(w2 * sx2),
|
|
381
|
+
height: Math.abs(h2 * sy2)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
} catch(e) {
|
|
385
|
+
return {
|
|
386
|
+
x: safeProp(node, 'x', 0),
|
|
387
|
+
y: safeProp(node, 'y', 0),
|
|
388
|
+
width: safeProp(node, 'width', 0) || 0,
|
|
389
|
+
height: safeProp(node, 'height', 0) || 0
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isInteractive(node) {
|
|
395
|
+
try {
|
|
396
|
+
var isV3 = !!(window.cc && window.cc.Scene);
|
|
397
|
+
if (isV3) {
|
|
398
|
+
var comps = node.components;
|
|
399
|
+
if (comps && Array.isArray(comps)) {
|
|
400
|
+
for (var ci = 0; ci < comps.length; ci++) {
|
|
401
|
+
var c = comps[ci];
|
|
402
|
+
if (c && c.constructor && (c.constructor.name === 'Button' || c.constructor.name === 'Sprite')) {
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (node._eventMask !== undefined) return !!(node._eventMask & 1);
|
|
408
|
+
} else {
|
|
409
|
+
return !!(safeProp(node, 'mouseEnabled', true));
|
|
410
|
+
}
|
|
411
|
+
} catch(e) {}
|
|
412
|
+
return !!(safeProp(node, 'mouseEnabled', true));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function getActive(node) {
|
|
416
|
+
var isV3 = !!(window.cc && window.cc.Scene);
|
|
417
|
+
if (isV3) return !!(safeProp(node, 'active', true));
|
|
418
|
+
return !!(safeProp(node, '_active', true));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getName(node) {
|
|
422
|
+
var isV3 = !!(window.cc && window.cc.Scene);
|
|
423
|
+
if (isV3) return safeProp(node, 'name', undefined);
|
|
424
|
+
return safeProp(node, '_name', undefined);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getPosition(node) {
|
|
428
|
+
var isV3 = !!(window.cc && window.cc.Scene);
|
|
429
|
+
if (isV3) {
|
|
430
|
+
var p = node.position;
|
|
431
|
+
if (p && typeof p.x === 'number') return { x: p.x, y: p.y };
|
|
432
|
+
return { x: safeProp(node, 'x', 0), y: safeProp(node, 'y', 0) };
|
|
433
|
+
}
|
|
434
|
+
return { x: safeProp(node, 'x', 0), y: safeProp(node, 'y', 0) };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function getType(node) {
|
|
438
|
+
var isV3 = !!(window.cc && window.cc.Scene);
|
|
439
|
+
if (isV3) return node.constructor ? node.constructor.name : 'Node';
|
|
440
|
+
return safeProp(node, '_className', node.constructor ? node.constructor.name : 'Node');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function nodePath(node) {
|
|
444
|
+
var parts = [];
|
|
445
|
+
var cur = node;
|
|
446
|
+
var scene = window.cc && window.cc.director ? window.cc.director.getScene() : null;
|
|
447
|
+
while (cur && cur !== scene) {
|
|
448
|
+
var name = getName(cur) || getNodeId(cur, 0);
|
|
449
|
+
parts.unshift(name);
|
|
450
|
+
cur = cur.parent;
|
|
451
|
+
}
|
|
452
|
+
parts.unshift('cc.director.getScene()');
|
|
453
|
+
return parts.join('/');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
var sx = ${x}, sy = ${y};
|
|
457
|
+
|
|
458
|
+
// Find the target canvas
|
|
459
|
+
var canvases = Array.from(document.querySelectorAll('canvas'));
|
|
460
|
+
var targetCanvas = null;
|
|
461
|
+
${canvasId ? `targetCanvas = document.getElementById(${JSON.stringify(canvasId)}) || canvases[parseInt(${JSON.stringify(canvasId)})] || null;` : `
|
|
462
|
+
for (var ci = canvases.length - 1; ci >= 0; ci--) {
|
|
463
|
+
var r = canvases[ci].getBoundingClientRect();
|
|
464
|
+
if (sx >= r.left && sx <= r.right && sy >= r.top && sy <= r.bottom) {
|
|
465
|
+
targetCanvas = canvases[ci];
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}`}
|
|
469
|
+
|
|
470
|
+
if (!window.cc || !window.cc.director) {
|
|
471
|
+
return {
|
|
472
|
+
success: false, picked: null, candidates: [], coordinates: {
|
|
473
|
+
screen: { x: sx, y: sy }, canvas: { x: 0, y: 0 }
|
|
474
|
+
},
|
|
475
|
+
hitTestMethod: 'none'
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
var canvasWidth = targetCanvas ? targetCanvas.width : 1920;
|
|
480
|
+
var canvasHeight = targetCanvas ? targetCanvas.height : 1080;
|
|
481
|
+
|
|
482
|
+
// Screen → canvas (CSS pixels to canvas pixels)
|
|
483
|
+
var canvasX = sx, canvasY = sy;
|
|
484
|
+
if (targetCanvas) {
|
|
485
|
+
var rect = targetCanvas.getBoundingClientRect();
|
|
486
|
+
canvasX = (sx - rect.left) * (targetCanvas.width / rect.width);
|
|
487
|
+
canvasY = (sy - rect.top) * (targetCanvas.height / rect.height);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Canvas → Cocos: FLIP Y because Cocos uses bottom-left origin
|
|
491
|
+
var cocosX = canvasX;
|
|
492
|
+
var cocosY = canvasHeight - canvasY;
|
|
493
|
+
|
|
494
|
+
var scene = window.cc.director.getScene ? window.cc.director.getScene() : null;
|
|
495
|
+
var candidates = [];
|
|
496
|
+
var hitTestMethod = 'none';
|
|
497
|
+
var enginePicked = null;
|
|
498
|
+
|
|
499
|
+
// Try v3 native hitTest via cc.Node.EventListener.POSITION
|
|
500
|
+
if (scene && typeof scene.hitTest === 'function') {
|
|
501
|
+
try {
|
|
502
|
+
var nativeHit = scene.hitTest({ x: cocosX, y: cocosY });
|
|
503
|
+
if (nativeHit) {
|
|
504
|
+
enginePicked = nativeHit;
|
|
505
|
+
hitTestMethod = 'engine';
|
|
506
|
+
}
|
|
507
|
+
} catch(e) {}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Recursive DFS hit test — always available
|
|
511
|
+
function hitTestDfs(node, depth, accPath) {
|
|
512
|
+
if (!node) return;
|
|
513
|
+
|
|
514
|
+
var active = getActive(node);
|
|
515
|
+
if (!active) return;
|
|
516
|
+
|
|
517
|
+
var wb = getWorldBounds(node);
|
|
518
|
+
|
|
519
|
+
// Convert screen coordinates to node local
|
|
520
|
+
var cur = node;
|
|
521
|
+
var screenPt = { x: sx, y: sy };
|
|
522
|
+
while (cur) {
|
|
523
|
+
if (cur.globalToLocal) {
|
|
524
|
+
try { screenPt = cur.globalToLocal(screenPt); } catch(e) { break; }
|
|
525
|
+
}
|
|
526
|
+
cur = cur.parent;
|
|
527
|
+
}
|
|
528
|
+
var lx = screenPt.x, ly = screenPt.y;
|
|
529
|
+
|
|
530
|
+
var pos = getPosition(node);
|
|
531
|
+
var nx = pos.x, ny = pos.y;
|
|
532
|
+
var sx2 = safeProp(node, 'scaleX', 1);
|
|
533
|
+
var sy2 = safeProp(node, 'scaleY', 1);
|
|
534
|
+
var w = safeProp(node, 'width', 0) || wb.width;
|
|
535
|
+
var h = safeProp(node, 'height', 0) || wb.height;
|
|
536
|
+
|
|
537
|
+
var inBounds = lx >= nx && lx <= nx + w * Math.abs(sx2) &&
|
|
538
|
+
ly >= ny && ly <= ny + h * Math.abs(sy2);
|
|
539
|
+
|
|
540
|
+
var interactive = isInteractive(node);
|
|
541
|
+
|
|
542
|
+
if (inBounds && interactive) {
|
|
543
|
+
var path = accPath || getNodeId(node, 0);
|
|
544
|
+
var alpha = safeProp(node, 'opacity', 1);
|
|
545
|
+
if (alpha === undefined) alpha = safeProp(node, 'alpha', 1);
|
|
546
|
+
|
|
547
|
+
candidates.push({
|
|
548
|
+
node: {
|
|
549
|
+
id: getNodeId(node, 0),
|
|
550
|
+
type: getType(node),
|
|
551
|
+
name: getName(node),
|
|
552
|
+
visible: active,
|
|
553
|
+
interactive: interactive,
|
|
554
|
+
alpha: alpha,
|
|
555
|
+
x: nx, y: ny,
|
|
556
|
+
width: w * Math.abs(sx2),
|
|
557
|
+
height: h * Math.abs(sy2),
|
|
558
|
+
worldBounds: wb,
|
|
559
|
+
path: path
|
|
560
|
+
},
|
|
561
|
+
depth: depth
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
var nodeChildren = getChildren(node);
|
|
566
|
+
for (var i = 0; i < nodeChildren.length; i++) {
|
|
567
|
+
var cn = nodeChildren[i];
|
|
568
|
+
if (!cn) continue;
|
|
569
|
+
var childPath = accPath ? accPath + '/' + getNodeId(cn, i) : getNodeId(cn, i);
|
|
570
|
+
hitTestDfs(cn, depth + 1, childPath);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (scene) hitTestDfs(scene, 0, 'cc.director.getScene()');
|
|
575
|
+
|
|
576
|
+
// Use engine pick if available, otherwise topmost DFS candidate
|
|
577
|
+
var picked = enginePicked;
|
|
578
|
+
var finalMethod = hitTestMethod;
|
|
579
|
+
|
|
580
|
+
if (!picked && candidates.length > 0) {
|
|
581
|
+
candidates.sort(function(a, b) { return a.depth - b.depth; });
|
|
582
|
+
picked = candidates[0].node;
|
|
583
|
+
finalMethod = 'manual';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
success: !!picked,
|
|
588
|
+
picked: picked,
|
|
589
|
+
candidates: candidates,
|
|
590
|
+
coordinates: {
|
|
591
|
+
screen: { x: sx, y: sy },
|
|
592
|
+
canvas: { x: canvasX, y: canvasY },
|
|
593
|
+
stage: { x: cocosX, y: cocosY }
|
|
594
|
+
},
|
|
595
|
+
hitTestMethod: finalMethod
|
|
596
|
+
};
|
|
597
|
+
})()`;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Cocos Creator canvas engine adapter.
|
|
601
|
+
*
|
|
602
|
+
* Handles both Cocos Creator 2.x and 3.x. Version is resolved from cc.ENGINE_VERSION
|
|
603
|
+
* at detect() time.
|
|
604
|
+
*/
|
|
605
|
+
var CocosCanvasAdapter = class {
|
|
606
|
+
id = "cocos";
|
|
607
|
+
engine = "CocosCreator";
|
|
608
|
+
version;
|
|
609
|
+
constructor() {
|
|
610
|
+
this.version = void 0;
|
|
611
|
+
}
|
|
612
|
+
async detect(env) {
|
|
613
|
+
try {
|
|
614
|
+
const result = await env.pageController.evaluate(`
|
|
615
|
+
(function() {
|
|
616
|
+
if (typeof window.cc === 'undefined' || window.cc === null) {
|
|
617
|
+
return { present: false, hasDirector: false };
|
|
618
|
+
}
|
|
619
|
+
var cocos = window.cc;
|
|
620
|
+
var hasDirector = !!(cocos.director);
|
|
621
|
+
var isV3 = !!(cocos.Scene);
|
|
622
|
+
var versionStr = cocos.ENGINE_VERSION || (isV3 ? '3.x' : '2.x');
|
|
623
|
+
var major = isV3 ? 3 : 2;
|
|
624
|
+
return {
|
|
625
|
+
present: true,
|
|
626
|
+
hasDirector: hasDirector,
|
|
627
|
+
version: versionStr,
|
|
628
|
+
versionMajor: major
|
|
629
|
+
};
|
|
630
|
+
})()
|
|
631
|
+
`);
|
|
632
|
+
if (!result.present || !result.hasDirector) return null;
|
|
633
|
+
const evidence = ["window.cc detected", "cc.director present"];
|
|
634
|
+
if (result.versionMajor === 2) evidence.push("Cocos Creator v2 API");
|
|
635
|
+
else if (result.versionMajor === 3) evidence.push("Cocos Creator v3 API");
|
|
636
|
+
return {
|
|
637
|
+
engine: this.engine,
|
|
638
|
+
version: result.version,
|
|
639
|
+
confidence: .95,
|
|
640
|
+
evidence,
|
|
641
|
+
adapterId: this.id
|
|
642
|
+
};
|
|
643
|
+
} catch {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async dumpScene(env, opts) {
|
|
648
|
+
const payload = buildCocosSceneTreeDumpPayload(opts);
|
|
649
|
+
const raw = await env.pageController.evaluate(payload);
|
|
650
|
+
return {
|
|
651
|
+
engine: raw.engine,
|
|
652
|
+
version: raw.version,
|
|
653
|
+
canvas: raw.canvas,
|
|
654
|
+
sceneTree: raw.sceneTree ?? {
|
|
655
|
+
id: "empty",
|
|
656
|
+
type: "Scene",
|
|
657
|
+
visible: true,
|
|
658
|
+
interactive: false,
|
|
659
|
+
alpha: 1,
|
|
660
|
+
x: 0,
|
|
661
|
+
y: 0,
|
|
662
|
+
width: raw.canvas?.width ?? 0,
|
|
663
|
+
height: raw.canvas?.height ?? 0,
|
|
664
|
+
worldBounds: {
|
|
665
|
+
x: 0,
|
|
666
|
+
y: 0,
|
|
667
|
+
width: raw.canvas?.width ?? 0,
|
|
668
|
+
height: raw.canvas?.height ?? 0
|
|
669
|
+
},
|
|
670
|
+
path: "cc.director.getScene()"
|
|
671
|
+
},
|
|
672
|
+
totalNodes: raw.totalNodes,
|
|
673
|
+
completeness: raw.completeness === "full" ? "full" : "partial"
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async pickAt(env, opts) {
|
|
677
|
+
const payload = buildCocosHitTestPayload(opts);
|
|
678
|
+
const result = await env.pageController.evaluate(payload);
|
|
679
|
+
return {
|
|
680
|
+
success: result.success,
|
|
681
|
+
picked: result.picked,
|
|
682
|
+
candidates: result.candidates,
|
|
683
|
+
coordinates: result.coordinates,
|
|
684
|
+
hitTestMethod: result.hitTestMethod
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/server/domains/canvas/adapters/laya-adapter.ts
|
|
690
|
+
/**
|
|
691
|
+
* Generates a self-contained JS string that traverses Laya.stage via DFS and
|
|
692
|
+
* returns a serialisable scene tree with worldBounds computed via localToGlobal().
|
|
693
|
+
*
|
|
694
|
+
* @param opts - Dump options (maxDepth, onlyInteractive, onlyVisible)
|
|
695
|
+
*/
|
|
696
|
+
function buildLayaSceneTreeDumpPayload(opts) {
|
|
697
|
+
const maxDepth = opts.maxDepth ?? 20;
|
|
698
|
+
const onlyInteractive = opts.onlyInteractive ?? false;
|
|
699
|
+
return `(function() {
|
|
700
|
+
function getChildren(node) {
|
|
701
|
+
if (!node) return [];
|
|
702
|
+
if (node.children && node.numChildren !== undefined) return node.children;
|
|
703
|
+
if (node._children) return node._children;
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function getNumChildren(node) {
|
|
708
|
+
if (!node) return 0;
|
|
709
|
+
if (node.numChildren !== undefined) return node.numChildren;
|
|
710
|
+
if (node._children) return node._children.length;
|
|
711
|
+
if (node.children) return Array.isArray(node.children) ? node.children.length : 0;
|
|
712
|
+
return 0;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function nodeId(node, idx) {
|
|
716
|
+
if (node.id !== undefined && node.id !== null && node.id !== '') return String(node.id);
|
|
717
|
+
return (node.constructor ? node.constructor.name : 'Node') + '_' + idx;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function safeProp(node, key, fallback) {
|
|
721
|
+
try { var v = node[key]; return v === undefined || v === null ? fallback : v; } catch(e) { return fallback; }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function localToGlobalRect(node) {
|
|
725
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
726
|
+
try {
|
|
727
|
+
var p0 = { x: 0, y: 0 };
|
|
728
|
+
var p1 = { x: safeProp(node, 'width', 0), y: safeProp(node, 'height', 0) };
|
|
729
|
+
var lp0 = node.localToGlobal ? node.localToGlobal(p0) : p0;
|
|
730
|
+
var lp1 = node.localToGlobal ? node.localToGlobal(p1) : p1;
|
|
731
|
+
var w = Math.abs(lp1.x - lp0.x) || safeProp(node, 'width', 0);
|
|
732
|
+
var h = Math.abs(lp1.y - lp0.y) || safeProp(node, 'height', 0);
|
|
733
|
+
return { x: lp0.x, y: lp0.y, width: w, height: h };
|
|
734
|
+
} catch(e) {
|
|
735
|
+
return { x: safeProp(node, 'x', 0), y: safeProp(node, 'y', 0),
|
|
736
|
+
width: safeProp(node, 'width', 0), height: safeProp(node, 'height', 0) };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
var totalNodes = 0;
|
|
741
|
+
|
|
742
|
+
function traverse(node, depth, path) {
|
|
743
|
+
if (!node || depth > ${maxDepth}) return null;
|
|
744
|
+
totalNodes++;
|
|
745
|
+
|
|
746
|
+
var interactive = !!(safeProp(node, 'mouseEnabled', true));
|
|
747
|
+
var visible = !!(safeProp(node, 'visible', true));
|
|
748
|
+
|
|
749
|
+
if (${opts.onlyVisible ?? false} && !visible) return null;
|
|
750
|
+
if (${onlyInteractive} && !interactive) return null;
|
|
751
|
+
|
|
752
|
+
var wb = localToGlobalRect(node);
|
|
753
|
+
var numC = getNumChildren(node);
|
|
754
|
+
var children = null;
|
|
755
|
+
|
|
756
|
+
if (numC > 0) {
|
|
757
|
+
children = [];
|
|
758
|
+
var nodeChildren = getChildren(node);
|
|
759
|
+
for (var i = 0; i < nodeChildren.length; i++) {
|
|
760
|
+
var cn = nodeChildren[i];
|
|
761
|
+
if (!cn) continue;
|
|
762
|
+
var childPath = path ? path + '/' + nodeId(cn, i) : nodeId(cn, i);
|
|
763
|
+
var sub = traverse(cn, depth + 1, childPath);
|
|
764
|
+
if (sub) children.push(sub);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
var result = {
|
|
769
|
+
id: nodeId(node, 0),
|
|
770
|
+
type: node.constructor ? node.constructor.name : 'Node',
|
|
771
|
+
name: safeProp(node, 'name', undefined),
|
|
772
|
+
visible: visible,
|
|
773
|
+
interactive: interactive,
|
|
774
|
+
mouseEnabled: safeProp(node, 'mouseEnabled', undefined),
|
|
775
|
+
alpha: safeProp(node, 'alpha', 1),
|
|
776
|
+
x: safeProp(node, 'x', 0),
|
|
777
|
+
y: safeProp(node, 'y', 0),
|
|
778
|
+
width: safeProp(node, 'width', 0),
|
|
779
|
+
height: safeProp(node, 'height', 0),
|
|
780
|
+
worldBounds: wb,
|
|
781
|
+
path: path || nodeId(node, 0),
|
|
782
|
+
customData: {
|
|
783
|
+
scaleX: safeProp(node, 'scaleX', 1),
|
|
784
|
+
scaleY: safeProp(node, 'scaleY', 1),
|
|
785
|
+
rotation: safeProp(node, 'rotation', 0),
|
|
786
|
+
pivotX: safeProp(node, 'pivotX', 0),
|
|
787
|
+
pivotY: safeProp(node, 'pivotY', 0),
|
|
788
|
+
mouseThrough: safeProp(node, 'mouseThrough', false),
|
|
789
|
+
hitArea: !!node.hitArea,
|
|
790
|
+
hitTestPrior: safeProp(node, 'hitTestPrior', undefined),
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
if (children && children.length > 0) result.children = children;
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (!window.Laya || !window.Laya.stage) {
|
|
799
|
+
return { engine: 'LayaAir', version: window.Laya ? window.Laya.version : undefined,
|
|
800
|
+
canvas: { width: 0, height: 0, dpr: 1, contextType: 'unknown' },
|
|
801
|
+
sceneTree: null, totalNodes: 0, completeness: 'partial',
|
|
802
|
+
error: 'Laya.stage not found' };
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
var stage = window.Laya.stage;
|
|
806
|
+
var layaVersion = window.Laya.version || '2.x';
|
|
807
|
+
var isLaya3 = !!(window.Laya.InputManager);
|
|
808
|
+
var scaleX = stage.clientScaleX !== undefined ? stage.clientScaleX : 1;
|
|
809
|
+
var scaleY = stage.clientScaleY !== undefined ? stage.clientScaleY : 1;
|
|
810
|
+
|
|
811
|
+
var canvasEl = document.querySelector('canvas');
|
|
812
|
+
var canvasInfo = { width: canvasEl ? canvasEl.width : safeProp(stage, 'width', 0),
|
|
813
|
+
height: canvasEl ? canvasEl.height : safeProp(stage, 'height', 0),
|
|
814
|
+
dpr: window.devicePixelRatio || 1,
|
|
815
|
+
contextType: 'unknown' };
|
|
816
|
+
if (canvasEl) {
|
|
817
|
+
var gl = canvasEl.getContext('webgl2') || canvasEl.getContext('webgl');
|
|
818
|
+
canvasInfo.contextType = gl ? (gl instanceof WebGL2RenderingContext ? 'webgl2' : 'webgl') : '2d';
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
var sceneTree = traverse(stage, 0, 'Laya.stage');
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
engine: 'LayaAir',
|
|
825
|
+
version: layaVersion,
|
|
826
|
+
canvas: canvasInfo,
|
|
827
|
+
sceneTree: sceneTree,
|
|
828
|
+
totalNodes: totalNodes,
|
|
829
|
+
completeness: 'full',
|
|
830
|
+
_meta: { isLaya3: isLaya3, scaleX: scaleX, scaleY: scaleY }
|
|
831
|
+
};
|
|
832
|
+
})()`;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Generates a self-contained JS string that:
|
|
836
|
+
* 1. Transforms screen coordinates → stage coordinates using clientScaleX/Y
|
|
837
|
+
* 2. Runs hit test via Stage.hitTest (3.x) or recursive DFS bounds check (2.x)
|
|
838
|
+
* 3. Returns all candidates sorted by depth (topmost first)
|
|
839
|
+
*
|
|
840
|
+
* @param opts - Pick options (x, y, canvasId)
|
|
841
|
+
*/
|
|
842
|
+
function buildLayaHitTestPayload(opts) {
|
|
843
|
+
const x = opts.x;
|
|
844
|
+
const y = opts.y;
|
|
845
|
+
const canvasId = opts.canvasId;
|
|
846
|
+
return `(function() {
|
|
847
|
+
function getChildren(node) {
|
|
848
|
+
if (!node) return [];
|
|
849
|
+
if (node.children && node.numChildren !== undefined) return node.children;
|
|
850
|
+
if (node._children) return node._children;
|
|
851
|
+
return [];
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function getNumChildren(node) {
|
|
855
|
+
if (!node) return 0;
|
|
856
|
+
if (node.numChildren !== undefined) return node.numChildren;
|
|
857
|
+
if (node._children) return node._children.length;
|
|
858
|
+
if (node.children) return Array.isArray(node.children) ? node.children.length : 0;
|
|
859
|
+
return 0;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function nodeId(node, idx) {
|
|
863
|
+
if (node.id !== undefined && node.id !== null && node.id !== '') return String(node.id);
|
|
864
|
+
return (node.constructor ? node.constructor.name : 'Node') + '_' + idx;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function safeProp(node, key, fallback) {
|
|
868
|
+
try { var v = node[key]; return v === undefined || v === null ? fallback : v; } catch(e) { return fallback; }
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function localToGlobalRect(node) {
|
|
872
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
873
|
+
try {
|
|
874
|
+
var p0 = { x: 0, y: 0 };
|
|
875
|
+
var p1 = { x: safeProp(node, 'width', 0), y: safeProp(node, 'height', 0) };
|
|
876
|
+
var lp0 = node.localToGlobal ? node.localToGlobal(p0) : p0;
|
|
877
|
+
var lp1 = node.localToGlobal ? node.localToGlobal(p1) : p1;
|
|
878
|
+
var w = Math.abs(lp1.x - lp0.x) || safeProp(node, 'width', 0);
|
|
879
|
+
var h = Math.abs(lp1.y - lp0.y) || safeProp(node, 'height', 0);
|
|
880
|
+
return { x: lp0.x, y: lp0.y, width: w, height: h };
|
|
881
|
+
} catch(e) {
|
|
882
|
+
return { x: safeProp(node, 'x', 0), y: safeProp(node, 'y', 0),
|
|
883
|
+
width: safeProp(node, 'width', 0), height: safeProp(node, 'height', 0) };
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function nodePath(node) {
|
|
888
|
+
var parts = [];
|
|
889
|
+
var cur = node;
|
|
890
|
+
while (cur && cur !== window.Laya.stage) {
|
|
891
|
+
var name = cur.name || nodeId(cur, 0);
|
|
892
|
+
parts.unshift(name);
|
|
893
|
+
cur = cur.parent;
|
|
894
|
+
}
|
|
895
|
+
parts.unshift('Laya.stage');
|
|
896
|
+
return parts.join('/');
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
var sx = ${x}, sy = ${y};
|
|
900
|
+
|
|
901
|
+
// Find the target canvas
|
|
902
|
+
var canvases = Array.from(document.querySelectorAll('canvas'));
|
|
903
|
+
var targetCanvas = null;
|
|
904
|
+
${canvasId ? `targetCanvas = document.getElementById(${JSON.stringify(canvasId)}) || canvases[parseInt(${JSON.stringify(canvasId)})] || null;` : `
|
|
905
|
+
for (var ci = canvases.length - 1; ci >= 0; ci--) {
|
|
906
|
+
var r = canvases[ci].getBoundingClientRect();
|
|
907
|
+
if (sx >= r.left && sx <= r.right && sy >= r.top && sy <= r.bottom) {
|
|
908
|
+
targetCanvas = canvases[ci];
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
}`}
|
|
912
|
+
|
|
913
|
+
if (!window.Laya || !window.Laya.stage) {
|
|
914
|
+
return { success: false, picked: null, candidates: [], coordinates: {
|
|
915
|
+
screen: { x: sx, y: sy }, canvas: { x: 0, y: 0 } }, hitTestMethod: 'none' };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
var stage = window.Laya.stage;
|
|
919
|
+
var isLaya3 = !!(window.Laya.InputManager);
|
|
920
|
+
var scaleX = stage.clientScaleX !== undefined ? stage.clientScaleX : 1;
|
|
921
|
+
var scaleY = stage.clientScaleY !== undefined ? stage.clientScaleY : 1;
|
|
922
|
+
|
|
923
|
+
// Screen → canvas
|
|
924
|
+
var canvasX = sx, canvasY = sy;
|
|
925
|
+
if (targetCanvas) {
|
|
926
|
+
var rect = targetCanvas.getBoundingClientRect();
|
|
927
|
+
canvasX = (sx - rect.left) * (targetCanvas.width / rect.width);
|
|
928
|
+
canvasY = (sy - rect.top) * (targetCanvas.height / rect.height);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Canvas → stage: use mouseX/mouseY when available (set by Laya's event system)
|
|
932
|
+
var stageX = safeProp(stage, 'mouseX', canvasX / (scaleX || 1));
|
|
933
|
+
var stageY = safeProp(stage, 'mouseY', canvasY / (scaleY || 1));
|
|
934
|
+
|
|
935
|
+
var candidates = [];
|
|
936
|
+
|
|
937
|
+
// Try engine-native hitTest first (3.x)
|
|
938
|
+
var hitTestMethod = 'none';
|
|
939
|
+
var enginePicked = null;
|
|
940
|
+
|
|
941
|
+
if (isLaya3 && typeof stage.hitTest === 'function') {
|
|
942
|
+
try {
|
|
943
|
+
var nativeHit = stage.hitTest({ x: stageX, y: stageY });
|
|
944
|
+
if (nativeHit) {
|
|
945
|
+
enginePicked = nativeHit;
|
|
946
|
+
hitTestMethod = 'engine';
|
|
947
|
+
}
|
|
948
|
+
} catch(e) {}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Recursive DFS hit test (always available; 2.x fallback for 3.x too)
|
|
952
|
+
function hitTestDfs(node, depth, accPath) {
|
|
953
|
+
if (!node || !safeProp(node, 'visible', true)) return;
|
|
954
|
+
|
|
955
|
+
var wb = localToGlobalRect(node);
|
|
956
|
+
var mx = sx, my = sy;
|
|
957
|
+
|
|
958
|
+
// Convert screen → node local using parent chain
|
|
959
|
+
var cur = node;
|
|
960
|
+
var screenPt = { x: sx, y: sy };
|
|
961
|
+
while (cur) {
|
|
962
|
+
if (cur.globalToLocal) {
|
|
963
|
+
try { screenPt = cur.globalToLocal(screenPt); } catch(e) { break; }
|
|
964
|
+
}
|
|
965
|
+
cur = cur.parent;
|
|
966
|
+
}
|
|
967
|
+
var lx = screenPt.x, ly = screenPt.y;
|
|
968
|
+
|
|
969
|
+
// Check bounds against node's local coordinate frame
|
|
970
|
+
var nw = safeProp(node, 'width', 0) || (wb.width / (safeProp(node, 'scaleX', 1) || 1));
|
|
971
|
+
var nh = safeProp(node, 'height', 0) || (wb.height / (safeProp(node, 'scaleY', 1) || 1));
|
|
972
|
+
var nx = safeProp(node, 'x', 0), ny = safeProp(node, 'y', 0);
|
|
973
|
+
var pivotX = safeProp(node, 'pivotX', 0), pivotY = safeProp(node, 'pivotY', 0);
|
|
974
|
+
|
|
975
|
+
var inBounds = lx >= nx - pivotX && lx <= nx - pivotX + nw &&
|
|
976
|
+
ly >= ny - pivotY && ly <= ny - pivotY + nh;
|
|
977
|
+
|
|
978
|
+
var interactive = !!(safeProp(node, 'mouseEnabled', true));
|
|
979
|
+
|
|
980
|
+
if (inBounds && interactive) {
|
|
981
|
+
var path = accPath ? accPath + '/' + nodeId(node, 0) : nodeId(node, 0);
|
|
982
|
+
candidates.push({
|
|
983
|
+
node: {
|
|
984
|
+
id: nodeId(node, 0),
|
|
985
|
+
type: node.constructor ? node.constructor.name : 'Node',
|
|
986
|
+
name: safeProp(node, 'name', undefined),
|
|
987
|
+
visible: !!(safeProp(node, 'visible', true)),
|
|
988
|
+
interactive: interactive,
|
|
989
|
+
mouseEnabled: safeProp(node, 'mouseEnabled', undefined),
|
|
990
|
+
alpha: safeProp(node, 'alpha', 1),
|
|
991
|
+
x: nx, y: ny,
|
|
992
|
+
width: nw, height: nh,
|
|
993
|
+
worldBounds: wb,
|
|
994
|
+
path: path
|
|
995
|
+
},
|
|
996
|
+
depth: depth
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
var nodeChildren = getChildren(node);
|
|
1001
|
+
for (var i = 0; i < nodeChildren.length; i++) {
|
|
1002
|
+
var cn = nodeChildren[i];
|
|
1003
|
+
if (!cn) continue;
|
|
1004
|
+
var childPath = accPath ? accPath + '/' + nodeId(cn, i) : nodeId(cn, i);
|
|
1005
|
+
hitTestDfs(cn, depth + 1, childPath);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
hitTestDfs(stage, 0, 'Laya.stage');
|
|
1010
|
+
|
|
1011
|
+
// Use engine pick if available, otherwise topmost DFS candidate
|
|
1012
|
+
var picked = enginePicked;
|
|
1013
|
+
var finalMethod = hitTestMethod;
|
|
1014
|
+
|
|
1015
|
+
if (!picked && candidates.length > 0) {
|
|
1016
|
+
// Sort by depth ascending (deepest/nested first = topmost)
|
|
1017
|
+
candidates.sort(function(a, b) { return a.depth - b.depth; });
|
|
1018
|
+
picked = candidates[0].node;
|
|
1019
|
+
finalMethod = 'manual';
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return {
|
|
1023
|
+
success: !!picked,
|
|
1024
|
+
picked: picked,
|
|
1025
|
+
candidates: candidates,
|
|
1026
|
+
coordinates: {
|
|
1027
|
+
screen: { x: sx, y: sy },
|
|
1028
|
+
canvas: { x: canvasX, y: canvasY },
|
|
1029
|
+
stage: { x: stageX, y: stageY }
|
|
1030
|
+
},
|
|
1031
|
+
hitTestMethod: finalMethod
|
|
1032
|
+
};
|
|
1033
|
+
})()`;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* LayaAir canvas engine adapter.
|
|
1037
|
+
*
|
|
1038
|
+
* Handles both LayaAir 2.x and 3.x. Version is resolved lazily from window.Laya.version
|
|
1039
|
+
* at first detect() call.
|
|
1040
|
+
*/
|
|
1041
|
+
var LayaCanvasAdapter = class {
|
|
1042
|
+
id = "laya";
|
|
1043
|
+
engine = "LayaAir";
|
|
1044
|
+
version;
|
|
1045
|
+
constructor() {
|
|
1046
|
+
this.version = void 0;
|
|
1047
|
+
}
|
|
1048
|
+
async detect(env) {
|
|
1049
|
+
try {
|
|
1050
|
+
const result = await env.pageController.evaluate(`
|
|
1051
|
+
(function() {
|
|
1052
|
+
if (typeof window.Laya === 'undefined' || window.Laya === null) {
|
|
1053
|
+
return { present: false, hasStage: false, laya2: false, laya3: false };
|
|
1054
|
+
}
|
|
1055
|
+
var laya = window.Laya;
|
|
1056
|
+
var hasStage = !!(laya.stage);
|
|
1057
|
+
var laya2 = !!(laya.MouseManager);
|
|
1058
|
+
var laya3 = !!(laya.InputManager);
|
|
1059
|
+
var version = laya.version || (laya2 ? '2.x' : laya3 ? '3.x' : undefined);
|
|
1060
|
+
return { present: true, hasStage: hasStage, version: version,
|
|
1061
|
+
laya2: laya2, laya3: laya3 };
|
|
1062
|
+
})()
|
|
1063
|
+
`);
|
|
1064
|
+
if (!result.present || !result.hasStage) return null;
|
|
1065
|
+
const evidence = ["window.Laya is defined"];
|
|
1066
|
+
if (result.laya2) evidence.push("Laya.MouseManager detected (LayaAir 2.x)");
|
|
1067
|
+
if (result.laya3) evidence.push("Laya.InputManager detected (LayaAir 3.x)");
|
|
1068
|
+
evidence.push("Laya.stage is present");
|
|
1069
|
+
return {
|
|
1070
|
+
engine: this.engine,
|
|
1071
|
+
version: result.version,
|
|
1072
|
+
confidence: .95,
|
|
1073
|
+
evidence,
|
|
1074
|
+
adapterId: this.id
|
|
1075
|
+
};
|
|
1076
|
+
} catch {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async dumpScene(env, opts) {
|
|
1081
|
+
const payload = buildLayaSceneTreeDumpPayload(opts);
|
|
1082
|
+
const raw = await env.pageController.evaluate(payload);
|
|
1083
|
+
return {
|
|
1084
|
+
engine: raw.engine,
|
|
1085
|
+
version: raw.version,
|
|
1086
|
+
canvas: raw.canvas,
|
|
1087
|
+
sceneTree: raw.sceneTree ?? {
|
|
1088
|
+
id: "empty",
|
|
1089
|
+
type: "Stage",
|
|
1090
|
+
visible: true,
|
|
1091
|
+
interactive: false,
|
|
1092
|
+
alpha: 1,
|
|
1093
|
+
x: 0,
|
|
1094
|
+
y: 0,
|
|
1095
|
+
width: raw.canvas?.width ?? 0,
|
|
1096
|
+
height: raw.canvas?.height ?? 0,
|
|
1097
|
+
worldBounds: {
|
|
1098
|
+
x: 0,
|
|
1099
|
+
y: 0,
|
|
1100
|
+
width: raw.canvas?.width ?? 0,
|
|
1101
|
+
height: raw.canvas?.height ?? 0
|
|
1102
|
+
},
|
|
1103
|
+
path: "Laya.stage"
|
|
1104
|
+
},
|
|
1105
|
+
totalNodes: raw.totalNodes,
|
|
1106
|
+
completeness: raw.completeness === "full" ? "full" : "partial"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
async pickAt(env, opts) {
|
|
1110
|
+
const payload = buildLayaHitTestPayload(opts);
|
|
1111
|
+
const result = await env.pageController.evaluate(payload);
|
|
1112
|
+
return {
|
|
1113
|
+
success: result.success,
|
|
1114
|
+
picked: result.picked,
|
|
1115
|
+
candidates: result.candidates,
|
|
1116
|
+
coordinates: result.coordinates,
|
|
1117
|
+
hitTestMethod: result.hitTestMethod
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
async traceClick(env, opts, services) {
|
|
1121
|
+
const { debuggerManager, traceRecorder, evidenceStore } = services;
|
|
1122
|
+
let traceId = "no-recording";
|
|
1123
|
+
try {
|
|
1124
|
+
traceId = (await traceRecorder.start(null, null, { recordMemoryDeltas: false })).sessionId;
|
|
1125
|
+
} catch {}
|
|
1126
|
+
await debuggerManager.enable();
|
|
1127
|
+
await debuggerManager.ensureAdvancedFeatures();
|
|
1128
|
+
const patchResult = await env.pageController.evaluate(`
|
|
1129
|
+
(function() {
|
|
1130
|
+
var k = Symbol.for('__laya_event_patched');
|
|
1131
|
+
var instrumented = [];
|
|
1132
|
+
function patchTarget(proto) {
|
|
1133
|
+
if (!proto || !proto.event || proto[k]) return;
|
|
1134
|
+
var original = proto.event;
|
|
1135
|
+
if (typeof original !== 'function') return;
|
|
1136
|
+
Object.defineProperty(proto, 'event', {
|
|
1137
|
+
value: function(type, data) {
|
|
1138
|
+
if (!this[k]) this[k] = new Set();
|
|
1139
|
+
if (this[k].has(type)) return; // idempotent
|
|
1140
|
+
this[k].add(type);
|
|
1141
|
+
return original.call(this, type, data);
|
|
1142
|
+
},
|
|
1143
|
+
configurable: true,
|
|
1144
|
+
enumerable: false
|
|
1145
|
+
});
|
|
1146
|
+
instrumented.push(proto.constructor ? proto.constructor.name : 'EventDispatcher');
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Walk prototype chain
|
|
1150
|
+
var seen = new Set();
|
|
1151
|
+
var queue = [window.Laya && window.Laya.EventDispatcher ? window.Laya.EventDispatcher.prototype : null];
|
|
1152
|
+
while (queue.length) {
|
|
1153
|
+
var p = queue.shift();
|
|
1154
|
+
if (!p || seen.has(p)) continue;
|
|
1155
|
+
seen.add(p);
|
|
1156
|
+
patchTarget(p);
|
|
1157
|
+
if (Object.getPrototypeOf) {
|
|
1158
|
+
var mp = Object.getPrototypeOf(p);
|
|
1159
|
+
if (mp) queue.push(mp);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return { success: true, instrumented: instrumented };
|
|
1164
|
+
})()
|
|
1165
|
+
`);
|
|
1166
|
+
const breakpointType = opts.breakpointType ?? "click";
|
|
1167
|
+
await debuggerManager.getEventManager().setEventListenerBreakpoint(breakpointType);
|
|
1168
|
+
const domEventChain = await env.pageController.evaluate(`
|
|
1169
|
+
(function() {
|
|
1170
|
+
var ev = new PointerEvent(${JSON.stringify(breakpointType)}, {
|
|
1171
|
+
view: window, bubbles: true, cancelable: true,
|
|
1172
|
+
clientX: ${opts.targetNodeId ?? 0}, clientY: 0, pointerId: 1
|
|
1173
|
+
});
|
|
1174
|
+
var events = [];
|
|
1175
|
+
var el = document.elementFromPoint(0, 0);
|
|
1176
|
+
if (el) el.dispatchEvent(ev);
|
|
1177
|
+
events.push(${JSON.stringify(breakpointType)});
|
|
1178
|
+
return events;
|
|
1179
|
+
})()
|
|
1180
|
+
`);
|
|
1181
|
+
const pausedState = await debuggerManager.waitForPaused(5e3);
|
|
1182
|
+
const handlerFrames = pausedState?.callFrames ? pausedState.callFrames.slice(0, opts.maxFrames ?? 50).map((f) => ({
|
|
1183
|
+
functionName: f.functionName || "(anonymous)",
|
|
1184
|
+
scriptUrl: f.url,
|
|
1185
|
+
lineNumber: f.location?.lineNumber,
|
|
1186
|
+
columnNumber: f.location?.columnNumber
|
|
1187
|
+
})) : [];
|
|
1188
|
+
await debuggerManager.resume();
|
|
1189
|
+
evidenceStore.addNode("function", `laya-click-trace:${traceId}`, {
|
|
1190
|
+
engine: this.engine,
|
|
1191
|
+
version: this.version,
|
|
1192
|
+
handlersFound: handlerFrames.length,
|
|
1193
|
+
instrumentedPrototypes: patchResult?.instrumented ?? []
|
|
1194
|
+
});
|
|
1195
|
+
try {
|
|
1196
|
+
traceRecorder.stop();
|
|
1197
|
+
} catch {}
|
|
1198
|
+
return {
|
|
1199
|
+
inputFlow: domEventChain,
|
|
1200
|
+
hitTarget: null,
|
|
1201
|
+
domEventChain: handlerFrames.map((f) => ({
|
|
1202
|
+
type: breakpointType,
|
|
1203
|
+
target: f.scriptUrl,
|
|
1204
|
+
phase: "at-target"
|
|
1205
|
+
})),
|
|
1206
|
+
engineDispatchChain: patchResult?.instrumented ?? [],
|
|
1207
|
+
handlerFrames,
|
|
1208
|
+
handlersTriggered: handlerFrames.map((f) => ({
|
|
1209
|
+
functionName: f.functionName,
|
|
1210
|
+
scriptUrl: f.scriptUrl,
|
|
1211
|
+
lineNumber: f.lineNumber
|
|
1212
|
+
})),
|
|
1213
|
+
networkEmitted: []
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
//#endregion
|
|
1218
|
+
//#region src/server/domains/canvas/adapters/phaser-adapter.ts
|
|
1219
|
+
/**
|
|
1220
|
+
* Generates a self-contained JS string that traverses Phaser scenes via DFS and
|
|
1221
|
+
* returns a serialisable scene tree with worldBounds computed via getBounds().
|
|
1222
|
+
*
|
|
1223
|
+
* @param opts - Dump options (maxDepth, onlyInteractive, onlyVisible)
|
|
1224
|
+
*/
|
|
1225
|
+
function buildPhaserSceneTreeDumpPayload(opts) {
|
|
1226
|
+
const maxDepth = opts.maxDepth ?? 20;
|
|
1227
|
+
const onlyInteractive = opts.onlyInteractive ?? false;
|
|
1228
|
+
return `(function() {
|
|
1229
|
+
function getChildren(node) {
|
|
1230
|
+
if (!node) return [];
|
|
1231
|
+
if (node.list && Array.isArray(node.list)) return node.list;
|
|
1232
|
+
if (node.children && Array.isArray(node.children)) return node.children;
|
|
1233
|
+
return [];
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function getNumChildren(node) {
|
|
1237
|
+
if (!node) return 0;
|
|
1238
|
+
if (node.list) return node.list.length;
|
|
1239
|
+
if (node.numChildren !== undefined) return node.numChildren;
|
|
1240
|
+
if (node.children && Array.isArray(node.children)) return node.children.length;
|
|
1241
|
+
return 0;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function nodeId(node, idx) {
|
|
1245
|
+
if (node.id !== undefined && node.id !== null && node.id !== '') return String(node.id);
|
|
1246
|
+
return (node.constructor ? node.constructor.name : 'DisplayObject') + '_' + idx;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function safeProp(node, key, fallback) {
|
|
1250
|
+
try { var v = node[key]; return v === undefined || v === null ? fallback : v; } catch(e) { return fallback; }
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function nodeBounds(node) {
|
|
1254
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1255
|
+
try {
|
|
1256
|
+
if (typeof node.getBounds === 'function') {
|
|
1257
|
+
var b = node.getBounds();
|
|
1258
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
1259
|
+
}
|
|
1260
|
+
} catch(e) {}
|
|
1261
|
+
return {
|
|
1262
|
+
x: safeProp(node, 'x', 0),
|
|
1263
|
+
y: safeProp(node, 'y', 0),
|
|
1264
|
+
width: safeProp(node, 'width', 0) || safeProp(node, 'displayWidth', 0),
|
|
1265
|
+
height: safeProp(node, 'height', 0) || safeProp(node, 'displayHeight', 0),
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
var totalNodes = 0;
|
|
1270
|
+
|
|
1271
|
+
function traverse(node, depth, path) {
|
|
1272
|
+
if (!node || depth > ${maxDepth}) return null;
|
|
1273
|
+
totalNodes++;
|
|
1274
|
+
|
|
1275
|
+
var interactive = !!(node.input && node.input.enabled);
|
|
1276
|
+
var visible = !!(safeProp(node, 'visible', true));
|
|
1277
|
+
|
|
1278
|
+
if (${opts.onlyVisible ?? false} && !visible) return null;
|
|
1279
|
+
if (${onlyInteractive} && !interactive) return null;
|
|
1280
|
+
|
|
1281
|
+
var wb = nodeBounds(node);
|
|
1282
|
+
var numC = getNumChildren(node);
|
|
1283
|
+
var children = null;
|
|
1284
|
+
|
|
1285
|
+
if (numC > 0) {
|
|
1286
|
+
children = [];
|
|
1287
|
+
var nodeChildren = getChildren(node);
|
|
1288
|
+
for (var i = 0; i < nodeChildren.length; i++) {
|
|
1289
|
+
var cn = nodeChildren[i];
|
|
1290
|
+
if (!cn) continue;
|
|
1291
|
+
var childPath = path ? path + '/' + nodeId(cn, i) : nodeId(cn, i);
|
|
1292
|
+
var sub = traverse(cn, depth + 1, childPath);
|
|
1293
|
+
if (sub) children.push(sub);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
var result = {
|
|
1298
|
+
id: nodeId(node, 0),
|
|
1299
|
+
type: node.constructor ? node.constructor.name : 'DisplayObject',
|
|
1300
|
+
name: safeProp(node, 'name', undefined),
|
|
1301
|
+
visible: visible,
|
|
1302
|
+
interactive: interactive,
|
|
1303
|
+
alpha: safeProp(node, 'alpha', 1),
|
|
1304
|
+
x: safeProp(node, 'x', 0),
|
|
1305
|
+
y: safeProp(node, 'y', 0),
|
|
1306
|
+
width: safeProp(node, 'width', 0) || safeProp(node, 'displayWidth', 0),
|
|
1307
|
+
height: safeProp(node, 'height', 0) || safeProp(node, 'displayHeight', 0),
|
|
1308
|
+
worldBounds: wb,
|
|
1309
|
+
path: path || nodeId(node, 0),
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
if (children && children.length > 0) result.children = children;
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (typeof window.Phaser === 'undefined' || !window.Phaser.GAMES || window.Phaser.GAMES.length === 0) {
|
|
1317
|
+
return { engine: 'Phaser',
|
|
1318
|
+
version: typeof window.Phaser !== 'undefined' ? window.Phaser.VERSION : undefined,
|
|
1319
|
+
canvas: { width: 0, height: 0, dpr: 1, contextType: 'unknown' },
|
|
1320
|
+
sceneTree: null, totalNodes: 0, completeness: 'partial',
|
|
1321
|
+
error: 'Phaser.GAMES has no entries' };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
var game = window.Phaser.GAMES[0];
|
|
1325
|
+
if (!game || !game.scene || !game.canvas) {
|
|
1326
|
+
return { engine: 'Phaser',
|
|
1327
|
+
version: window.Phaser.VERSION || undefined,
|
|
1328
|
+
canvas: { width: 0, height: 0, dpr: 1, contextType: 'unknown' },
|
|
1329
|
+
sceneTree: null, totalNodes: 0, completeness: 'partial',
|
|
1330
|
+
error: 'Game or canvas not accessible' };
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
var canvasEl = game.canvas;
|
|
1334
|
+
var dpr = window.devicePixelRatio || 1;
|
|
1335
|
+
var gl = null;
|
|
1336
|
+
try { gl = canvasEl.getContext('webgl2') || canvasEl.getContext('webgl'); } catch(e) {}
|
|
1337
|
+
var contextType = '2d';
|
|
1338
|
+
if (gl) contextType = gl instanceof WebGL2RenderingContext ? 'webgl2' : 'webgl';
|
|
1339
|
+
|
|
1340
|
+
var canvasInfo = {
|
|
1341
|
+
width: canvasEl.width || safeProp(game.scale, 'width', 0),
|
|
1342
|
+
height: canvasEl.height || safeProp(game.scale, 'height', 0),
|
|
1343
|
+
dpr: dpr,
|
|
1344
|
+
contextType: contextType,
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
var sceneTree = null;
|
|
1348
|
+
var scenes = game.scene ? game.scene.scenes : [];
|
|
1349
|
+
|
|
1350
|
+
if (scenes && scenes.length > 0) {
|
|
1351
|
+
for (var si = 0; si < scenes.length; si++) {
|
|
1352
|
+
var sc = scenes[si];
|
|
1353
|
+
if (!sc || !sc.sys) continue;
|
|
1354
|
+
var status = safeProp(sc.sys, 'settings', {}).status;
|
|
1355
|
+
// Only process active scenes (Phaser 3.x status: 0=init,1=start,2=loading,3=created,4=shutdown)
|
|
1356
|
+
if (status !== undefined && status !== 4) {
|
|
1357
|
+
var displayList = sc.sys && sc.sys.displayList;
|
|
1358
|
+
if (displayList) {
|
|
1359
|
+
var scenePath = 'Phaser.Game.scenes[' + si + ']';
|
|
1360
|
+
sceneTree = traverse(displayList, 0, scenePath);
|
|
1361
|
+
}
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return {
|
|
1368
|
+
engine: 'Phaser',
|
|
1369
|
+
version: window.Phaser.VERSION || undefined,
|
|
1370
|
+
canvas: canvasInfo,
|
|
1371
|
+
sceneTree: sceneTree,
|
|
1372
|
+
totalNodes: totalNodes,
|
|
1373
|
+
completeness: sceneTree ? 'full' : 'partial',
|
|
1374
|
+
};
|
|
1375
|
+
})()`;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Generates a self-contained JS string that:
|
|
1379
|
+
* 1. Transforms screen coordinates to canvas coordinates
|
|
1380
|
+
* 2. Runs hit test via scene.input.hitTestPointer() (Phaser's native method)
|
|
1381
|
+
* 3. Falls back to recursive bounds check using getBounds() / getTopLeft/getBottomRight
|
|
1382
|
+
* 4. Returns all candidates sorted by depth (topmost first)
|
|
1383
|
+
*
|
|
1384
|
+
* @param opts - Pick options (x, y, canvasId)
|
|
1385
|
+
*/
|
|
1386
|
+
function buildPhaserHitTestPayload(opts) {
|
|
1387
|
+
const x = opts.x;
|
|
1388
|
+
const y = opts.y;
|
|
1389
|
+
const canvasId = opts.canvasId;
|
|
1390
|
+
return `(function() {
|
|
1391
|
+
function getChildren(node) {
|
|
1392
|
+
if (!node) return [];
|
|
1393
|
+
if (node.list && Array.isArray(node.list)) return node.list;
|
|
1394
|
+
if (node.children && Array.isArray(node.children)) return node.children;
|
|
1395
|
+
return [];
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function getNumChildren(node) {
|
|
1399
|
+
if (!node) return 0;
|
|
1400
|
+
if (node.list) return node.list.length;
|
|
1401
|
+
if (node.numChildren !== undefined) return node.numChildren;
|
|
1402
|
+
if (node.children && Array.isArray(node.children)) return node.children.length;
|
|
1403
|
+
return 0;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function nodeId(node, idx) {
|
|
1407
|
+
if (node.id !== undefined && node.id !== null && node.id !== '') return String(node.id);
|
|
1408
|
+
return (node.constructor ? node.constructor.name : 'DisplayObject') + '_' + idx;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function safeProp(node, key, fallback) {
|
|
1412
|
+
try { var v = node[key]; return v === undefined || v === null ? fallback : v; } catch(e) { return fallback; }
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function nodeBounds(node) {
|
|
1416
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1417
|
+
try {
|
|
1418
|
+
if (typeof node.getBounds === 'function') {
|
|
1419
|
+
var b = node.getBounds();
|
|
1420
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
1421
|
+
}
|
|
1422
|
+
} catch(e) {}
|
|
1423
|
+
return {
|
|
1424
|
+
x: safeProp(node, 'x', 0),
|
|
1425
|
+
y: safeProp(node, 'y', 0),
|
|
1426
|
+
width: safeProp(node, 'width', 0) || safeProp(node, 'displayWidth', 0),
|
|
1427
|
+
height: safeProp(node, 'height', 0) || safeProp(node, 'displayHeight', 0),
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function nodePath(node, sceneIdx) {
|
|
1432
|
+
var parts = [];
|
|
1433
|
+
var cur = node;
|
|
1434
|
+
while (cur) {
|
|
1435
|
+
var name = cur.name || nodeId(cur, 0);
|
|
1436
|
+
parts.unshift(name);
|
|
1437
|
+
cur = cur.parent;
|
|
1438
|
+
}
|
|
1439
|
+
return 'Phaser.Game.scenes[' + sceneIdx + ']/' + parts.join('/');
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
var sx = ${x}, sy = ${y};
|
|
1443
|
+
|
|
1444
|
+
// Find the target canvas
|
|
1445
|
+
var canvases = Array.from(document.querySelectorAll('canvas'));
|
|
1446
|
+
var targetCanvas = null;
|
|
1447
|
+
${canvasId ? `targetCanvas = document.getElementById(${JSON.stringify(canvasId)}) || canvases[parseInt(${JSON.stringify(canvasId)})] || null;` : `
|
|
1448
|
+
for (var ci = canvases.length - 1; ci >= 0; ci--) {
|
|
1449
|
+
var r = canvases[ci].getBoundingClientRect();
|
|
1450
|
+
if (sx >= r.left && sx <= r.right && sy >= r.top && sy <= r.bottom) {
|
|
1451
|
+
targetCanvas = canvases[ci];
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
}`}
|
|
1455
|
+
|
|
1456
|
+
if (typeof window.Phaser === 'undefined' || !window.Phaser.GAMES || window.Phaser.GAMES.length === 0) {
|
|
1457
|
+
return { success: false, picked: null, candidates: [], coordinates: {
|
|
1458
|
+
screen: { x: sx, y: sy }, canvas: { x: 0, y: 0 } }, hitTestMethod: 'none' };
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
var game = window.Phaser.GAMES[0];
|
|
1462
|
+
var canvasX = sx, canvasY = sy;
|
|
1463
|
+
if (targetCanvas) {
|
|
1464
|
+
var rect = targetCanvas.getBoundingClientRect();
|
|
1465
|
+
canvasX = (sx - rect.left) * (targetCanvas.width / rect.width);
|
|
1466
|
+
canvasY = (sy - rect.top) * (targetCanvas.height / rect.height);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
var candidates = [];
|
|
1470
|
+
var hitTestMethod = 'none';
|
|
1471
|
+
var enginePicked = null;
|
|
1472
|
+
var sceneIdx = 0;
|
|
1473
|
+
|
|
1474
|
+
// Try engine-native hit test via scene.input.hitTestPointer
|
|
1475
|
+
if (game && game.scene && game.scene.scenes) {
|
|
1476
|
+
var scenes = game.scene.scenes;
|
|
1477
|
+
for (var si = 0; si < scenes.length; si++) {
|
|
1478
|
+
var sc = scenes[si];
|
|
1479
|
+
if (!sc || !sc.sys) continue;
|
|
1480
|
+
var status = safeProp(sc.sys, 'settings', {}).status;
|
|
1481
|
+
if (status !== undefined && status !== 4) {
|
|
1482
|
+
// Scene is active
|
|
1483
|
+
if (sc.input && typeof sc.input.hitTestPointer === 'function') {
|
|
1484
|
+
try {
|
|
1485
|
+
var nativeHit = sc.input.hitTestPointer({ x: canvasX, y: canvasY });
|
|
1486
|
+
if (nativeHit && nativeHit.length > 0) {
|
|
1487
|
+
enginePicked = nativeHit[0];
|
|
1488
|
+
hitTestMethod = 'engine';
|
|
1489
|
+
sceneIdx = si;
|
|
1490
|
+
}
|
|
1491
|
+
} catch(e) {}
|
|
1492
|
+
}
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Recursive DFS fallback bounds check
|
|
1499
|
+
function hitTestDfs(node, depth, accPath) {
|
|
1500
|
+
if (!node || !safeProp(node, 'visible', true)) return;
|
|
1501
|
+
|
|
1502
|
+
var wb = nodeBounds(node);
|
|
1503
|
+
|
|
1504
|
+
// Convert screen → node local using parent chain
|
|
1505
|
+
var cur = node;
|
|
1506
|
+
var screenPt = { x: sx, y: sy };
|
|
1507
|
+
while (cur) {
|
|
1508
|
+
if (cur.globalToLocal) {
|
|
1509
|
+
try { screenPt = cur.globalToLocal(screenPt); } catch(e) { break; }
|
|
1510
|
+
}
|
|
1511
|
+
cur = cur.parent;
|
|
1512
|
+
}
|
|
1513
|
+
var lx = screenPt.x, ly = screenPt.y;
|
|
1514
|
+
|
|
1515
|
+
var nw = safeProp(node, 'width', 0) || safeProp(node, 'displayWidth', 0) || wb.width;
|
|
1516
|
+
var nh = safeProp(node, 'height', 0) || safeProp(node, 'displayHeight', 0) || wb.height;
|
|
1517
|
+
var nx = safeProp(node, 'x', 0), ny = safeProp(node, 'y', 0);
|
|
1518
|
+
var pivotX = safeProp(node, 'pivotX', 0), pivotY = safeProp(node, 'pivotY', 0);
|
|
1519
|
+
|
|
1520
|
+
var inBounds = lx >= nx - pivotX && lx <= nx - pivotX + nw &&
|
|
1521
|
+
ly >= ny - pivotY && ly <= ny - pivotY + nh;
|
|
1522
|
+
|
|
1523
|
+
var interactive = !!(node.input && node.input.enabled);
|
|
1524
|
+
|
|
1525
|
+
if (inBounds && interactive) {
|
|
1526
|
+
var path = accPath || nodePath(node, sceneIdx);
|
|
1527
|
+
candidates.push({
|
|
1528
|
+
node: {
|
|
1529
|
+
id: nodeId(node, 0),
|
|
1530
|
+
type: node.constructor ? node.constructor.name : 'DisplayObject',
|
|
1531
|
+
name: safeProp(node, 'name', undefined),
|
|
1532
|
+
visible: !!(safeProp(node, 'visible', true)),
|
|
1533
|
+
interactive: interactive,
|
|
1534
|
+
alpha: safeProp(node, 'alpha', 1),
|
|
1535
|
+
x: nx, y: ny,
|
|
1536
|
+
width: nw, height: nh,
|
|
1537
|
+
worldBounds: wb,
|
|
1538
|
+
path: path,
|
|
1539
|
+
},
|
|
1540
|
+
depth: depth
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
var nodeChildren = getChildren(node);
|
|
1545
|
+
for (var i = 0; i < nodeChildren.length; i++) {
|
|
1546
|
+
var cn = nodeChildren[i];
|
|
1547
|
+
if (!cn) continue;
|
|
1548
|
+
var childPath = accPath ? accPath + '/' + nodeId(cn, i) : nodePath(cn, sceneIdx);
|
|
1549
|
+
hitTestDfs(cn, depth + 1, childPath);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Traverse displayList for manual hit test
|
|
1554
|
+
if (game && game.scene && game.scene.scenes) {
|
|
1555
|
+
var scenes2 = game.scene.scenes;
|
|
1556
|
+
for (var sj = 0; sj < scenes2.length; sj++) {
|
|
1557
|
+
var scj = scenes2[sj];
|
|
1558
|
+
if (!scj || !scj.sys) continue;
|
|
1559
|
+
var statusj = safeProp(scj.sys, 'settings', {}).status;
|
|
1560
|
+
if (statusj !== undefined && statusj !== 4) {
|
|
1561
|
+
sceneIdx = sj;
|
|
1562
|
+
var displayList = scj.sys && scj.sys.displayList;
|
|
1563
|
+
if (displayList) {
|
|
1564
|
+
hitTestDfs(displayList, 0, 'Phaser.Game.scenes[' + sj + ']');
|
|
1565
|
+
}
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Use engine pick if available, otherwise topmost DFS candidate
|
|
1572
|
+
var picked = enginePicked ? {
|
|
1573
|
+
id: enginePicked.id !== undefined ? String(enginePicked.id) : nodeId(enginePicked, 0),
|
|
1574
|
+
type: enginePicked.constructor ? enginePicked.constructor.name : 'DisplayObject',
|
|
1575
|
+
name: safeProp(enginePicked, 'name', undefined),
|
|
1576
|
+
visible: !!(safeProp(enginePicked, 'visible', true)),
|
|
1577
|
+
interactive: !!(enginePicked.input && enginePicked.input.enabled),
|
|
1578
|
+
alpha: safeProp(enginePicked, 'alpha', 1),
|
|
1579
|
+
x: safeProp(enginePicked, 'x', 0),
|
|
1580
|
+
y: safeProp(enginePicked, 'y', 0),
|
|
1581
|
+
width: safeProp(enginePicked, 'width', 0) || safeProp(enginePicked, 'displayWidth', 0),
|
|
1582
|
+
height: safeProp(enginePicked, 'height', 0) || safeProp(enginePicked, 'displayHeight', 0),
|
|
1583
|
+
worldBounds: nodeBounds(enginePicked),
|
|
1584
|
+
path: 'Phaser.Game.scenes[' + sceneIdx + ']/hit',
|
|
1585
|
+
} : null;
|
|
1586
|
+
|
|
1587
|
+
if (!picked && candidates.length > 0) {
|
|
1588
|
+
candidates.sort(function(a, b) { return a.depth - b.depth; });
|
|
1589
|
+
picked = candidates[0].node;
|
|
1590
|
+
hitTestMethod = 'manual';
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return {
|
|
1594
|
+
success: !!picked,
|
|
1595
|
+
picked: picked,
|
|
1596
|
+
candidates: candidates,
|
|
1597
|
+
coordinates: {
|
|
1598
|
+
screen: { x: sx, y: sy },
|
|
1599
|
+
canvas: { x: canvasX, y: canvasY },
|
|
1600
|
+
},
|
|
1601
|
+
hitTestMethod: hitTestMethod
|
|
1602
|
+
};
|
|
1603
|
+
})()`;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Phaser 3.x canvas engine adapter.
|
|
1607
|
+
*
|
|
1608
|
+
* Detection checks window.Phaser and Phaser.GAMES.length > 0.
|
|
1609
|
+
* dumpScene() traverses Phaser.GAMES[0].scene.scenes active scenes.
|
|
1610
|
+
* pickAt() uses scene.input.hitTestPointer() with recursive DFS fallback.
|
|
1611
|
+
*/
|
|
1612
|
+
var PhaserCanvasAdapter = class {
|
|
1613
|
+
id = "phaser";
|
|
1614
|
+
engine = "Phaser";
|
|
1615
|
+
version;
|
|
1616
|
+
constructor() {
|
|
1617
|
+
this.version = void 0;
|
|
1618
|
+
}
|
|
1619
|
+
async detect(env) {
|
|
1620
|
+
try {
|
|
1621
|
+
const result = await env.pageController.evaluate(`
|
|
1622
|
+
(function() {
|
|
1623
|
+
if (typeof window.Phaser === 'undefined' || window.Phaser === null) {
|
|
1624
|
+
return { present: false, hasGames: false };
|
|
1625
|
+
}
|
|
1626
|
+
var hasGames = !!(window.Phaser.GAMES && window.Phaser.GAMES.length > 0);
|
|
1627
|
+
return {
|
|
1628
|
+
present: true,
|
|
1629
|
+
hasGames: hasGames,
|
|
1630
|
+
version: window.Phaser.VERSION || undefined,
|
|
1631
|
+
};
|
|
1632
|
+
})()
|
|
1633
|
+
`);
|
|
1634
|
+
if (!result.present || !result.hasGames) return null;
|
|
1635
|
+
const evidence = ["window.Phaser detected"];
|
|
1636
|
+
evidence.push("Phaser.GAMES has entries");
|
|
1637
|
+
return {
|
|
1638
|
+
engine: this.engine,
|
|
1639
|
+
version: result.version,
|
|
1640
|
+
confidence: .95,
|
|
1641
|
+
evidence,
|
|
1642
|
+
adapterId: this.id
|
|
1643
|
+
};
|
|
1644
|
+
} catch {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
async dumpScene(env, opts) {
|
|
1649
|
+
const payload = buildPhaserSceneTreeDumpPayload(opts);
|
|
1650
|
+
const raw = await env.pageController.evaluate(payload);
|
|
1651
|
+
return {
|
|
1652
|
+
engine: raw.engine,
|
|
1653
|
+
version: raw.version,
|
|
1654
|
+
canvas: raw.canvas,
|
|
1655
|
+
sceneTree: raw.sceneTree ?? {
|
|
1656
|
+
id: "empty",
|
|
1657
|
+
type: "Game",
|
|
1658
|
+
visible: true,
|
|
1659
|
+
interactive: false,
|
|
1660
|
+
alpha: 1,
|
|
1661
|
+
x: 0,
|
|
1662
|
+
y: 0,
|
|
1663
|
+
width: raw.canvas?.width ?? 0,
|
|
1664
|
+
height: raw.canvas?.height ?? 0,
|
|
1665
|
+
worldBounds: {
|
|
1666
|
+
x: 0,
|
|
1667
|
+
y: 0,
|
|
1668
|
+
width: raw.canvas?.width ?? 0,
|
|
1669
|
+
height: raw.canvas?.height ?? 0
|
|
1670
|
+
},
|
|
1671
|
+
path: "Phaser.Game"
|
|
1672
|
+
},
|
|
1673
|
+
totalNodes: raw.totalNodes,
|
|
1674
|
+
completeness: raw.completeness === "full" ? "full" : "partial"
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
async pickAt(env, opts) {
|
|
1678
|
+
const payload = buildPhaserHitTestPayload(opts);
|
|
1679
|
+
const result = await env.pageController.evaluate(payload);
|
|
1680
|
+
return {
|
|
1681
|
+
success: result.success,
|
|
1682
|
+
picked: result.picked,
|
|
1683
|
+
candidates: result.candidates,
|
|
1684
|
+
coordinates: result.coordinates,
|
|
1685
|
+
hitTestMethod: result.hitTestMethod
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
//#endregion
|
|
1690
|
+
//#region src/server/domains/canvas/adapters/pixi-adapter.ts
|
|
1691
|
+
/**
|
|
1692
|
+
* Generates a self-contained JS string that traverses the PIXI scene graph via DFS
|
|
1693
|
+
* and returns a serialisable scene tree with worldBounds computed via getBounds().
|
|
1694
|
+
*
|
|
1695
|
+
* Handles both PIXI v7 (interactive property) and v8 (eventMode property).
|
|
1696
|
+
*
|
|
1697
|
+
* @param opts - Dump options (maxDepth, onlyInteractive, onlyVisible)
|
|
1698
|
+
*/
|
|
1699
|
+
function buildPixiSceneTreeDumpPayload(opts) {
|
|
1700
|
+
const maxDepth = opts.maxDepth ?? 20;
|
|
1701
|
+
const onlyInteractive = opts.onlyInteractive ?? false;
|
|
1702
|
+
return `(function() {
|
|
1703
|
+
function getChildren(node) {
|
|
1704
|
+
if (!node) return [];
|
|
1705
|
+
if (node.children && node.numChildren !== undefined) return node.children;
|
|
1706
|
+
if (Array.isArray(node.children)) return node.children;
|
|
1707
|
+
return [];
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function getNumChildren(node) {
|
|
1711
|
+
if (!node) return 0;
|
|
1712
|
+
if (node.numChildren !== undefined) return node.numChildren;
|
|
1713
|
+
if (node.children && Array.isArray(node.children)) return node.children.length;
|
|
1714
|
+
return 0;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function nodeId(node, idx) {
|
|
1718
|
+
if (node.uid !== undefined && node.uid !== null) return 'uid_' + node.uid;
|
|
1719
|
+
if (node._uid !== undefined && node._uid !== null) return 'uid_' + node._uid;
|
|
1720
|
+
if (node.id !== undefined && node.id !== null && node.id !== '') return String(node.id);
|
|
1721
|
+
return (node.constructor ? node.constructor.name : 'DisplayObject') + '_' + idx;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function safeProp(node, key, fallback) {
|
|
1725
|
+
try { var v = node[key]; return v === undefined || v === null ? fallback : v; } catch(e) { return fallback; }
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function getWorldBounds(node, stage) {
|
|
1729
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1730
|
+
try {
|
|
1731
|
+
if (typeof node.getBounds === 'function') {
|
|
1732
|
+
var b = node.getBounds(stage);
|
|
1733
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
1734
|
+
}
|
|
1735
|
+
} catch(e) {}
|
|
1736
|
+
return {
|
|
1737
|
+
x: safeProp(node, 'x', 0),
|
|
1738
|
+
y: safeProp(node, 'y', 0),
|
|
1739
|
+
width: safeProp(node, 'width', 0) || safeProp(node, 'getBounds', { width: 0 }).width,
|
|
1740
|
+
height: safeProp(node, 'height', 0) || safeProp(node, 'getBounds', { height: 0 }).height
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
var totalNodes = 0;
|
|
1745
|
+
|
|
1746
|
+
function traverse(node, depth, path) {
|
|
1747
|
+
if (!node || depth > ${maxDepth}) return null;
|
|
1748
|
+
totalNodes++;
|
|
1749
|
+
|
|
1750
|
+
var visible = !!(safeProp(node, 'visible', true));
|
|
1751
|
+
// PIXI v7 uses 'interactive', v8 uses 'eventMode'
|
|
1752
|
+
var interactive = !!(safeProp(node, 'interactive', false)) || !!(safeProp(node, 'eventMode', 'none') !== 'none');
|
|
1753
|
+
|
|
1754
|
+
if (${opts.onlyVisible ?? false} && !visible) return null;
|
|
1755
|
+
if (${onlyInteractive} && !interactive) return null;
|
|
1756
|
+
|
|
1757
|
+
var numC = getNumChildren(node);
|
|
1758
|
+
var children = null;
|
|
1759
|
+
|
|
1760
|
+
if (numC > 0) {
|
|
1761
|
+
children = [];
|
|
1762
|
+
var nodeChildren = getChildren(node);
|
|
1763
|
+
for (var i = 0; i < nodeChildren.length; i++) {
|
|
1764
|
+
var cn = nodeChildren[i];
|
|
1765
|
+
if (!cn) continue;
|
|
1766
|
+
var childPath = path ? path + '/' + nodeId(cn, i) : nodeId(cn, i);
|
|
1767
|
+
var sub = traverse(cn, depth + 1, childPath);
|
|
1768
|
+
if (sub) children.push(sub);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
var eventMode = safeProp(node, 'eventMode', null);
|
|
1773
|
+
var result = {
|
|
1774
|
+
id: nodeId(node, 0),
|
|
1775
|
+
type: node.constructor ? node.constructor.name : 'DisplayObject',
|
|
1776
|
+
name: safeProp(node, 'name', undefined),
|
|
1777
|
+
visible: visible,
|
|
1778
|
+
interactive: interactive,
|
|
1779
|
+
alpha: safeProp(node, 'alpha', 1),
|
|
1780
|
+
x: safeProp(node, 'x', 0),
|
|
1781
|
+
y: safeProp(node, 'y', 0),
|
|
1782
|
+
width: safeProp(node, 'width', 0),
|
|
1783
|
+
height: safeProp(node, 'height', 0),
|
|
1784
|
+
worldBounds: getWorldBounds(node, null),
|
|
1785
|
+
path: path || nodeId(node, 0),
|
|
1786
|
+
customData: {
|
|
1787
|
+
eventMode: eventMode,
|
|
1788
|
+
rotation: safeProp(node, 'rotation', 0),
|
|
1789
|
+
scaleX: safeProp(node, 'scaleX', 1),
|
|
1790
|
+
scaleY: safeProp(node, 'scaleY', 1),
|
|
1791
|
+
pivotX: safeProp(node, 'pivotX', 0),
|
|
1792
|
+
pivotY: safeProp(node, 'pivotY', 0),
|
|
1793
|
+
sortDirty: !!(node.sortDirty),
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
if (children && children.length > 0) result.children = children;
|
|
1798
|
+
return result;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Find PIXI app: scan canvas elements for _pixiApp property, or check window.__pixiApp
|
|
1802
|
+
var app = null;
|
|
1803
|
+
if (window.__pixiApp) {
|
|
1804
|
+
app = window.__pixiApp;
|
|
1805
|
+
} else {
|
|
1806
|
+
var canvases = document.querySelectorAll('canvas');
|
|
1807
|
+
for (var ci = 0; ci < canvases.length; ci++) {
|
|
1808
|
+
if (canvases[ci]._pixiApp) { app = canvases[ci]._pixiApp; break; }
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if (!app || !app.stage) {
|
|
1813
|
+
var pixiVersion = window.PIXI ? (window.PIXI.VERSION || window.PIXI.version) : undefined;
|
|
1814
|
+
return {
|
|
1815
|
+
engine: 'PixiJS',
|
|
1816
|
+
version: pixiVersion,
|
|
1817
|
+
canvas: { width: 0, height: 0, dpr: 1, contextType: 'unknown' },
|
|
1818
|
+
sceneTree: null,
|
|
1819
|
+
totalNodes: 0,
|
|
1820
|
+
completeness: 'partial',
|
|
1821
|
+
error: 'PIXI.Application or stage not found'
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
var stage = app.stage;
|
|
1826
|
+
var pixiVersion = window.PIXI ? (window.PIXI.VERSION || window.PIXI.version) : undefined;
|
|
1827
|
+
var canvasEl = app.view || null;
|
|
1828
|
+
var canvasInfo = {
|
|
1829
|
+
width: canvasEl ? (canvasEl.width || 0) : 0,
|
|
1830
|
+
height: canvasEl ? (canvasEl.height || 0) : 0,
|
|
1831
|
+
dpr: window.devicePixelRatio || 1,
|
|
1832
|
+
contextType: 'webgl2'
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
if (canvasEl) {
|
|
1836
|
+
var gl = canvasEl.getContext('webgl2') || canvasEl.getContext('webgl');
|
|
1837
|
+
if (!gl) {
|
|
1838
|
+
var ctx = canvasEl.getContext('2d');
|
|
1839
|
+
canvasInfo.contextType = ctx ? '2d' : 'webgl';
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
var sceneTree = traverse(stage, 0, 'PIXI.Application.stage');
|
|
1844
|
+
|
|
1845
|
+
return {
|
|
1846
|
+
engine: 'PixiJS',
|
|
1847
|
+
version: pixiVersion,
|
|
1848
|
+
canvas: canvasInfo,
|
|
1849
|
+
sceneTree: sceneTree,
|
|
1850
|
+
totalNodes: totalNodes,
|
|
1851
|
+
completeness: 'full'
|
|
1852
|
+
};
|
|
1853
|
+
})()`;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Generates a self-contained JS string that:
|
|
1857
|
+
* 1. Transforms screen coordinates → canvas coordinates
|
|
1858
|
+
* 2. Runs hit test via Stage.hitTest() if available (PIXI v7+), or DFS with getBounds()
|
|
1859
|
+
* 3. Returns all candidates sorted by depth (topmost first)
|
|
1860
|
+
*
|
|
1861
|
+
* @param opts - Pick options (x, y, canvasId)
|
|
1862
|
+
*/
|
|
1863
|
+
function buildPixiHitTestPayload(opts) {
|
|
1864
|
+
const x = opts.x;
|
|
1865
|
+
const y = opts.y;
|
|
1866
|
+
const canvasId = opts.canvasId;
|
|
1867
|
+
return `(function() {
|
|
1868
|
+
function getChildren(node) {
|
|
1869
|
+
if (!node) return [];
|
|
1870
|
+
if (node.children && node.numChildren !== undefined) return node.children;
|
|
1871
|
+
if (Array.isArray(node.children)) return node.children;
|
|
1872
|
+
return [];
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
function getNumChildren(node) {
|
|
1876
|
+
if (!node) return 0;
|
|
1877
|
+
if (node.numChildren !== undefined) return node.numChildren;
|
|
1878
|
+
if (node.children && Array.isArray(node.children)) return node.children.length;
|
|
1879
|
+
return 0;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function nodeId(node, idx) {
|
|
1883
|
+
if (node.uid !== undefined && node.uid !== null) return 'uid_' + node.uid;
|
|
1884
|
+
if (node._uid !== undefined && node._uid !== null) return 'uid_' + node._uid;
|
|
1885
|
+
if (node.id !== undefined && node.id !== null && node.id !== '') return String(node.id);
|
|
1886
|
+
return (node.constructor ? node.constructor.name : 'DisplayObject') + '_' + idx;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function safeProp(node, key, fallback) {
|
|
1890
|
+
try { var v = node[key]; return v === undefined || v === null ? fallback : v; } catch(e) { return fallback; }
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
function getWorldBounds(node, stage) {
|
|
1894
|
+
if (!node) return { x: 0, y: 0, width: 0, height: 0 };
|
|
1895
|
+
try {
|
|
1896
|
+
if (typeof node.getBounds === 'function') {
|
|
1897
|
+
var b = node.getBounds(stage || true);
|
|
1898
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
1899
|
+
}
|
|
1900
|
+
} catch(e) {}
|
|
1901
|
+
return {
|
|
1902
|
+
x: safeProp(node, 'x', 0),
|
|
1903
|
+
y: safeProp(node, 'y', 0),
|
|
1904
|
+
width: safeProp(node, 'width', 0),
|
|
1905
|
+
height: safeProp(node, 'height', 0)
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function nodePath(node) {
|
|
1910
|
+
var parts = [];
|
|
1911
|
+
var cur = node;
|
|
1912
|
+
while (cur && cur.parent && cur.parent !== cur) {
|
|
1913
|
+
var name = cur.name || nodeId(cur, 0);
|
|
1914
|
+
parts.unshift(name);
|
|
1915
|
+
cur = cur.parent;
|
|
1916
|
+
if (!cur) break;
|
|
1917
|
+
}
|
|
1918
|
+
parts.unshift('PIXI.Application.stage');
|
|
1919
|
+
return parts.join('/');
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
var sx = ${x}, sy = ${y};
|
|
1923
|
+
|
|
1924
|
+
// Find the target canvas
|
|
1925
|
+
var canvases = Array.from(document.querySelectorAll('canvas'));
|
|
1926
|
+
var targetCanvas = null;
|
|
1927
|
+
${canvasId ? `targetCanvas = document.getElementById(${JSON.stringify(canvasId)}) || null;` : `
|
|
1928
|
+
for (var ci = canvases.length - 1; ci >= 0; ci--) {
|
|
1929
|
+
var r = canvases[ci].getBoundingClientRect();
|
|
1930
|
+
if (sx >= r.left && sx <= r.right && sy >= r.top && sy <= r.bottom) {
|
|
1931
|
+
targetCanvas = canvases[ci];
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
}`}
|
|
1935
|
+
|
|
1936
|
+
// Find PIXI app
|
|
1937
|
+
var app = null;
|
|
1938
|
+
if (window.__pixiApp) {
|
|
1939
|
+
app = window.__pixiApp;
|
|
1940
|
+
} else {
|
|
1941
|
+
for (var ci2 = 0; ci2 < canvases.length; ci2++) {
|
|
1942
|
+
if (canvases[ci2]._pixiApp) { app = canvases[ci2]._pixiApp; break; }
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
if (!app || !app.stage) {
|
|
1947
|
+
return {
|
|
1948
|
+
success: false,
|
|
1949
|
+
picked: null,
|
|
1950
|
+
candidates: [],
|
|
1951
|
+
coordinates: { screen: { x: sx, y: sy }, canvas: { x: sx, y: sy } },
|
|
1952
|
+
hitTestMethod: 'none'
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
var stage = app.stage;
|
|
1957
|
+
|
|
1958
|
+
// Screen → canvas
|
|
1959
|
+
var canvasX = sx, canvasY = sy;
|
|
1960
|
+
if (targetCanvas) {
|
|
1961
|
+
var rect = targetCanvas.getBoundingClientRect();
|
|
1962
|
+
canvasX = (sx - rect.left) * ((targetCanvas.width || 1) / (rect.width || 1));
|
|
1963
|
+
canvasY = (sy - rect.top) * ((targetCanvas.height || 1) / (rect.height || 1));
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
var candidates = [];
|
|
1967
|
+
var hitTestMethod = 'none';
|
|
1968
|
+
var enginePicked = null;
|
|
1969
|
+
|
|
1970
|
+
// Try engine-native hitTest first (PIXI v7+)
|
|
1971
|
+
if (typeof stage.hitTest === 'function') {
|
|
1972
|
+
try {
|
|
1973
|
+
var nativeHit = stage.hitTest(canvasX, canvasY);
|
|
1974
|
+
if (nativeHit) {
|
|
1975
|
+
enginePicked = nativeHit;
|
|
1976
|
+
hitTestMethod = 'engine';
|
|
1977
|
+
}
|
|
1978
|
+
} catch(e) {}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// Recursive DFS hit test (always available; fallback for all versions)
|
|
1982
|
+
function hitTestDfs(node, depth, accPath) {
|
|
1983
|
+
if (!node) return;
|
|
1984
|
+
var visible = safeProp(node, 'visible', true);
|
|
1985
|
+
if (!visible) return;
|
|
1986
|
+
|
|
1987
|
+
var wb = getWorldBounds(node, stage);
|
|
1988
|
+
var lx = canvasX, ly = canvasY;
|
|
1989
|
+
|
|
1990
|
+
// Convert canvas → node local using parent chain
|
|
1991
|
+
var cur = node;
|
|
1992
|
+
var screenPt = { x: canvasX, y: canvasY };
|
|
1993
|
+
while (cur) {
|
|
1994
|
+
if (typeof cur.worldTransform !== 'undefined') {
|
|
1995
|
+
try {
|
|
1996
|
+
var wt = cur.worldTransform;
|
|
1997
|
+
var det = wt.a * wt.d - wt.b * wt.c;
|
|
1998
|
+
if (Math.abs(det) > 1e-10) {
|
|
1999
|
+
var invX = (wt.d * screenPt.x - wt.b * screenPt.y + wt.c * wt.ty - wt.d * wt.tx) / det;
|
|
2000
|
+
var invY = (-wt.b * screenPt.x + wt.a * screenPt.y - wt.a * wt.ty + wt.c * wt.tx) / det;
|
|
2001
|
+
screenPt = { x: invX, y: invY };
|
|
2002
|
+
}
|
|
2003
|
+
} catch(e) { break; }
|
|
2004
|
+
}
|
|
2005
|
+
cur = cur.parent;
|
|
2006
|
+
}
|
|
2007
|
+
lx = screenPt.x;
|
|
2008
|
+
ly = screenPt.y;
|
|
2009
|
+
|
|
2010
|
+
var nw = safeProp(node, 'width', 0) || wb.width;
|
|
2011
|
+
var nh = safeProp(node, 'height', 0) || wb.height;
|
|
2012
|
+
var nx = safeProp(node, 'x', 0), ny = safeProp(node, 'y', 0);
|
|
2013
|
+
var pivotX = safeProp(node, 'pivotX', 0), pivotY = safeProp(node, 'pivotY', 0);
|
|
2014
|
+
var scaleX = safeProp(node, 'scaleX', 1), scaleY = safeProp(node, 'scaleY', 1);
|
|
2015
|
+
var rotation = safeProp(node, 'rotation', 0);
|
|
2016
|
+
|
|
2017
|
+
// Simple AABB bounds check in node local space
|
|
2018
|
+
var halfW = (nw * Math.abs(scaleX)) / 2;
|
|
2019
|
+
var halfH = (nh * Math.abs(scaleY)) / 2;
|
|
2020
|
+
var centerX = nx + pivotX * scaleX;
|
|
2021
|
+
var centerY = ny + pivotY * scaleY;
|
|
2022
|
+
|
|
2023
|
+
// Apply rotation
|
|
2024
|
+
var cosR = Math.cos(rotation), sinR = Math.sin(rotation);
|
|
2025
|
+
var rotLx = (lx - centerX) * cosR + (ly - centerY) * sinR;
|
|
2026
|
+
var rotLy = -(lx - centerX) * sinR + (ly - centerY) * cosR;
|
|
2027
|
+
|
|
2028
|
+
var inBounds = Math.abs(rotLx) <= halfW && Math.abs(rotLy) <= halfH;
|
|
2029
|
+
|
|
2030
|
+
// PIXI v7 uses 'interactive', v8 uses 'eventMode'
|
|
2031
|
+
var interactive = !!(safeProp(node, 'interactive', false)) ||
|
|
2032
|
+
!!(safeProp(node, 'eventMode', 'none') !== 'none');
|
|
2033
|
+
|
|
2034
|
+
if (inBounds && interactive) {
|
|
2035
|
+
var path = accPath || nodeId(node, 0);
|
|
2036
|
+
candidates.push({
|
|
2037
|
+
node: {
|
|
2038
|
+
id: nodeId(node, 0),
|
|
2039
|
+
type: node.constructor ? node.constructor.name : 'DisplayObject',
|
|
2040
|
+
name: safeProp(node, 'name', undefined),
|
|
2041
|
+
visible: visible,
|
|
2042
|
+
interactive: interactive,
|
|
2043
|
+
alpha: safeProp(node, 'alpha', 1),
|
|
2044
|
+
x: nx, y: ny,
|
|
2045
|
+
width: nw, height: nh,
|
|
2046
|
+
worldBounds: wb,
|
|
2047
|
+
path: path
|
|
2048
|
+
},
|
|
2049
|
+
depth: depth
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
var nodeChildren = getChildren(node);
|
|
2054
|
+
for (var i = 0; i < nodeChildren.length; i++) {
|
|
2055
|
+
var cn = nodeChildren[i];
|
|
2056
|
+
if (!cn) continue;
|
|
2057
|
+
var childPath = accPath ? accPath + '/' + nodeId(cn, i) : nodeId(cn, i);
|
|
2058
|
+
hitTestDfs(cn, depth + 1, childPath);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
hitTestDfs(stage, 0, 'PIXI.Application.stage');
|
|
2063
|
+
|
|
2064
|
+
// Use engine pick if available, otherwise topmost DFS candidate
|
|
2065
|
+
var picked = enginePicked;
|
|
2066
|
+
var finalMethod = hitTestMethod;
|
|
2067
|
+
|
|
2068
|
+
if (!picked && candidates.length > 0) {
|
|
2069
|
+
candidates.sort(function(a, b) { return a.depth - b.depth; });
|
|
2070
|
+
picked = candidates[0].node;
|
|
2071
|
+
finalMethod = 'manual';
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
return {
|
|
2075
|
+
success: !!picked,
|
|
2076
|
+
picked: picked,
|
|
2077
|
+
candidates: candidates,
|
|
2078
|
+
coordinates: {
|
|
2079
|
+
screen: { x: sx, y: sy },
|
|
2080
|
+
canvas: { x: canvasX, y: canvasY }
|
|
2081
|
+
},
|
|
2082
|
+
hitTestMethod: finalMethod
|
|
2083
|
+
};
|
|
2084
|
+
})()`;
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* PixiJS canvas engine adapter.
|
|
2088
|
+
*
|
|
2089
|
+
* Handles both PixiJS v7 (interactive) and v8 (eventMode). Version is resolved
|
|
2090
|
+
* lazily from window.PIXI.VERSION at detect() time.
|
|
2091
|
+
*/
|
|
2092
|
+
var PixiJSCanvasAdapter = class {
|
|
2093
|
+
id = "pixi";
|
|
2094
|
+
engine = "PixiJS";
|
|
2095
|
+
version;
|
|
2096
|
+
constructor() {
|
|
2097
|
+
this.version = void 0;
|
|
2098
|
+
}
|
|
2099
|
+
async detect(env) {
|
|
2100
|
+
try {
|
|
2101
|
+
const result = await env.pageController.evaluate(`
|
|
2102
|
+
(function() {
|
|
2103
|
+
if (typeof window.PIXI === 'undefined' || window.PIXI === null) {
|
|
2104
|
+
return { present: false, hasApp: false };
|
|
2105
|
+
}
|
|
2106
|
+
var pixi = window.PIXI;
|
|
2107
|
+
var hasApp = !!(pixi.Application);
|
|
2108
|
+
var version = pixi.VERSION || pixi.version;
|
|
2109
|
+
return { present: true, hasApp: hasApp, version: version };
|
|
2110
|
+
})()
|
|
2111
|
+
`);
|
|
2112
|
+
if (!result.present || !result.hasApp) return null;
|
|
2113
|
+
const evidence = ["window.PIXI detected"];
|
|
2114
|
+
if (result.version) evidence.push("PIXI.VERSION: " + result.version);
|
|
2115
|
+
evidence.push("PIXI.Application detected");
|
|
2116
|
+
return {
|
|
2117
|
+
engine: this.engine,
|
|
2118
|
+
version: result.version,
|
|
2119
|
+
confidence: .95,
|
|
2120
|
+
evidence,
|
|
2121
|
+
adapterId: this.id
|
|
2122
|
+
};
|
|
2123
|
+
} catch {
|
|
2124
|
+
return null;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
async dumpScene(env, opts) {
|
|
2128
|
+
const payload = buildPixiSceneTreeDumpPayload(opts);
|
|
2129
|
+
const raw = await env.pageController.evaluate(payload);
|
|
2130
|
+
return {
|
|
2131
|
+
engine: raw.engine,
|
|
2132
|
+
version: raw.version,
|
|
2133
|
+
canvas: raw.canvas,
|
|
2134
|
+
sceneTree: raw.sceneTree ?? {
|
|
2135
|
+
id: "empty",
|
|
2136
|
+
type: "Container",
|
|
2137
|
+
visible: true,
|
|
2138
|
+
interactive: false,
|
|
2139
|
+
alpha: 1,
|
|
2140
|
+
x: 0,
|
|
2141
|
+
y: 0,
|
|
2142
|
+
width: raw.canvas?.width ?? 0,
|
|
2143
|
+
height: raw.canvas?.height ?? 0,
|
|
2144
|
+
worldBounds: {
|
|
2145
|
+
x: 0,
|
|
2146
|
+
y: 0,
|
|
2147
|
+
width: raw.canvas?.width ?? 0,
|
|
2148
|
+
height: raw.canvas?.height ?? 0
|
|
2149
|
+
},
|
|
2150
|
+
path: "PIXI.Application.stage"
|
|
2151
|
+
},
|
|
2152
|
+
totalNodes: raw.totalNodes,
|
|
2153
|
+
completeness: raw.completeness === "full" ? "full" : "partial"
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
async pickAt(env, opts) {
|
|
2157
|
+
const payload = buildPixiHitTestPayload(opts);
|
|
2158
|
+
const result = await env.pageController.evaluate(payload);
|
|
2159
|
+
return {
|
|
2160
|
+
success: result.success,
|
|
2161
|
+
picked: result.picked,
|
|
2162
|
+
candidates: result.candidates,
|
|
2163
|
+
coordinates: result.coordinates,
|
|
2164
|
+
hitTestMethod: result.hitTestMethod
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
//#endregion
|
|
2169
|
+
//#region src/server/domains/canvas/handlers/shared.ts
|
|
2170
|
+
/**
|
|
2171
|
+
* Shared utilities for canvas tool handlers.
|
|
2172
|
+
*
|
|
2173
|
+
* Engine anchor patterns, adapter registry, probe env builder, and shared helpers.
|
|
2174
|
+
*/
|
|
2175
|
+
/** Well-known engine global anchors and their adapter IDs. */
|
|
2176
|
+
const ENGINE_ANCHORS = [
|
|
2177
|
+
{
|
|
2178
|
+
pattern: "Laya",
|
|
2179
|
+
adapterId: "laya",
|
|
2180
|
+
engine: "LayaAir"
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
pattern: "PIXI",
|
|
2184
|
+
adapterId: "pixi",
|
|
2185
|
+
engine: "PixiJS"
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
pattern: "Phaser",
|
|
2189
|
+
adapterId: "phaser",
|
|
2190
|
+
engine: "Phaser"
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
pattern: "cc",
|
|
2194
|
+
adapterId: "cocos",
|
|
2195
|
+
engine: "CocosCreator"
|
|
2196
|
+
},
|
|
2197
|
+
{
|
|
2198
|
+
pattern: "legacyCC",
|
|
2199
|
+
adapterId: "cocos",
|
|
2200
|
+
engine: "CocosCreator"
|
|
2201
|
+
},
|
|
2202
|
+
{
|
|
2203
|
+
pattern: "BABYLON",
|
|
2204
|
+
adapterId: "babylon",
|
|
2205
|
+
engine: "Babylon.js"
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
pattern: "THREE",
|
|
2209
|
+
adapterId: "three",
|
|
2210
|
+
engine: "Three.js"
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
pattern: "createUnityInstance",
|
|
2214
|
+
adapterId: "unity",
|
|
2215
|
+
engine: "UnityWebGL"
|
|
2216
|
+
}
|
|
2217
|
+
];
|
|
2218
|
+
/** Adapter registry — registered per engine. */
|
|
2219
|
+
const adapterRegistry = /* @__PURE__ */ new Map();
|
|
2220
|
+
const adapterFactories = {
|
|
2221
|
+
laya: () => new LayaCanvasAdapter(),
|
|
2222
|
+
pixi: () => new PixiJSCanvasAdapter(),
|
|
2223
|
+
phaser: () => new PhaserCanvasAdapter(),
|
|
2224
|
+
cocos: () => new CocosCanvasAdapter()
|
|
2225
|
+
};
|
|
2226
|
+
function resolveAdapter(detection) {
|
|
2227
|
+
if (!detection) return null;
|
|
2228
|
+
if (adapterRegistry.has(detection.adapterId)) return adapterRegistry.get(detection.adapterId);
|
|
2229
|
+
const factory = adapterFactories[detection.adapterId];
|
|
2230
|
+
if (!factory) return null;
|
|
2231
|
+
const adapter = factory();
|
|
2232
|
+
adapterRegistry.set(detection.adapterId, adapter);
|
|
2233
|
+
return adapter;
|
|
2234
|
+
}
|
|
2235
|
+
function buildEnv(pageController) {
|
|
2236
|
+
return {
|
|
2237
|
+
pageController,
|
|
2238
|
+
cdpSession: null,
|
|
2239
|
+
tabId: "default"
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
async function fingerprintCanvas(pageController, canvasId) {
|
|
2243
|
+
try {
|
|
2244
|
+
const result = await pageController.evaluate(`
|
|
2245
|
+
(function() {
|
|
2246
|
+
const hits = [];
|
|
2247
|
+
${ENGINE_ANCHORS.map(({ pattern, adapterId, engine }) => `
|
|
2248
|
+
try {
|
|
2249
|
+
if (window[${JSON.stringify(pattern)}] !== undefined) {
|
|
2250
|
+
const v = window[${JSON.stringify(pattern)}];
|
|
2251
|
+
hits.push({
|
|
2252
|
+
engine: ${JSON.stringify(engine)},
|
|
2253
|
+
adapterId: ${JSON.stringify(adapterId)},
|
|
2254
|
+
version: v.version || v.VERSION || (v.Laya && v.Laya.version) || undefined
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
} catch(e) {}
|
|
2258
|
+
`).join("")}
|
|
2259
|
+
|
|
2260
|
+
if (hits.length === 0) {
|
|
2261
|
+
return { hits: hits, selected: null, selectedEvidence: [] };
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
var targetCanvas = null;
|
|
2265
|
+
if (${canvasId !== void 0}) {
|
|
2266
|
+
var canvases = Array.from(document.querySelectorAll('canvas'));
|
|
2267
|
+
var requestedId = ${JSON.stringify(canvasId ?? null)};
|
|
2268
|
+
var requestedIndex = Number.parseInt(requestedId, 10);
|
|
2269
|
+
targetCanvas =
|
|
2270
|
+
document.getElementById(requestedId) ||
|
|
2271
|
+
(Number.isNaN(requestedIndex) ? null : canvases[requestedIndex] || null);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (!targetCanvas || hits.length === 1) {
|
|
2275
|
+
return { hits: hits, selected: hits[0] || null, selectedEvidence: [] };
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function canvasEquals(candidate) {
|
|
2279
|
+
return candidate === targetCanvas;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function collectCanvasEvidence(adapterId) {
|
|
2283
|
+
var evidence = [];
|
|
2284
|
+
|
|
2285
|
+
if (adapterId === 'pixi') {
|
|
2286
|
+
if (targetCanvas._pixiApp) {
|
|
2287
|
+
evidence.push('target canvas owns _pixiApp');
|
|
2288
|
+
}
|
|
2289
|
+
if (window.__pixiApp && canvasEquals(window.__pixiApp.view)) {
|
|
2290
|
+
evidence.push('target canvas matches window.__pixiApp.view');
|
|
2291
|
+
}
|
|
2292
|
+
if (window.__PIXI_APP__ && canvasEquals(window.__PIXI_APP__.view)) {
|
|
2293
|
+
evidence.push('target canvas matches window.__PIXI_APP__.view');
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (adapterId === 'phaser' && window.Phaser && Array.isArray(window.Phaser.GAMES)) {
|
|
2298
|
+
if (window.Phaser.GAMES.some(function(game) { return game && canvasEquals(game.canvas); })) {
|
|
2299
|
+
evidence.push('target canvas matches Phaser.GAMES[].canvas');
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
if (adapterId === 'cocos') {
|
|
2304
|
+
var cocos = window.cc || window.legacyCC;
|
|
2305
|
+
var game = cocos && cocos.game;
|
|
2306
|
+
if (game) {
|
|
2307
|
+
if (canvasEquals(game.canvas) || canvasEquals(game._canvas)) {
|
|
2308
|
+
evidence.push('target canvas matches cc.game canvas');
|
|
2309
|
+
}
|
|
2310
|
+
if (game.container && typeof game.container.contains === 'function' && game.container.contains(targetCanvas)) {
|
|
2311
|
+
evidence.push('target canvas is inside cc.game.container');
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
if (adapterId === 'laya' && window.Laya) {
|
|
2317
|
+
var candidates = [
|
|
2318
|
+
window.Laya.Browser && window.Laya.Browser.canvas,
|
|
2319
|
+
window.Laya.Render && window.Laya.Render._mainCanvas && (window.Laya.Render._mainCanvas.source || window.Laya.Render._mainCanvas._source || window.Laya.Render._mainCanvas),
|
|
2320
|
+
window.Laya.Render && window.Laya.Render._context && window.Laya.Render._context.canvas && (window.Laya.Render._context.canvas.source || window.Laya.Render._context.canvas),
|
|
2321
|
+
window.Laya.stage && window.Laya.stage._canvas && (window.Laya.stage._canvas.source || window.Laya.stage._canvas._source || window.Laya.stage._canvas),
|
|
2322
|
+
window.Laya.stage && window.Laya.stage.canvas && (window.Laya.stage.canvas.source || window.Laya.stage.canvas._source || window.Laya.stage.canvas)
|
|
2323
|
+
].filter(Boolean);
|
|
2324
|
+
if (candidates.some(canvasEquals)) {
|
|
2325
|
+
evidence.push('target canvas matches Laya render canvas');
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
return evidence;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
for (var i = 0; i < hits.length; i++) {
|
|
2333
|
+
var selectedEvidence = collectCanvasEvidence(hits[i].adapterId);
|
|
2334
|
+
if (selectedEvidence.length > 0) {
|
|
2335
|
+
return {
|
|
2336
|
+
hits: hits,
|
|
2337
|
+
selected: hits[i],
|
|
2338
|
+
selectedEvidence: selectedEvidence
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
return { hits: hits, selected: hits[0] || null, selectedEvidence: [] };
|
|
2344
|
+
})()
|
|
2345
|
+
`);
|
|
2346
|
+
if (!result.selected) return null;
|
|
2347
|
+
const hit = result.selected;
|
|
2348
|
+
return {
|
|
2349
|
+
engine: hit.engine,
|
|
2350
|
+
version: hit.version,
|
|
2351
|
+
confidence: result.selectedEvidence.length > 0 ? .95 : .9,
|
|
2352
|
+
evidence: ["window global detected", ...result.selectedEvidence],
|
|
2353
|
+
adapterId: hit.adapterId
|
|
2354
|
+
};
|
|
2355
|
+
} catch {
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
//#endregion
|
|
2360
|
+
//#region src/server/domains/canvas/handlers/fingerprint.ts
|
|
2361
|
+
async function handleFingerprint(pageController, args) {
|
|
2362
|
+
const canvasId = args["canvasId"];
|
|
2363
|
+
const globalScan = await pageController.evaluate(`
|
|
2364
|
+
(function() {
|
|
2365
|
+
const results = [];
|
|
2366
|
+
${ENGINE_ANCHORS.map(({ pattern, adapterId, engine }) => `
|
|
2367
|
+
try {
|
|
2368
|
+
const global = window[${JSON.stringify(pattern)}];
|
|
2369
|
+
if (global !== undefined) {
|
|
2370
|
+
results.push({
|
|
2371
|
+
pattern: ${JSON.stringify(pattern)},
|
|
2372
|
+
adapterId: ${JSON.stringify(adapterId)},
|
|
2373
|
+
engine: ${JSON.stringify(engine)},
|
|
2374
|
+
present: true,
|
|
2375
|
+
version: (global.version || global.VERSION || global.Laya?.version || undefined)
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
} catch(e) {}
|
|
2379
|
+
`).join("")}
|
|
2380
|
+
return results;
|
|
2381
|
+
})()
|
|
2382
|
+
`);
|
|
2383
|
+
const candidates = [];
|
|
2384
|
+
for (const hit of globalScan) candidates.push({
|
|
2385
|
+
engine: hit.engine,
|
|
2386
|
+
version: hit.version,
|
|
2387
|
+
confidence: .9,
|
|
2388
|
+
evidence: [`global window.${hit.pattern} is defined`],
|
|
2389
|
+
adapterId: hit.adapterId
|
|
2390
|
+
});
|
|
2391
|
+
const canvasInfo = await pageController.evaluate(`
|
|
2392
|
+
(function() {
|
|
2393
|
+
return Array.from(document.querySelectorAll('canvas')).map(function(c) {
|
|
2394
|
+
var ctx = c.getContext('webgl2') || c.getContext('webgl') || c.getContext('2d');
|
|
2395
|
+
var info = {
|
|
2396
|
+
id: c.id || '',
|
|
2397
|
+
width: c.width,
|
|
2398
|
+
height: c.height,
|
|
2399
|
+
contextType: ctx ? ctx.constructor.name : 'none'
|
|
2400
|
+
};
|
|
2401
|
+
if (ctx && (ctx instanceof WebGLRenderingContext || ctx instanceof WebGL2RenderingContext)) {
|
|
2402
|
+
var debugInfo = ctx.getExtension('WEBGL_debug_renderer_info');
|
|
2403
|
+
if (debugInfo) {
|
|
2404
|
+
info.renderer = ctx.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return info;
|
|
2408
|
+
});
|
|
2409
|
+
})()
|
|
2410
|
+
`);
|
|
2411
|
+
if (await pageController.evaluate(`
|
|
2412
|
+
!!(requestAnimationFrame.toString().includes('[native]') &&
|
|
2413
|
+
(window.__canvasEngineHint || document.querySelector('[data-engine]')))
|
|
2414
|
+
`) && candidates.length === 0) candidates.push({
|
|
2415
|
+
engine: "Unknown Canvas Engine",
|
|
2416
|
+
confidence: .3,
|
|
2417
|
+
evidence: ["requestAnimationFrame hook detected", "canvas elements found"],
|
|
2418
|
+
adapterId: "none"
|
|
2419
|
+
});
|
|
2420
|
+
return {
|
|
2421
|
+
candidates,
|
|
2422
|
+
canvasCount: canvasInfo.length,
|
|
2423
|
+
canvasDetails: canvasId !== void 0 ? canvasInfo.filter((c) => c.id === canvasId || canvasInfo.indexOf(c).toString() === canvasId) : canvasInfo,
|
|
2424
|
+
fingerprintComplete: candidates.length > 0
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
//#endregion
|
|
2428
|
+
//#region src/server/domains/canvas/handlers/scene-dump.ts
|
|
2429
|
+
async function handleSceneDump(pageController, args) {
|
|
2430
|
+
const canvasId = args["canvasId"];
|
|
2431
|
+
const opts = {
|
|
2432
|
+
canvasId,
|
|
2433
|
+
maxDepth: args["maxDepth"] ?? 20,
|
|
2434
|
+
onlyInteractive: args["onlyInteractive"] ?? false,
|
|
2435
|
+
onlyVisible: args["onlyVisible"] ?? false
|
|
2436
|
+
};
|
|
2437
|
+
const detection = await fingerprintCanvas(pageController, canvasId);
|
|
2438
|
+
if (!detection) return partialSceneDump(pageController, canvasId);
|
|
2439
|
+
const adapter = resolveAdapter(detection);
|
|
2440
|
+
if (!adapter) return partialSceneDump(pageController, canvasId);
|
|
2441
|
+
return adapter.dumpScene(buildEnv(pageController), opts);
|
|
2442
|
+
}
|
|
2443
|
+
async function partialSceneDump(pageController, canvasId) {
|
|
2444
|
+
const canvases = await pageController.evaluate(`
|
|
2445
|
+
(function() {
|
|
2446
|
+
return Array.from(document.querySelectorAll('canvas')).map(function(c, i) {
|
|
2447
|
+
var ctx2d = c.getContext('2d');
|
|
2448
|
+
var ctxWebgl = c.getContext('webgl') || c.getContext('webgl2');
|
|
2449
|
+
return {
|
|
2450
|
+
id: c.id || String(i),
|
|
2451
|
+
width: c.width,
|
|
2452
|
+
height: c.height,
|
|
2453
|
+
dpr: window.devicePixelRatio || 1,
|
|
2454
|
+
contextType: ctx2d ? '2d' : (ctxWebgl ? 'webgl' : 'unknown')
|
|
2455
|
+
};
|
|
2456
|
+
});
|
|
2457
|
+
})()
|
|
2458
|
+
`);
|
|
2459
|
+
return {
|
|
2460
|
+
engine: "unknown",
|
|
2461
|
+
version: void 0,
|
|
2462
|
+
canvas: (canvasId ? canvases.filter((c) => c.id === canvasId || canvases.indexOf(c).toString() === canvasId) : canvases)[0] ?? {
|
|
2463
|
+
id: canvasId ?? "",
|
|
2464
|
+
width: 0,
|
|
2465
|
+
height: 0,
|
|
2466
|
+
dpr: 1,
|
|
2467
|
+
contextType: "unknown"
|
|
2468
|
+
},
|
|
2469
|
+
sceneTree: null,
|
|
2470
|
+
totalNodes: 0,
|
|
2471
|
+
completeness: "partial",
|
|
2472
|
+
partialReason: "No canvas engine detected — only DOM canvas metadata returned"
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
//#endregion
|
|
2476
|
+
//#region src/server/domains/canvas/handlers/pick.ts
|
|
2477
|
+
async function handlePick(pageController, args) {
|
|
2478
|
+
const x = args["x"];
|
|
2479
|
+
const y = args["y"];
|
|
2480
|
+
const canvasId = args["canvasId"];
|
|
2481
|
+
const highlight = args["highlight"] ?? false;
|
|
2482
|
+
const opts = {
|
|
2483
|
+
x,
|
|
2484
|
+
y,
|
|
2485
|
+
canvasId
|
|
2486
|
+
};
|
|
2487
|
+
const coordInfo = await pageController.evaluate(`
|
|
2488
|
+
(function() {
|
|
2489
|
+
var sx = ${x}, sy = ${y};
|
|
2490
|
+
var canvases = Array.from(document.querySelectorAll('canvas'));
|
|
2491
|
+
var target = null;
|
|
2492
|
+
${canvasId ? `target = document.getElementById(${JSON.stringify(canvasId)}) || canvases[parseInt(${JSON.stringify(canvasId)})];` : `
|
|
2493
|
+
for (var i = canvases.length - 1; i >= 0; i--) {
|
|
2494
|
+
var r = canvases[i].getBoundingClientRect();
|
|
2495
|
+
if (sx >= r.left && sx <= r.right && sy >= r.top && sy <= r.bottom) {
|
|
2496
|
+
target = canvases[i];
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
`}
|
|
2501
|
+
if (!target) return { screen: { x: sx, y: sy }, canvasX: sx, canvasY: sy };
|
|
2502
|
+
var rect = target.getBoundingClientRect();
|
|
2503
|
+
var dpr = window.devicePixelRatio || 1;
|
|
2504
|
+
return {
|
|
2505
|
+
screen: { x: sx, y: sy },
|
|
2506
|
+
canvasRect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
|
2507
|
+
canvasX: (sx - rect.left) * (target.width / rect.width),
|
|
2508
|
+
canvasY: (sy - rect.top) * (target.height / rect.height)
|
|
2509
|
+
};
|
|
2510
|
+
})()
|
|
2511
|
+
`);
|
|
2512
|
+
const detection = await fingerprintCanvas(pageController, canvasId);
|
|
2513
|
+
if (!detection) return {
|
|
2514
|
+
success: false,
|
|
2515
|
+
picked: null,
|
|
2516
|
+
candidates: [],
|
|
2517
|
+
coordinates: {
|
|
2518
|
+
screen: coordInfo.screen,
|
|
2519
|
+
canvas: {
|
|
2520
|
+
x: coordInfo.canvasX,
|
|
2521
|
+
y: coordInfo.canvasY
|
|
2522
|
+
}
|
|
2523
|
+
},
|
|
2524
|
+
hitTestMethod: "none"
|
|
2525
|
+
};
|
|
2526
|
+
const adapter = resolveAdapter(detection);
|
|
2527
|
+
if (!adapter) return {
|
|
2528
|
+
success: false,
|
|
2529
|
+
picked: null,
|
|
2530
|
+
candidates: [],
|
|
2531
|
+
coordinates: {
|
|
2532
|
+
screen: coordInfo.screen,
|
|
2533
|
+
canvas: {
|
|
2534
|
+
x: coordInfo.canvasX,
|
|
2535
|
+
y: coordInfo.canvasY
|
|
2536
|
+
}
|
|
2537
|
+
},
|
|
2538
|
+
hitTestMethod: "none"
|
|
2539
|
+
};
|
|
2540
|
+
const result = await adapter.pickAt(buildEnv(pageController), opts);
|
|
2541
|
+
if (highlight && result.picked) await highlightNode(pageController, result.picked.worldBounds).catch(() => {});
|
|
2542
|
+
return result;
|
|
2543
|
+
}
|
|
2544
|
+
async function highlightNode(pageController, bounds) {
|
|
2545
|
+
await pageController.evaluate(`
|
|
2546
|
+
(function() {
|
|
2547
|
+
var existing = document.getElementById('__canvas-highlight');
|
|
2548
|
+
if (existing) existing.remove();
|
|
2549
|
+
var div = document.createElement('div');
|
|
2550
|
+
div.id = '__canvas-highlight';
|
|
2551
|
+
Object.assign(div.style, {
|
|
2552
|
+
position: 'fixed', left: ${bounds.x} + 'px', top: ${bounds.y} + 'px',
|
|
2553
|
+
width: ${bounds.width} + 'px', height: ${bounds.height} + 'px',
|
|
2554
|
+
border: '2px solid #00ff88', pointerEvents: 'none', zIndex: 2147483647,
|
|
2555
|
+
background: 'rgba(0,255,136,0.15)', boxSizing: 'border-box'
|
|
2556
|
+
});
|
|
2557
|
+
document.body.appendChild(div);
|
|
2558
|
+
setTimeout(function() { div.remove(); }, 3000);
|
|
2559
|
+
})()
|
|
2560
|
+
`);
|
|
2561
|
+
}
|
|
2562
|
+
//#endregion
|
|
2563
|
+
//#region src/server/domains/canvas/handlers/trace.ts
|
|
2564
|
+
async function handleTraceClick(pageController, debuggerManager, evidenceStore, args) {
|
|
2565
|
+
const x = args["x"];
|
|
2566
|
+
const y = args["y"];
|
|
2567
|
+
const canvasId = args["canvasId"];
|
|
2568
|
+
const maxFrames = args["maxFrames"] ?? 50;
|
|
2569
|
+
const breakpointType = args["breakpointType"] ?? "click";
|
|
2570
|
+
await debuggerManager.enable();
|
|
2571
|
+
await debuggerManager.ensureAdvancedFeatures();
|
|
2572
|
+
const eventMgr = debuggerManager.getEventManager();
|
|
2573
|
+
const breakpointId = await eventMgr.setEventListenerBreakpoint(breakpointType);
|
|
2574
|
+
try {
|
|
2575
|
+
const dispatchResult = await dispatchCanvasClick(pageController, x, y, canvasId, breakpointType);
|
|
2576
|
+
const pausedState = await debuggerManager.waitForPaused(5e3);
|
|
2577
|
+
let stackFrames = [];
|
|
2578
|
+
if (pausedState?.callFrames) stackFrames = pausedState.callFrames.slice(0, maxFrames).map((frame) => ({
|
|
2579
|
+
functionName: frame.functionName || "(anonymous)",
|
|
2580
|
+
scriptUrl: frame.url,
|
|
2581
|
+
lineNumber: frame.location?.lineNumber,
|
|
2582
|
+
columnNumber: frame.location?.columnNumber
|
|
2583
|
+
}));
|
|
2584
|
+
recordEvidence(evidenceStore, "canvas_trace", {
|
|
2585
|
+
engine: dispatchResult.engine ?? "unknown",
|
|
2586
|
+
x,
|
|
2587
|
+
y,
|
|
2588
|
+
handlerCount: stackFrames.length
|
|
2589
|
+
});
|
|
2590
|
+
return {
|
|
2591
|
+
inputFlow: dispatchResult.domEventChain ?? [],
|
|
2592
|
+
hitTarget: null,
|
|
2593
|
+
domEventChain: stackFrames.map((f) => ({
|
|
2594
|
+
type: breakpointType,
|
|
2595
|
+
target: f.scriptUrl,
|
|
2596
|
+
phase: "at-target"
|
|
2597
|
+
})),
|
|
2598
|
+
engineDispatchChain: dispatchResult.engineChain ?? [],
|
|
2599
|
+
handlerFrames: stackFrames,
|
|
2600
|
+
handlersTriggered: stackFrames.map((f) => ({
|
|
2601
|
+
functionName: f.functionName,
|
|
2602
|
+
scriptUrl: f.scriptUrl,
|
|
2603
|
+
lineNumber: f.lineNumber
|
|
2604
|
+
})),
|
|
2605
|
+
networkEmitted: []
|
|
2606
|
+
};
|
|
2607
|
+
} finally {
|
|
2608
|
+
try {
|
|
2609
|
+
await debuggerManager.resume();
|
|
2610
|
+
} catch {}
|
|
2611
|
+
if (typeof eventMgr.removeEventListenerBreakpoint === "function") try {
|
|
2612
|
+
await eventMgr.removeEventListenerBreakpoint(breakpointId);
|
|
2613
|
+
} catch {}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
async function dispatchCanvasClick(pageController, x, y, canvasId, breakpointType = "click") {
|
|
2617
|
+
const dispatchSequence = getDispatchSequence(breakpointType);
|
|
2618
|
+
const script = `
|
|
2619
|
+
(function() {
|
|
2620
|
+
var canvases = ${canvasId ? `[document.getElementById(${JSON.stringify(canvasId)})]` : "Array.from(document.querySelectorAll(\"canvas\"))"};
|
|
2621
|
+
var target = null;
|
|
2622
|
+
if (${!!canvasId}) {
|
|
2623
|
+
target = document.getElementById(${JSON.stringify(canvasId)});
|
|
2624
|
+
} else {
|
|
2625
|
+
for (var i = canvases.length - 1; i >= 0; i--) {
|
|
2626
|
+
var r = canvases[i].getBoundingClientRect();
|
|
2627
|
+
if (${x} >= r.left && ${x} <= r.right && ${y} >= r.top && ${y} <= r.bottom) {
|
|
2628
|
+
target = canvases[i];
|
|
2629
|
+
break;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
if (!target) return { domEventChain: [], pickedNode: null, engine: undefined, engineChain: [] };
|
|
2634
|
+
|
|
2635
|
+
var rect = target.getBoundingClientRect();
|
|
2636
|
+
var cx = (${x} - rect.left) * (target.width / rect.width);
|
|
2637
|
+
var cy = (${y} - rect.top) * (target.height / rect.height);
|
|
2638
|
+
|
|
2639
|
+
var events = [];
|
|
2640
|
+
${JSON.stringify(dispatchSequence)}.forEach(function(type) {
|
|
2641
|
+
var EventCtor = type.indexOf('pointer') === 0 && typeof PointerEvent === 'function'
|
|
2642
|
+
? PointerEvent
|
|
2643
|
+
: MouseEvent;
|
|
2644
|
+
var e = new EventCtor(type, {
|
|
2645
|
+
view: window,
|
|
2646
|
+
bubbles: true,
|
|
2647
|
+
cancelable: true,
|
|
2648
|
+
clientX: ${x},
|
|
2649
|
+
clientY: ${y},
|
|
2650
|
+
button: 0,
|
|
2651
|
+
buttons: 1,
|
|
2652
|
+
pointerId: 1
|
|
2653
|
+
});
|
|
2654
|
+
target.dispatchEvent(e);
|
|
2655
|
+
events.push(type);
|
|
2656
|
+
});
|
|
2657
|
+
return {
|
|
2658
|
+
domEventChain: events,
|
|
2659
|
+
pickedNode: null,
|
|
2660
|
+
engine: window.Laya ? 'LayaAir' : (window.PIXI ? 'PixiJS' : undefined),
|
|
2661
|
+
engineChain: []
|
|
2662
|
+
};
|
|
2663
|
+
})()
|
|
2664
|
+
`;
|
|
2665
|
+
return pageController.evaluate(script);
|
|
2666
|
+
}
|
|
2667
|
+
function getDispatchSequence(breakpointType) {
|
|
2668
|
+
if (breakpointType === "mousedown") return ["pointerdown", "mousedown"];
|
|
2669
|
+
if (breakpointType === "pointerdown") return ["pointerdown"];
|
|
2670
|
+
return [
|
|
2671
|
+
"pointerdown",
|
|
2672
|
+
"mousedown",
|
|
2673
|
+
"pointerup",
|
|
2674
|
+
"mouseup",
|
|
2675
|
+
"click"
|
|
2676
|
+
];
|
|
2677
|
+
}
|
|
2678
|
+
function recordEvidence(evidenceStore, label, metadata) {
|
|
2679
|
+
try {
|
|
2680
|
+
return evidenceStore.addNode("function", label, metadata).id;
|
|
2681
|
+
} catch {
|
|
2682
|
+
return "evidence-unavailable";
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
//#endregion
|
|
2686
|
+
//#region src/server/domains/canvas/handlers.impl.ts
|
|
2687
|
+
var CanvasToolHandlers = class {
|
|
2688
|
+
pageController;
|
|
2689
|
+
debuggerManager;
|
|
2690
|
+
evidenceStore;
|
|
2691
|
+
constructor(deps) {
|
|
2692
|
+
this.pageController = deps.pageController;
|
|
2693
|
+
this.debuggerManager = deps.debuggerManager;
|
|
2694
|
+
this.evidenceStore = deps.evidenceStore;
|
|
2695
|
+
}
|
|
2696
|
+
async handleFingerprint(args) {
|
|
2697
|
+
return asJsonResponse(await handleFingerprint(this.pageController, args));
|
|
2698
|
+
}
|
|
2699
|
+
async handleSceneDump(args) {
|
|
2700
|
+
return asJsonResponse(await handleSceneDump(this.pageController, args));
|
|
2701
|
+
}
|
|
2702
|
+
async handlePick(args) {
|
|
2703
|
+
const x = argNumber(args, "x", 0);
|
|
2704
|
+
const y = argNumber(args, "y", 0);
|
|
2705
|
+
return asJsonResponse(await handlePick(this.pageController, {
|
|
2706
|
+
...args,
|
|
2707
|
+
x,
|
|
2708
|
+
y
|
|
2709
|
+
}));
|
|
2710
|
+
}
|
|
2711
|
+
async handleTraceClick(args) {
|
|
2712
|
+
return asJsonResponse(await handleTraceClick(this.pageController, this.debuggerManager, this.evidenceStore, args));
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
//#endregion
|
|
2716
|
+
export { CanvasToolHandlers };
|