@buoy-gg/image-overlay 2.1.11
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/lib/commonjs/imageOverlay/components/ImageOverlayModal.js +847 -0
- package/lib/commonjs/imageOverlay/components/ImageOverlayStandalone.js +455 -0
- package/lib/commonjs/imageOverlay/components/OverlayControls.js +253 -0
- package/lib/commonjs/imageOverlay/components/TargetList.js +183 -0
- package/lib/commonjs/imageOverlay/index.js +33 -0
- package/lib/commonjs/imageOverlay/types/index.js +1 -0
- package/lib/commonjs/imageOverlay/utils/ImageOverlayController.js +392 -0
- package/lib/commonjs/imageOverlay/utils/componentMeasurement.js +106 -0
- package/lib/commonjs/imageOverlay/utils/fiberScanner.js +101 -0
- package/lib/commonjs/index.js +46 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +59 -0
- package/lib/module/imageOverlay/components/ImageOverlayModal.js +843 -0
- package/lib/module/imageOverlay/components/ImageOverlayStandalone.js +451 -0
- package/lib/module/imageOverlay/components/OverlayControls.js +249 -0
- package/lib/module/imageOverlay/components/TargetList.js +179 -0
- package/lib/module/imageOverlay/index.js +6 -0
- package/lib/module/imageOverlay/types/index.js +1 -0
- package/lib/module/imageOverlay/utils/ImageOverlayController.js +388 -0
- package/lib/module/imageOverlay/utils/componentMeasurement.js +102 -0
- package/lib/module/imageOverlay/utils/fiberScanner.js +97 -0
- package/lib/module/index.js +34 -0
- package/lib/module/preset.js +53 -0
- package/lib/typescript/imageOverlay/components/ImageOverlayModal.d.ts +3 -0
- package/lib/typescript/imageOverlay/components/ImageOverlayModal.d.ts.map +1 -0
- package/lib/typescript/imageOverlay/components/ImageOverlayStandalone.d.ts +12 -0
- package/lib/typescript/imageOverlay/components/ImageOverlayStandalone.d.ts.map +1 -0
- package/lib/typescript/imageOverlay/components/OverlayControls.d.ts +18 -0
- package/lib/typescript/imageOverlay/components/OverlayControls.d.ts.map +1 -0
- package/lib/typescript/imageOverlay/components/TargetList.d.ts +12 -0
- package/lib/typescript/imageOverlay/components/TargetList.d.ts.map +1 -0
- package/lib/typescript/imageOverlay/index.d.ts +6 -0
- package/lib/typescript/imageOverlay/index.d.ts.map +1 -0
- package/lib/typescript/imageOverlay/types/index.d.ts +53 -0
- package/lib/typescript/imageOverlay/types/index.d.ts.map +1 -0
- package/lib/typescript/imageOverlay/utils/ImageOverlayController.d.ts +34 -0
- package/lib/typescript/imageOverlay/utils/ImageOverlayController.d.ts.map +1 -0
- package/lib/typescript/imageOverlay/utils/componentMeasurement.d.ts +13 -0
- package/lib/typescript/imageOverlay/utils/componentMeasurement.d.ts.map +1 -0
- package/lib/typescript/imageOverlay/utils/fiberScanner.d.ts +13 -0
- package/lib/typescript/imageOverlay/utils/fiberScanner.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +14 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +46 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* List of discovered image-overlay target components.
|
|
5
|
+
* Styled to match RenderListItem from highlight-updates.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from "react-native";
|
|
9
|
+
import { ChevronRight, buoyColors, Hash } from "@buoy-gg/shared-ui";
|
|
10
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
function TargetRow({
|
|
12
|
+
target,
|
|
13
|
+
onSelect
|
|
14
|
+
}) {
|
|
15
|
+
return /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
16
|
+
style: styles.card,
|
|
17
|
+
onPress: onSelect,
|
|
18
|
+
activeOpacity: 0.7,
|
|
19
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
20
|
+
style: styles.colorIndicator
|
|
21
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
22
|
+
style: styles.content,
|
|
23
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
24
|
+
style: styles.topRow,
|
|
25
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
26
|
+
style: styles.label,
|
|
27
|
+
numberOfLines: 1,
|
|
28
|
+
children: target.label
|
|
29
|
+
})
|
|
30
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
31
|
+
style: styles.bottomRow,
|
|
32
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
33
|
+
style: styles.badgeRow,
|
|
34
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
35
|
+
style: styles.testIdBadge,
|
|
36
|
+
children: [/*#__PURE__*/_jsx(Hash, {
|
|
37
|
+
size: 8,
|
|
38
|
+
color: buoyColors.success
|
|
39
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
40
|
+
style: styles.testIdBadgeText,
|
|
41
|
+
children: "testID"
|
|
42
|
+
})]
|
|
43
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
44
|
+
style: styles.testIdValue,
|
|
45
|
+
numberOfLines: 1,
|
|
46
|
+
children: target.testID
|
|
47
|
+
})]
|
|
48
|
+
}), target.componentName && /*#__PURE__*/_jsx(Text, {
|
|
49
|
+
style: styles.componentName,
|
|
50
|
+
numberOfLines: 1,
|
|
51
|
+
children: target.componentName
|
|
52
|
+
})]
|
|
53
|
+
})]
|
|
54
|
+
}), /*#__PURE__*/_jsx(ChevronRight, {
|
|
55
|
+
size: 16,
|
|
56
|
+
color: buoyColors.textMuted
|
|
57
|
+
})]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function TargetList({
|
|
61
|
+
targets,
|
|
62
|
+
onSelect
|
|
63
|
+
}) {
|
|
64
|
+
if (targets.length === 0) {
|
|
65
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
66
|
+
style: styles.emptyState,
|
|
67
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
68
|
+
style: styles.emptyTitle,
|
|
69
|
+
children: "No targets found"
|
|
70
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
71
|
+
style: styles.emptyText,
|
|
72
|
+
children: ["Tag components with", "\n", "testID=\"image-target:YourLabel\""]
|
|
73
|
+
})]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return /*#__PURE__*/_jsx(ScrollView, {
|
|
77
|
+
style: styles.list,
|
|
78
|
+
contentContainerStyle: styles.listContent,
|
|
79
|
+
children: targets.map(target => /*#__PURE__*/_jsx(TargetRow, {
|
|
80
|
+
target: target,
|
|
81
|
+
onSelect: () => onSelect(target)
|
|
82
|
+
}, target.testID))
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const styles = StyleSheet.create({
|
|
86
|
+
list: {
|
|
87
|
+
flex: 1
|
|
88
|
+
},
|
|
89
|
+
listContent: {
|
|
90
|
+
paddingTop: 8,
|
|
91
|
+
paddingBottom: 20
|
|
92
|
+
},
|
|
93
|
+
card: {
|
|
94
|
+
flexDirection: "row",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
paddingVertical: 10,
|
|
97
|
+
paddingHorizontal: 12,
|
|
98
|
+
marginHorizontal: 12,
|
|
99
|
+
marginBottom: 6,
|
|
100
|
+
backgroundColor: buoyColors.card,
|
|
101
|
+
borderRadius: 8,
|
|
102
|
+
borderWidth: 1,
|
|
103
|
+
borderColor: buoyColors.border
|
|
104
|
+
},
|
|
105
|
+
colorIndicator: {
|
|
106
|
+
width: 4,
|
|
107
|
+
height: 36,
|
|
108
|
+
borderRadius: 2,
|
|
109
|
+
marginRight: 10,
|
|
110
|
+
backgroundColor: buoyColors.primary
|
|
111
|
+
},
|
|
112
|
+
content: {
|
|
113
|
+
flex: 1,
|
|
114
|
+
marginRight: 8
|
|
115
|
+
},
|
|
116
|
+
topRow: {
|
|
117
|
+
flexDirection: "row",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
marginBottom: 4
|
|
120
|
+
},
|
|
121
|
+
label: {
|
|
122
|
+
fontSize: 13,
|
|
123
|
+
fontWeight: "600",
|
|
124
|
+
color: buoyColors.text,
|
|
125
|
+
flex: 1
|
|
126
|
+
},
|
|
127
|
+
bottomRow: {
|
|
128
|
+
gap: 2
|
|
129
|
+
},
|
|
130
|
+
badgeRow: {
|
|
131
|
+
flexDirection: "row",
|
|
132
|
+
alignItems: "center",
|
|
133
|
+
gap: 4
|
|
134
|
+
},
|
|
135
|
+
testIdBadge: {
|
|
136
|
+
flexDirection: "row",
|
|
137
|
+
alignItems: "center",
|
|
138
|
+
paddingVertical: 2,
|
|
139
|
+
paddingHorizontal: 5,
|
|
140
|
+
borderRadius: 4,
|
|
141
|
+
backgroundColor: buoyColors.success + "20",
|
|
142
|
+
gap: 3
|
|
143
|
+
},
|
|
144
|
+
testIdBadgeText: {
|
|
145
|
+
fontSize: 9,
|
|
146
|
+
fontWeight: "700",
|
|
147
|
+
color: buoyColors.success
|
|
148
|
+
},
|
|
149
|
+
testIdValue: {
|
|
150
|
+
fontSize: 11,
|
|
151
|
+
color: buoyColors.text,
|
|
152
|
+
fontFamily: "monospace",
|
|
153
|
+
flex: 1
|
|
154
|
+
},
|
|
155
|
+
componentName: {
|
|
156
|
+
fontSize: 10,
|
|
157
|
+
color: "#a855f7",
|
|
158
|
+
fontFamily: "monospace",
|
|
159
|
+
marginTop: 1
|
|
160
|
+
},
|
|
161
|
+
emptyState: {
|
|
162
|
+
alignItems: "center",
|
|
163
|
+
paddingVertical: 40,
|
|
164
|
+
paddingHorizontal: 32
|
|
165
|
+
},
|
|
166
|
+
emptyTitle: {
|
|
167
|
+
color: buoyColors.text,
|
|
168
|
+
fontSize: 14,
|
|
169
|
+
fontWeight: "600",
|
|
170
|
+
marginBottom: 6
|
|
171
|
+
},
|
|
172
|
+
emptyText: {
|
|
173
|
+
color: buoyColors.textMuted,
|
|
174
|
+
fontSize: 12,
|
|
175
|
+
textAlign: "center",
|
|
176
|
+
lineHeight: 18,
|
|
177
|
+
fontFamily: "monospace"
|
|
178
|
+
}
|
|
179
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export { ImageOverlayModal } from "./components/ImageOverlayModal";
|
|
4
|
+
export { ImageOverlayStandalone } from "./components/ImageOverlayStandalone";
|
|
5
|
+
export { ImageOverlayController } from "./utils/ImageOverlayController";
|
|
6
|
+
export { scanForImageTargets } from "./utils/fiberScanner";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Singleton state manager for the image overlay tool.
|
|
5
|
+
* Bridges the modal (controls) and standalone overlay (rendering).
|
|
6
|
+
* Supports two modes: "component" (match to tagged element) and "free" (drag anywhere).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Image as RNImage, Dimensions } from "react-native";
|
|
10
|
+
import { measureInstance } from "./componentMeasurement";
|
|
11
|
+
import { scanForImageTargets } from "./fiberScanner";
|
|
12
|
+
const DEFAULT_STATE = {
|
|
13
|
+
mode: null,
|
|
14
|
+
enabled: false,
|
|
15
|
+
imageUri: null,
|
|
16
|
+
imageWidth: null,
|
|
17
|
+
imageHeight: null,
|
|
18
|
+
targetTag: null,
|
|
19
|
+
targetLabel: null,
|
|
20
|
+
targetRect: null,
|
|
21
|
+
showOutline: true,
|
|
22
|
+
opacity: 0.5,
|
|
23
|
+
scale: 1.0,
|
|
24
|
+
offsetX: 0,
|
|
25
|
+
offsetY: 0,
|
|
26
|
+
invertX: false,
|
|
27
|
+
invertY: false,
|
|
28
|
+
locked: false,
|
|
29
|
+
freeX: 0,
|
|
30
|
+
freeY: 0,
|
|
31
|
+
freeWidth: 200,
|
|
32
|
+
freeHeight: 200
|
|
33
|
+
};
|
|
34
|
+
let state = {
|
|
35
|
+
...DEFAULT_STATE
|
|
36
|
+
};
|
|
37
|
+
const listeners = new Set();
|
|
38
|
+
|
|
39
|
+
// Hold onto references so we can remeasure
|
|
40
|
+
let targetInstance = null;
|
|
41
|
+
let targetLabel = null;
|
|
42
|
+
|
|
43
|
+
// Auto-tracking via React DevTools traceUpdates
|
|
44
|
+
let autoTrackEnabled = false;
|
|
45
|
+
let traceUnsubscribe = null;
|
|
46
|
+
let trackingNativeTag = null;
|
|
47
|
+
function notify() {
|
|
48
|
+
for (const listener of listeners) {
|
|
49
|
+
listener(state);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getNativeTag(instance) {
|
|
53
|
+
if (!instance) return null;
|
|
54
|
+
if (typeof instance._nativeTag === "number") return instance._nativeTag;
|
|
55
|
+
if (typeof instance.canonical?._nativeTag === "number") return instance.canonical._nativeTag;
|
|
56
|
+
if (typeof instance.__nativeTag === "number") return instance.__nativeTag;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
async function measureWithFallback(instance, label) {
|
|
60
|
+
let rect = await measureInstance(instance);
|
|
61
|
+
if (rect) return {
|
|
62
|
+
rect,
|
|
63
|
+
freshInstance: instance
|
|
64
|
+
};
|
|
65
|
+
const targets = scanForImageTargets();
|
|
66
|
+
const fresh = targets.find(t => t.label === label);
|
|
67
|
+
if (fresh) {
|
|
68
|
+
rect = await measureInstance(fresh.instance);
|
|
69
|
+
if (rect) return {
|
|
70
|
+
rect,
|
|
71
|
+
freshInstance: fresh.instance
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
rect: null,
|
|
76
|
+
freshInstance: instance
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Subscribe to React DevTools traceUpdates to remeasure our target
|
|
82
|
+
* component in real-time whenever ANY component re-renders (which
|
|
83
|
+
* includes scroll events that reposition our target).
|
|
84
|
+
*/
|
|
85
|
+
function startTraceTracking() {
|
|
86
|
+
if (traceUnsubscribe) return; // already tracking
|
|
87
|
+
|
|
88
|
+
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
89
|
+
if (!hook) return;
|
|
90
|
+
|
|
91
|
+
// Enable trace updates on all renderers (same as highlight-updates)
|
|
92
|
+
if (hook.rendererInterfaces) {
|
|
93
|
+
for (const [, renderer] of hook.rendererInterfaces) {
|
|
94
|
+
if (typeof renderer.setTraceUpdatesEnabled === "function") {
|
|
95
|
+
renderer.setTraceUpdatesEnabled(true);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Subscribe to traceUpdates — fires whenever components render
|
|
101
|
+
if (typeof hook.sub === "function") {
|
|
102
|
+
traceUnsubscribe = hook.sub("traceUpdates", handleTraceForAutoTrack);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function stopTraceTracking() {
|
|
106
|
+
if (traceUnsubscribe) {
|
|
107
|
+
traceUnsubscribe();
|
|
108
|
+
traceUnsubscribe = null;
|
|
109
|
+
}
|
|
110
|
+
// Note: we don't disable traceUpdates on renderers because
|
|
111
|
+
// highlight-updates may also be using them
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Called on every traceUpdates event — remeasure our target */
|
|
115
|
+
function handleTraceForAutoTrack(_nodes) {
|
|
116
|
+
if (!autoTrackEnabled || !targetInstance) return;
|
|
117
|
+
|
|
118
|
+
// Don't filter by nativeTag — any render could have caused a layout
|
|
119
|
+
// shift (e.g. sibling resize, scroll). Just remeasure cheaply.
|
|
120
|
+
measureInstance(targetInstance).then(rect => {
|
|
121
|
+
if (rect && state.targetRect) {
|
|
122
|
+
// Only notify if position actually changed
|
|
123
|
+
const prev = state.targetRect;
|
|
124
|
+
if (Math.abs(rect.x - prev.x) > 0.5 || Math.abs(rect.y - prev.y) > 0.5 || Math.abs(rect.width - prev.width) > 0.5 || Math.abs(rect.height - prev.height) > 0.5) {
|
|
125
|
+
state = {
|
|
126
|
+
...state,
|
|
127
|
+
targetRect: rect
|
|
128
|
+
};
|
|
129
|
+
notify();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function getImageSize(uri) {
|
|
135
|
+
return new Promise(resolve => {
|
|
136
|
+
RNImage.getSize(uri, (width, height) => resolve({
|
|
137
|
+
width,
|
|
138
|
+
height
|
|
139
|
+
}), () => resolve(null));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export const ImageOverlayController = {
|
|
143
|
+
subscribe(listener) {
|
|
144
|
+
listeners.add(listener);
|
|
145
|
+
listener(state);
|
|
146
|
+
return () => listeners.delete(listener);
|
|
147
|
+
},
|
|
148
|
+
getState() {
|
|
149
|
+
return state;
|
|
150
|
+
},
|
|
151
|
+
// ─── Component Match Mode ───────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
async setTarget(instance, label, fiber) {
|
|
154
|
+
targetInstance = instance;
|
|
155
|
+
targetLabel = label;
|
|
156
|
+
const instanceToMeasure = fiber?.stateNode ?? instance;
|
|
157
|
+
const {
|
|
158
|
+
rect,
|
|
159
|
+
freshInstance
|
|
160
|
+
} = await measureWithFallback(instanceToMeasure, label);
|
|
161
|
+
targetInstance = freshInstance;
|
|
162
|
+
const tag = getNativeTag(targetInstance);
|
|
163
|
+
state = {
|
|
164
|
+
...state,
|
|
165
|
+
mode: "component",
|
|
166
|
+
targetTag: tag,
|
|
167
|
+
targetLabel: label,
|
|
168
|
+
targetRect: rect
|
|
169
|
+
};
|
|
170
|
+
notify();
|
|
171
|
+
},
|
|
172
|
+
async setImageUri(uri) {
|
|
173
|
+
const size = await getImageSize(uri);
|
|
174
|
+
let autoScale = 1.0;
|
|
175
|
+
if (size && state.targetRect && size.width > 0) {
|
|
176
|
+
autoScale = state.targetRect.width / size.width;
|
|
177
|
+
}
|
|
178
|
+
state = {
|
|
179
|
+
...state,
|
|
180
|
+
imageUri: uri,
|
|
181
|
+
imageWidth: size?.width ?? null,
|
|
182
|
+
imageHeight: size?.height ?? null,
|
|
183
|
+
scale: autoScale,
|
|
184
|
+
showOutline: false
|
|
185
|
+
};
|
|
186
|
+
notify();
|
|
187
|
+
},
|
|
188
|
+
setOpacity(value) {
|
|
189
|
+
state = {
|
|
190
|
+
...state,
|
|
191
|
+
opacity: Math.max(0, Math.min(1, value))
|
|
192
|
+
};
|
|
193
|
+
notify();
|
|
194
|
+
},
|
|
195
|
+
setScale(value) {
|
|
196
|
+
state = {
|
|
197
|
+
...state,
|
|
198
|
+
scale: Math.max(0.01, Math.min(5, value))
|
|
199
|
+
};
|
|
200
|
+
notify();
|
|
201
|
+
},
|
|
202
|
+
setOffset(x, y) {
|
|
203
|
+
state = {
|
|
204
|
+
...state,
|
|
205
|
+
offsetX: x,
|
|
206
|
+
offsetY: y
|
|
207
|
+
};
|
|
208
|
+
notify();
|
|
209
|
+
},
|
|
210
|
+
toggleInvertX() {
|
|
211
|
+
state = {
|
|
212
|
+
...state,
|
|
213
|
+
invertX: !state.invertX
|
|
214
|
+
};
|
|
215
|
+
notify();
|
|
216
|
+
},
|
|
217
|
+
toggleInvertY() {
|
|
218
|
+
state = {
|
|
219
|
+
...state,
|
|
220
|
+
invertY: !state.invertY
|
|
221
|
+
};
|
|
222
|
+
notify();
|
|
223
|
+
},
|
|
224
|
+
setLocked(locked) {
|
|
225
|
+
state = {
|
|
226
|
+
...state,
|
|
227
|
+
locked
|
|
228
|
+
};
|
|
229
|
+
notify();
|
|
230
|
+
},
|
|
231
|
+
setAutoTrack(enabled) {
|
|
232
|
+
autoTrackEnabled = enabled;
|
|
233
|
+
if (enabled) {
|
|
234
|
+
startTraceTracking();
|
|
235
|
+
} else {
|
|
236
|
+
stopTraceTracking();
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
isAutoTracking() {
|
|
240
|
+
return autoTrackEnabled;
|
|
241
|
+
},
|
|
242
|
+
setShowOutline(show) {
|
|
243
|
+
state = {
|
|
244
|
+
...state,
|
|
245
|
+
showOutline: show
|
|
246
|
+
};
|
|
247
|
+
notify();
|
|
248
|
+
},
|
|
249
|
+
setEnabled(enabled) {
|
|
250
|
+
state = {
|
|
251
|
+
...state,
|
|
252
|
+
enabled
|
|
253
|
+
};
|
|
254
|
+
notify();
|
|
255
|
+
},
|
|
256
|
+
toggle() {
|
|
257
|
+
state = {
|
|
258
|
+
...state,
|
|
259
|
+
enabled: !state.enabled
|
|
260
|
+
};
|
|
261
|
+
notify();
|
|
262
|
+
},
|
|
263
|
+
fitWidth() {
|
|
264
|
+
if (state.imageWidth && state.targetRect && state.imageWidth > 0) {
|
|
265
|
+
state = {
|
|
266
|
+
...state,
|
|
267
|
+
scale: state.targetRect.width / state.imageWidth
|
|
268
|
+
};
|
|
269
|
+
notify();
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
fitHeight() {
|
|
273
|
+
if (state.imageHeight && state.targetRect && state.imageHeight > 0) {
|
|
274
|
+
state = {
|
|
275
|
+
...state,
|
|
276
|
+
scale: state.targetRect.height / state.imageHeight
|
|
277
|
+
};
|
|
278
|
+
notify();
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
resetSettings() {
|
|
282
|
+
if (state.mode === "free") {
|
|
283
|
+
const screen = Dimensions.get("window");
|
|
284
|
+
const w = state.imageWidth ?? 200;
|
|
285
|
+
const h = state.imageHeight ?? 200;
|
|
286
|
+
state = {
|
|
287
|
+
...state,
|
|
288
|
+
opacity: 0.5,
|
|
289
|
+
freeX: (screen.width - w) / 2,
|
|
290
|
+
freeY: (screen.height - h) / 2,
|
|
291
|
+
freeWidth: w,
|
|
292
|
+
freeHeight: h
|
|
293
|
+
};
|
|
294
|
+
} else {
|
|
295
|
+
let autoScale = 1.0;
|
|
296
|
+
if (state.imageWidth && state.targetRect && state.imageWidth > 0) {
|
|
297
|
+
autoScale = state.targetRect.width / state.imageWidth;
|
|
298
|
+
}
|
|
299
|
+
state = {
|
|
300
|
+
...state,
|
|
301
|
+
opacity: 0.5,
|
|
302
|
+
scale: autoScale,
|
|
303
|
+
offsetX: 0,
|
|
304
|
+
offsetY: 0
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
notify();
|
|
308
|
+
},
|
|
309
|
+
async remeasure() {
|
|
310
|
+
if (!targetLabel) return;
|
|
311
|
+
const {
|
|
312
|
+
rect,
|
|
313
|
+
freshInstance
|
|
314
|
+
} = await measureWithFallback(targetInstance, targetLabel);
|
|
315
|
+
targetInstance = freshInstance;
|
|
316
|
+
if (rect) {
|
|
317
|
+
state = {
|
|
318
|
+
...state,
|
|
319
|
+
targetRect: rect
|
|
320
|
+
};
|
|
321
|
+
notify();
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
// ─── Free Placement Mode ───────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
async startFreeMode(uri) {
|
|
327
|
+
const size = await getImageSize(uri);
|
|
328
|
+
const screen = Dimensions.get("window");
|
|
329
|
+
const w = size?.width ?? 200;
|
|
330
|
+
const h = size?.height ?? 200;
|
|
331
|
+
|
|
332
|
+
// Scale down if image is larger than 80% of screen
|
|
333
|
+
let displayW = w;
|
|
334
|
+
let displayH = h;
|
|
335
|
+
const maxW = screen.width * 0.8;
|
|
336
|
+
const maxH = screen.height * 0.6;
|
|
337
|
+
if (displayW > maxW || displayH > maxH) {
|
|
338
|
+
const ratio = Math.min(maxW / displayW, maxH / displayH);
|
|
339
|
+
displayW = Math.round(displayW * ratio);
|
|
340
|
+
displayH = Math.round(displayH * ratio);
|
|
341
|
+
}
|
|
342
|
+
state = {
|
|
343
|
+
...state,
|
|
344
|
+
mode: "free",
|
|
345
|
+
enabled: true,
|
|
346
|
+
imageUri: uri,
|
|
347
|
+
imageWidth: size?.width ?? null,
|
|
348
|
+
imageHeight: size?.height ?? null,
|
|
349
|
+
opacity: 0.5,
|
|
350
|
+
freeX: Math.round((screen.width - displayW) / 2),
|
|
351
|
+
freeY: Math.round((screen.height - displayH) / 2),
|
|
352
|
+
freeWidth: Math.round(displayW),
|
|
353
|
+
freeHeight: Math.round(displayH)
|
|
354
|
+
};
|
|
355
|
+
notify();
|
|
356
|
+
},
|
|
357
|
+
setFreePosition(x, y) {
|
|
358
|
+
state = {
|
|
359
|
+
...state,
|
|
360
|
+
freeX: x,
|
|
361
|
+
freeY: y
|
|
362
|
+
};
|
|
363
|
+
notify();
|
|
364
|
+
},
|
|
365
|
+
setFreeDimensions(width, height, x, y) {
|
|
366
|
+
state = {
|
|
367
|
+
...state,
|
|
368
|
+
freeWidth: width,
|
|
369
|
+
freeHeight: height,
|
|
370
|
+
freeX: x,
|
|
371
|
+
freeY: y
|
|
372
|
+
};
|
|
373
|
+
notify();
|
|
374
|
+
},
|
|
375
|
+
// ─── Shared ─────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
reset() {
|
|
378
|
+
stopTraceTracking();
|
|
379
|
+
autoTrackEnabled = false;
|
|
380
|
+
trackingNativeTag = null;
|
|
381
|
+
targetInstance = null;
|
|
382
|
+
targetLabel = null;
|
|
383
|
+
state = {
|
|
384
|
+
...DEFAULT_STATE
|
|
385
|
+
};
|
|
386
|
+
notify();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component measurement utilities for image overlay positioning.
|
|
5
|
+
* Adapted from debug-borders/utils/componentMeasurement.js
|
|
6
|
+
*
|
|
7
|
+
* Supports both Fabric (new arch) and Paper (legacy) measurement APIs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { findNodeHandle, UIManager } from "react-native";
|
|
11
|
+
/**
|
|
12
|
+
* Measures a component instance and returns its screen-space bounding rect.
|
|
13
|
+
* Tries multiple strategies to handle Fabric, Paper, and edge cases.
|
|
14
|
+
*/
|
|
15
|
+
export function measureInstance(instance) {
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
try {
|
|
18
|
+
// Build a list of candidates to try measuring
|
|
19
|
+
const candidates = [];
|
|
20
|
+
|
|
21
|
+
// Direct instance (ReactFabricHostComponent has measure/getBoundingClientRect)
|
|
22
|
+
if (instance) candidates.push(instance);
|
|
23
|
+
|
|
24
|
+
// Fabric: instance.canonical.publicInstance
|
|
25
|
+
if (instance?.canonical?.publicInstance) {
|
|
26
|
+
candidates.push(instance.canonical.publicInstance);
|
|
27
|
+
}
|
|
28
|
+
// Fabric fallback: instance.canonical itself
|
|
29
|
+
if (instance?.canonical && instance.canonical !== instance) {
|
|
30
|
+
candidates.push(instance.canonical);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Try getBoundingClientRect first (modern Fabric, synchronous)
|
|
34
|
+
for (const publicInstance of candidates) {
|
|
35
|
+
if (typeof publicInstance.getBoundingClientRect === "function") {
|
|
36
|
+
try {
|
|
37
|
+
const rect = publicInstance.getBoundingClientRect();
|
|
38
|
+
if (rect && typeof rect.x === "number" && typeof rect.y === "number" && typeof rect.width === "number" && typeof rect.height === "number" && rect.width > 0 && rect.height > 0) {
|
|
39
|
+
resolve({
|
|
40
|
+
x: rect.x,
|
|
41
|
+
y: rect.y,
|
|
42
|
+
width: rect.width,
|
|
43
|
+
height: rect.height
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Try next candidate
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Try measure() callback on all candidates
|
|
54
|
+
for (const publicInstance of candidates) {
|
|
55
|
+
if (typeof publicInstance.measure === "function") {
|
|
56
|
+
const timeout = setTimeout(() => resolve(null), 200);
|
|
57
|
+
publicInstance.measure((_x, _y, width, height, pageX, pageY) => {
|
|
58
|
+
clearTimeout(timeout);
|
|
59
|
+
if (pageX != null && pageY != null && width != null && height != null && width > 0 && height > 0) {
|
|
60
|
+
resolve({
|
|
61
|
+
x: pageX,
|
|
62
|
+
y: pageY,
|
|
63
|
+
width,
|
|
64
|
+
height
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
resolve(null);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Last resort: try findNodeHandle + UIManager.measure
|
|
75
|
+
try {
|
|
76
|
+
const nodeHandle = findNodeHandle(instance);
|
|
77
|
+
if (nodeHandle != null) {
|
|
78
|
+
const timeout = setTimeout(() => resolve(null), 200);
|
|
79
|
+
UIManager.measure(nodeHandle, (_x, _y, width, height, pageX, pageY) => {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
if (pageX != null && pageY != null && width != null && height != null && width > 0 && height > 0) {
|
|
82
|
+
resolve({
|
|
83
|
+
x: pageX,
|
|
84
|
+
y: pageY,
|
|
85
|
+
width,
|
|
86
|
+
height
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
resolve(null);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// findNodeHandle not available or failed
|
|
96
|
+
}
|
|
97
|
+
resolve(null);
|
|
98
|
+
} catch {
|
|
99
|
+
resolve(null);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|