@buoy-gg/debug-borders 2.0.2
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 +334 -0
- package/lib/commonjs/debug-borders/components/DebugBordersModal.js +234 -0
- package/lib/commonjs/debug-borders/components/DebugBordersStandaloneOverlay.js +436 -0
- package/lib/commonjs/debug-borders/index.js +51 -0
- package/lib/commonjs/debug-borders/types.js +1 -0
- package/lib/commonjs/debug-borders/utils/DebugBordersManager.js +119 -0
- package/lib/commonjs/debug-borders/utils/ViewTypeMapper.js +264 -0
- package/lib/commonjs/debug-borders/utils/colorGeneration.js +76 -0
- package/lib/commonjs/debug-borders/utils/componentInfo.js +183 -0
- package/lib/commonjs/debug-borders/utils/componentMeasurement.js +111 -0
- package/lib/commonjs/debug-borders/utils/fiberTreeTraversal.js +309 -0
- package/lib/commonjs/debug-borders/utils/labelPositioning.js +202 -0
- package/lib/commonjs/index.js +34 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +178 -0
- package/lib/module/debug-borders/components/DebugBordersModal.js +229 -0
- package/lib/module/debug-borders/components/DebugBordersStandaloneOverlay.js +432 -0
- package/lib/module/debug-borders/index.js +15 -0
- package/lib/module/debug-borders/types.js +1 -0
- package/lib/module/debug-borders/utils/DebugBordersManager.js +119 -0
- package/lib/module/debug-borders/utils/ViewTypeMapper.js +255 -0
- package/lib/module/debug-borders/utils/colorGeneration.js +76 -0
- package/lib/module/debug-borders/utils/componentInfo.js +183 -0
- package/lib/module/debug-borders/utils/componentMeasurement.js +111 -0
- package/lib/module/debug-borders/utils/fiberTreeTraversal.js +309 -0
- package/lib/module/debug-borders/utils/labelPositioning.js +202 -0
- package/lib/module/index.js +7 -0
- package/lib/module/preset.js +166 -0
- package/lib/typescript/debug-borders/components/DebugBordersModal.d.ts +11 -0
- package/lib/typescript/debug-borders/components/DebugBordersModal.d.ts.map +1 -0
- package/lib/typescript/debug-borders/components/DebugBordersStandaloneOverlay.d.ts +15 -0
- package/lib/typescript/debug-borders/components/DebugBordersStandaloneOverlay.d.ts.map +1 -0
- package/lib/typescript/debug-borders/index.d.ts +8 -0
- package/lib/typescript/debug-borders/index.d.ts.map +1 -0
- package/lib/typescript/debug-borders/types.d.ts +45 -0
- package/lib/typescript/debug-borders/types.d.ts.map +1 -0
- package/lib/typescript/debug-borders/utils/ViewTypeMapper.d.ts +66 -0
- package/lib/typescript/debug-borders/utils/ViewTypeMapper.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +3 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +108 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function getReactDevToolsHook() {
|
|
4
|
+
if (typeof global === "undefined") {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
8
|
+
if (!hook) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return hook;
|
|
12
|
+
}
|
|
13
|
+
function getFiberRoots() {
|
|
14
|
+
const hook = getReactDevToolsHook();
|
|
15
|
+
if (!hook) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
if (!hook.getFiberRoots) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const rootsSet = hook.getFiberRoots(1);
|
|
23
|
+
if (!rootsSet) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
return Array.from(rootsSet);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const FiberTags = {
|
|
32
|
+
FunctionComponent: 0,
|
|
33
|
+
ClassComponent: 1,
|
|
34
|
+
IndeterminateComponent: 2,
|
|
35
|
+
HostRoot: 3,
|
|
36
|
+
HostPortal: 4,
|
|
37
|
+
HostComponent: 5,
|
|
38
|
+
HostText: 6,
|
|
39
|
+
Fragment: 7,
|
|
40
|
+
Mode: 8,
|
|
41
|
+
ContextConsumer: 9,
|
|
42
|
+
ContextProvider: 10,
|
|
43
|
+
ForwardRef: 11,
|
|
44
|
+
Profiler: 12,
|
|
45
|
+
SuspenseComponent: 13,
|
|
46
|
+
MemoComponent: 14,
|
|
47
|
+
SimpleMemoComponent: 15,
|
|
48
|
+
LazyComponent: 16,
|
|
49
|
+
IncompleteClassComponent: 17,
|
|
50
|
+
DehydratedFragment: 18,
|
|
51
|
+
SuspenseListComponent: 19,
|
|
52
|
+
ScopeComponent: 21,
|
|
53
|
+
OffscreenComponent: 22,
|
|
54
|
+
LegacyHiddenComponent: 23,
|
|
55
|
+
CacheComponent: 24,
|
|
56
|
+
TracingMarkerComponent: 25
|
|
57
|
+
};
|
|
58
|
+
const DEV_TOOLS_COMPONENT_NAMES = new Set(["FloatingTools", "FloatingDevTools", "FloatingMenu", "DialDevTools", "DevToolsVisibilityProvider", "AppHostProvider", "MinimizedToolsProvider", "MinimizedToolsStack", "GlitchToolButton", "DefaultConfigProvider", "JsModalComponent", "JsModal", "ModalHeader", "DraggableHeader", "DragIndicator", "CornerHandle", "WindowControls", "HighlightUpdatesModal", "HighlightUpdatesOverlay", "HighlightFilterView", "RenderDetailView", "RenderListItem", "RenderListItemInner", "RenderHistoryViewer", "RenderCauseBadge", "DebugBordersStandaloneOverlay", "DebugBordersModal", "StorageBrowser", "StorageModal", "GameUIStorageBrowser", "StorageEventListener", "StorageEventFilterView", "NetworkMonitor", "NetworkModal", "NetworkRequestList", "NetworkRequestDetail", "RouteEventsModal", "RouteEventsList", "EnvSwitcher", "EnvSwitcherModal", "TabSelector", "SectionHeader", "TypePicker", "PatternInput", "PatternChip", "DetectedItemsSection", "DetectedCategoryBadge", "IdentifierBadge", "CategoryBadge", "AppRenderer", "AppOverlay", "ExpandablePopover", "Divider", "UserStatus", "GripVerticalIcon", "LogBox", "LogBoxLog", "LogBoxLogNotification", "LogBoxNotificationContainer", "_LogBoxNotificationContainer", "LogBoxInspector", "LogBoxInspectorContainer", "LogBoxInspectorHeader", "LogBoxInspectorBody", "LogBoxInspectorFooter", "LogBoxInspectorMessageHeader", "LogBoxInspectorStackFrame", "LogBoxInspectorSection", "LogBoxButton", "LogBoxMessage"]);
|
|
59
|
+
const DEV_TOOLS_COMPONENT_PREFIXES = ["JsModal", "HighlightUpdates", "RenderList", "RenderDetail", "DebugBorders", "Storage", "Network", "RouteEvents", "EnvSwitcher", "Floating", "Minimized", "Expandable", "DevTools", "GameUI"];
|
|
60
|
+
const DEV_TOOLS_NATIVE_IDS = new Set(["debug-borders-overlay", "floating-devtools-root", "dial-devtools-root", "jsmodal-root", "highlight-updates-overlay", "__rn_buoy__highlight-modal", "logbox_inspector", "logbox"]);
|
|
61
|
+
const devToolsNodeCache = new Map();
|
|
62
|
+
const CACHE_MAX_SIZE = 500;
|
|
63
|
+
function getNativeTag(stateNode) {
|
|
64
|
+
if (!stateNode) return null;
|
|
65
|
+
if (typeof stateNode._nativeTag === "number") return stateNode._nativeTag;
|
|
66
|
+
if (typeof stateNode.canonical?._nativeTag === "number") {
|
|
67
|
+
return stateNode.canonical._nativeTag;
|
|
68
|
+
}
|
|
69
|
+
if (typeof stateNode.__nativeTag === "number") return stateNode.__nativeTag;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function isDevToolsNativeID(nativeID) {
|
|
73
|
+
if (!nativeID) return false;
|
|
74
|
+
if (DEV_TOOLS_NATIVE_IDS.has(nativeID)) return true;
|
|
75
|
+
const firstChar = nativeID.charCodeAt(0);
|
|
76
|
+
if (firstChar === 95) {
|
|
77
|
+
if (nativeID.startsWith("__highlight_") || nativeID.startsWith("__rn_buoy__")) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (firstChar === 108 && nativeID.startsWith("logbox")) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function getComponentName(fiber) {
|
|
87
|
+
if (!fiber) return null;
|
|
88
|
+
const type = fiber.type;
|
|
89
|
+
if (type) {
|
|
90
|
+
if (typeof type === "function") {
|
|
91
|
+
return type.displayName || type.name || null;
|
|
92
|
+
}
|
|
93
|
+
if (typeof type === "string") {
|
|
94
|
+
return type;
|
|
95
|
+
}
|
|
96
|
+
if (type.displayName) return type.displayName;
|
|
97
|
+
if (type.name) return type.name;
|
|
98
|
+
}
|
|
99
|
+
const elementType = fiber.elementType;
|
|
100
|
+
if (elementType) {
|
|
101
|
+
if (typeof elementType === "function") {
|
|
102
|
+
return elementType.displayName || elementType.name || null;
|
|
103
|
+
}
|
|
104
|
+
if (elementType.displayName) return elementType.displayName;
|
|
105
|
+
if (elementType.name) return elementType.name;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function isDevToolsComponent(fiber, stateNode) {
|
|
110
|
+
const nativeTag = getNativeTag(stateNode);
|
|
111
|
+
if (nativeTag != null) {
|
|
112
|
+
const cached = devToolsNodeCache.get(nativeTag);
|
|
113
|
+
if (cached !== undefined) {
|
|
114
|
+
return cached;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let result = false;
|
|
118
|
+
const directNativeID = fiber.pendingProps?.nativeID || fiber.memoizedProps?.nativeID;
|
|
119
|
+
if (isDevToolsNativeID(directNativeID)) {
|
|
120
|
+
result = true;
|
|
121
|
+
} else {
|
|
122
|
+
let currentFiber = fiber;
|
|
123
|
+
let depth = 0;
|
|
124
|
+
while (currentFiber && depth < 30) {
|
|
125
|
+
const name = getComponentName(currentFiber);
|
|
126
|
+
if (name) {
|
|
127
|
+
if (DEV_TOOLS_COMPONENT_NAMES.has(name)) {
|
|
128
|
+
result = true;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
for (const prefix of DEV_TOOLS_COMPONENT_PREFIXES) {
|
|
132
|
+
if (name.startsWith(prefix)) {
|
|
133
|
+
result = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (result) break;
|
|
138
|
+
}
|
|
139
|
+
const nativeID = currentFiber.pendingProps?.nativeID || currentFiber.memoizedProps?.nativeID;
|
|
140
|
+
if (isDevToolsNativeID(nativeID)) {
|
|
141
|
+
result = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
currentFiber = currentFiber.return;
|
|
145
|
+
depth++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (result && nativeTag != null) {
|
|
149
|
+
if (devToolsNodeCache.size >= CACHE_MAX_SIZE) {
|
|
150
|
+
const entries = Array.from(devToolsNodeCache.keys());
|
|
151
|
+
for (let i = 0; i < CACHE_MAX_SIZE / 2; i++) {
|
|
152
|
+
devToolsNodeCache.delete(entries[i]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
devToolsNodeCache.set(nativeTag, result);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
function clearDevToolsCache() {
|
|
160
|
+
devToolsNodeCache.clear();
|
|
161
|
+
}
|
|
162
|
+
function isHiddenOffscreen(fiber) {
|
|
163
|
+
if (fiber.tag !== FiberTags.OffscreenComponent) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return fiber.memoizedState !== null;
|
|
167
|
+
}
|
|
168
|
+
function isInactiveScreen(fiber) {
|
|
169
|
+
const props = fiber.memoizedProps;
|
|
170
|
+
if (!props) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return props.activityState === 0;
|
|
174
|
+
}
|
|
175
|
+
function getNativeViewClassName(stateNode) {
|
|
176
|
+
if (!stateNode) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (stateNode.canonical?.viewConfig?.uiViewClassName) {
|
|
180
|
+
return stateNode.canonical.viewConfig.uiViewClassName;
|
|
181
|
+
}
|
|
182
|
+
if (stateNode.viewConfig?.uiViewClassName) {
|
|
183
|
+
return stateNode.viewConfig.uiViewClassName;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
function isSVGComponent(stateNode) {
|
|
188
|
+
const viewClassName = getNativeViewClassName(stateNode);
|
|
189
|
+
if (!viewClassName) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return viewClassName.startsWith("RNSVG");
|
|
193
|
+
}
|
|
194
|
+
function traverseFiberTree(fiber, callback, depth = 0, visited = new Set()) {
|
|
195
|
+
if (!fiber) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (visited.has(fiber)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
visited.add(fiber);
|
|
202
|
+
if (depth > 500) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (isHiddenOffscreen(fiber)) {
|
|
206
|
+
if (fiber.sibling) {
|
|
207
|
+
traverseFiberTree(fiber.sibling, callback, depth, visited);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (isInactiveScreen(fiber)) {
|
|
212
|
+
if (fiber.sibling) {
|
|
213
|
+
traverseFiberTree(fiber.sibling, callback, depth, visited);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
callback(fiber, depth);
|
|
218
|
+
if (fiber.child) {
|
|
219
|
+
traverseFiberTree(fiber.child, callback, depth + 1, visited);
|
|
220
|
+
}
|
|
221
|
+
if (fiber.sibling) {
|
|
222
|
+
traverseFiberTree(fiber.sibling, callback, depth, visited);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function getAllHostComponentInstances() {
|
|
226
|
+
const roots = getFiberRoots();
|
|
227
|
+
if (roots.length === 0) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const instances = [];
|
|
231
|
+
roots.forEach((root, rootIndex) => {
|
|
232
|
+
traverseFiberTree(root.current, (fiber, depth) => {
|
|
233
|
+
if (fiber.tag === FiberTags.HostComponent) {
|
|
234
|
+
const publicInstance = fiber.stateNode;
|
|
235
|
+
if (isDevToolsComponent(fiber, publicInstance)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (isSVGComponent(publicInstance)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (publicInstance) {
|
|
242
|
+
instances.push({
|
|
243
|
+
instance: publicInstance,
|
|
244
|
+
fiber: fiber,
|
|
245
|
+
depth: depth
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
return instances;
|
|
252
|
+
}
|
|
253
|
+
function isReactDevToolsAvailable() {
|
|
254
|
+
const hook = getReactDevToolsHook();
|
|
255
|
+
if (!hook) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (!hook.getFiberRoots) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
function getReactDevToolsDiagnostics() {
|
|
264
|
+
const hook = getReactDevToolsHook();
|
|
265
|
+
if (!hook) {
|
|
266
|
+
return {
|
|
267
|
+
available: false,
|
|
268
|
+
reason: "Hook not found on global object"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const diagnostics = {
|
|
272
|
+
available: true,
|
|
273
|
+
hasAgent: !!hook.reactDevtoolsAgent,
|
|
274
|
+
hasFiberRoots: typeof hook.getFiberRoots === "function",
|
|
275
|
+
rendererCount: 0,
|
|
276
|
+
rootCount: 0,
|
|
277
|
+
hookKeys: Object.keys(hook)
|
|
278
|
+
};
|
|
279
|
+
if (hook.getFiberRoots) {
|
|
280
|
+
try {
|
|
281
|
+
const roots = hook.getFiberRoots(1);
|
|
282
|
+
if (roots) {
|
|
283
|
+
diagnostics.rootCount = roots.size;
|
|
284
|
+
}
|
|
285
|
+
} catch (e) {
|
|
286
|
+
diagnostics.error = e.message;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return diagnostics;
|
|
290
|
+
}
|
|
291
|
+
module.exports = {
|
|
292
|
+
getReactDevToolsHook,
|
|
293
|
+
getFiberRoots,
|
|
294
|
+
traverseFiberTree,
|
|
295
|
+
getAllHostComponentInstances,
|
|
296
|
+
isReactDevToolsAvailable,
|
|
297
|
+
getReactDevToolsDiagnostics,
|
|
298
|
+
isHiddenOffscreen,
|
|
299
|
+
isInactiveScreen,
|
|
300
|
+
isSVGComponent,
|
|
301
|
+
getNativeViewClassName,
|
|
302
|
+
isDevToolsComponent,
|
|
303
|
+
isDevToolsNativeID,
|
|
304
|
+
getComponentName,
|
|
305
|
+
clearDevToolsCache,
|
|
306
|
+
FiberTags,
|
|
307
|
+
DEV_TOOLS_COMPONENT_NAMES,
|
|
308
|
+
DEV_TOOLS_NATIVE_IDS
|
|
309
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label Positioning Utility
|
|
3
|
+
*
|
|
4
|
+
* Resolves overlapping labels by repositioning them.
|
|
5
|
+
* Uses a greedy algorithm that processes labels once and offsets
|
|
6
|
+
* any that would overlap with already-placed labels.
|
|
7
|
+
*
|
|
8
|
+
* Time Complexity: O(n²) worst case, but typically much faster
|
|
9
|
+
* since we only compare against nearby labels and use early exit.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for label positioning
|
|
16
|
+
*/
|
|
17
|
+
const CONFIG = {
|
|
18
|
+
// Minimum spacing between labels (pixels)
|
|
19
|
+
LABEL_SPACING: 0,
|
|
20
|
+
// Maximum vertical offset before giving up (pixels)
|
|
21
|
+
MAX_VERTICAL_OFFSET: 100,
|
|
22
|
+
// Label height estimate (used for offset increments)
|
|
23
|
+
LABEL_HEIGHT: 10,
|
|
24
|
+
// Padding around labels for collision detection
|
|
25
|
+
PADDING: 0
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if two rectangles overlap
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} a - First rectangle {x, y, width, height}
|
|
32
|
+
* @param {Object} b - Second rectangle {x, y, width, height}
|
|
33
|
+
* @returns {boolean} - True if rectangles overlap
|
|
34
|
+
*/
|
|
35
|
+
function rectsOverlap(a, b) {
|
|
36
|
+
const padding = CONFIG.PADDING;
|
|
37
|
+
const aLeft = a.x - padding;
|
|
38
|
+
const aRight = a.x + a.width + padding;
|
|
39
|
+
const aTop = a.y - padding;
|
|
40
|
+
const aBottom = a.y + a.height + padding;
|
|
41
|
+
const bLeft = b.x - padding;
|
|
42
|
+
const bRight = b.x + b.width + padding;
|
|
43
|
+
const bTop = b.y - padding;
|
|
44
|
+
const bBottom = b.y + b.height + padding;
|
|
45
|
+
|
|
46
|
+
// No overlap if one is completely to the side or above/below the other
|
|
47
|
+
return !(aRight < bLeft || aLeft > bRight || aBottom < bTop || aTop > bBottom);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Estimate label width based on text length
|
|
52
|
+
* (Used when actual measurement isn't available)
|
|
53
|
+
*
|
|
54
|
+
* @param {string} text - Label text
|
|
55
|
+
* @returns {number} - Estimated width in pixels
|
|
56
|
+
*/
|
|
57
|
+
function estimateLabelWidth(text) {
|
|
58
|
+
if (!text) return 0;
|
|
59
|
+
// Approximate: 5px per character for 8pt monospace font + padding
|
|
60
|
+
return text.length * 5 + 8;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve overlapping labels by stacking them upward like a menu
|
|
65
|
+
*
|
|
66
|
+
* Strategy: Process labels in order. For each label, check if it overlaps
|
|
67
|
+
* with any already-placed label. If so, stack it above (going upward).
|
|
68
|
+
* Labels are positioned above their boxes, stacking upward with no gaps.
|
|
69
|
+
*
|
|
70
|
+
* @param {Array<Object>} rectangles - Array of rectangle data with componentInfo
|
|
71
|
+
* @returns {Array<Object>} - Same array with added labelOffset property
|
|
72
|
+
*/
|
|
73
|
+
function resolveOverlappingLabels(rectangles) {
|
|
74
|
+
if (!rectangles || rectangles.length === 0) {
|
|
75
|
+
return rectangles;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Filter to only rectangles with valid labels (testID or accessibilityLabel)
|
|
79
|
+
const validRectangles = rectangles.filter(rect => {
|
|
80
|
+
const info = rect.componentInfo;
|
|
81
|
+
return info && (info.testID || info.accessibilityLabel);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Build label rects with estimated dimensions
|
|
85
|
+
// Labels are positioned ABOVE the box (y - labelHeight)
|
|
86
|
+
const labelRects = rectangles.map((rect, index) => {
|
|
87
|
+
const info = rect.componentInfo;
|
|
88
|
+
const hasValidLabel = info && (info.testID || info.accessibilityLabel);
|
|
89
|
+
const labelText = hasValidLabel ? info.primaryLabel : "";
|
|
90
|
+
const labelWidth = estimateLabelWidth(labelText);
|
|
91
|
+
return {
|
|
92
|
+
index,
|
|
93
|
+
hasValidLabel,
|
|
94
|
+
// Label position is above the box
|
|
95
|
+
x: rect.x,
|
|
96
|
+
y: rect.y - CONFIG.LABEL_HEIGHT,
|
|
97
|
+
// Position above the box
|
|
98
|
+
width: labelWidth,
|
|
99
|
+
height: CONFIG.LABEL_HEIGHT,
|
|
100
|
+
// Track the offset we apply (going upward, so negative)
|
|
101
|
+
offsetY: 0
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Sort by position (top-to-bottom, left-to-right) for consistent placement
|
|
106
|
+
const sortedIndices = labelRects.map((_, i) => i).filter(i => labelRects[i].hasValidLabel) // Only process valid labels
|
|
107
|
+
.sort((a, b) => {
|
|
108
|
+
const rectA = rectangles[a];
|
|
109
|
+
const rectB = rectangles[b];
|
|
110
|
+
// Primary sort by Y, secondary by X
|
|
111
|
+
if (Math.abs(rectA.y - rectB.y) > 10) {
|
|
112
|
+
return rectA.y - rectB.y;
|
|
113
|
+
}
|
|
114
|
+
return rectA.x - rectB.x;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Track placed labels for collision detection
|
|
118
|
+
const placedLabels = [];
|
|
119
|
+
|
|
120
|
+
// Process each label in sorted order
|
|
121
|
+
for (const idx of sortedIndices) {
|
|
122
|
+
const label = labelRects[idx];
|
|
123
|
+
|
|
124
|
+
// Skip empty labels
|
|
125
|
+
if (label.width === 0) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let offsetY = 0;
|
|
129
|
+
let hasOverlap = true;
|
|
130
|
+
let attempts = 0;
|
|
131
|
+
const maxAttempts = Math.ceil(CONFIG.MAX_VERTICAL_OFFSET / CONFIG.LABEL_HEIGHT);
|
|
132
|
+
while (hasOverlap && attempts < maxAttempts) {
|
|
133
|
+
// Create test rect with current offset (going upward)
|
|
134
|
+
const testRect = {
|
|
135
|
+
x: label.x,
|
|
136
|
+
y: label.y - offsetY,
|
|
137
|
+
// Subtract to go upward
|
|
138
|
+
width: label.width,
|
|
139
|
+
height: label.height
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Check against all placed labels
|
|
143
|
+
hasOverlap = placedLabels.some(placed => rectsOverlap(testRect, placed));
|
|
144
|
+
if (hasOverlap) {
|
|
145
|
+
// Stack upward with no spacing
|
|
146
|
+
offsetY += CONFIG.LABEL_HEIGHT;
|
|
147
|
+
attempts++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Store the offset
|
|
152
|
+
label.offsetY = offsetY;
|
|
153
|
+
|
|
154
|
+
// Add to placed labels with final position
|
|
155
|
+
placedLabels.push({
|
|
156
|
+
x: label.x,
|
|
157
|
+
y: label.y - offsetY,
|
|
158
|
+
width: label.width,
|
|
159
|
+
height: label.height
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Apply offsets to original rectangles
|
|
164
|
+
return rectangles.map((rect, index) => ({
|
|
165
|
+
...rect,
|
|
166
|
+
labelOffsetY: labelRects[index].offsetY
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Quick check if any labels might overlap (for early exit optimization)
|
|
172
|
+
*
|
|
173
|
+
* @param {Array<Object>} rectangles - Array of rectangle data
|
|
174
|
+
* @returns {boolean} - True if there might be overlaps worth resolving
|
|
175
|
+
*/
|
|
176
|
+
function mightHaveOverlaps(rectangles) {
|
|
177
|
+
if (!rectangles || rectangles.length < 2) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Quick heuristic: check if any boxes are close together
|
|
182
|
+
for (let i = 0; i < Math.min(rectangles.length, 20); i++) {
|
|
183
|
+
for (let j = i + 1; j < Math.min(rectangles.length, 20); j++) {
|
|
184
|
+
const a = rectangles[i];
|
|
185
|
+
const b = rectangles[j];
|
|
186
|
+
|
|
187
|
+
// If boxes are within label height of each other vertically
|
|
188
|
+
// and overlap horizontally, we might have label overlaps
|
|
189
|
+
if (Math.abs(a.y - b.y) < CONFIG.LABEL_HEIGHT * 2 && !(a.x + 100 < b.x || b.x + 100 < a.x)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
module.exports = {
|
|
197
|
+
resolveOverlappingLabels,
|
|
198
|
+
rectsOverlap,
|
|
199
|
+
estimateLabelWidth,
|
|
200
|
+
mightHaveOverlaps,
|
|
201
|
+
CONFIG
|
|
202
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
var _exportNames = {
|
|
7
|
+
debugBordersToolPreset: true,
|
|
8
|
+
createDebugBordersTool: true
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "createDebugBordersTool", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
get: function () {
|
|
13
|
+
return _preset.createDebugBordersTool;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(exports, "debugBordersToolPreset", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () {
|
|
19
|
+
return _preset.debugBordersToolPreset;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
var _preset = require("./preset");
|
|
23
|
+
var _debugBorders = require("./debug-borders");
|
|
24
|
+
Object.keys(_debugBorders).forEach(function (key) {
|
|
25
|
+
if (key === "default" || key === "__esModule") return;
|
|
26
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
27
|
+
if (key in exports && exports[key] === _debugBorders[key]) return;
|
|
28
|
+
Object.defineProperty(exports, key, {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
get: function () {
|
|
31
|
+
return _debugBorders[key];
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|