@archireport/react-native-drawing 0.2.0

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.
Files changed (323) hide show
  1. package/README.md +181 -0
  2. package/lib/commonjs/DrawingEditor.js +815 -0
  3. package/lib/commonjs/DrawingEditor.js.map +1 -0
  4. package/lib/commonjs/assets/toolbar-icons/arrow-disabled.png +0 -0
  5. package/lib/commonjs/assets/toolbar-icons/arrow-enabled.png +0 -0
  6. package/lib/commonjs/assets/toolbar-icons/arrow.png +0 -0
  7. package/lib/commonjs/assets/toolbar-icons/circle-disabled.png +0 -0
  8. package/lib/commonjs/assets/toolbar-icons/circle-enabled.png +0 -0
  9. package/lib/commonjs/assets/toolbar-icons/circle.png +0 -0
  10. package/lib/commonjs/assets/toolbar-icons/freehand-disabled.png +0 -0
  11. package/lib/commonjs/assets/toolbar-icons/freehand-enabled.png +0 -0
  12. package/lib/commonjs/assets/toolbar-icons/freehand.png +0 -0
  13. package/lib/commonjs/assets/toolbar-icons/line-disabled.png +0 -0
  14. package/lib/commonjs/assets/toolbar-icons/line-enabled.png +0 -0
  15. package/lib/commonjs/assets/toolbar-icons/line.png +0 -0
  16. package/lib/commonjs/assets/toolbar-icons/measure-disabled.png +0 -0
  17. package/lib/commonjs/assets/toolbar-icons/measure-enabled.png +0 -0
  18. package/lib/commonjs/assets/toolbar-icons/measure.png +0 -0
  19. package/lib/commonjs/assets/toolbar-icons/move-disabled.png +0 -0
  20. package/lib/commonjs/assets/toolbar-icons/move-enabled.png +0 -0
  21. package/lib/commonjs/assets/toolbar-icons/move.png +0 -0
  22. package/lib/commonjs/assets/toolbar-icons/polygon-disabled.png +0 -0
  23. package/lib/commonjs/assets/toolbar-icons/polygon-enabled.png +0 -0
  24. package/lib/commonjs/assets/toolbar-icons/polygon.png +0 -0
  25. package/lib/commonjs/assets/toolbar-icons/rectangle-disabled.png +0 -0
  26. package/lib/commonjs/assets/toolbar-icons/rectangle-enabled.png +0 -0
  27. package/lib/commonjs/assets/toolbar-icons/rectangle.png +0 -0
  28. package/lib/commonjs/assets/toolbar-icons/text-disabled.png +0 -0
  29. package/lib/commonjs/assets/toolbar-icons/text-enabled.png +0 -0
  30. package/lib/commonjs/assets/toolbar-icons/text.png +0 -0
  31. package/lib/commonjs/components/ColorPalette.js +379 -0
  32. package/lib/commonjs/components/ColorPalette.js.map +1 -0
  33. package/lib/commonjs/components/LineWidthSlider.js +70 -0
  34. package/lib/commonjs/components/LineWidthSlider.js.map +1 -0
  35. package/lib/commonjs/components/MeasurementEditModal.js +153 -0
  36. package/lib/commonjs/components/MeasurementEditModal.js.map +1 -0
  37. package/lib/commonjs/components/MiniMap.js +244 -0
  38. package/lib/commonjs/components/MiniMap.js.map +1 -0
  39. package/lib/commonjs/components/TextAnnotation.js +162 -0
  40. package/lib/commonjs/components/TextAnnotation.js.map +1 -0
  41. package/lib/commonjs/components/TextEditModal.js +133 -0
  42. package/lib/commonjs/components/TextEditModal.js.map +1 -0
  43. package/lib/commonjs/components/Toolbar.js +198 -0
  44. package/lib/commonjs/components/Toolbar.js.map +1 -0
  45. package/lib/commonjs/components/ZoomBadge.js +161 -0
  46. package/lib/commonjs/components/ZoomBadge.js.map +1 -0
  47. package/lib/commonjs/hooks/useFreehandGesture.js +173 -0
  48. package/lib/commonjs/hooks/useFreehandGesture.js.map +1 -0
  49. package/lib/commonjs/hooks/usePolygonGesture.js +109 -0
  50. package/lib/commonjs/hooks/usePolygonGesture.js.map +1 -0
  51. package/lib/commonjs/hooks/useSelectionGesture.js +236 -0
  52. package/lib/commonjs/hooks/useSelectionGesture.js.map +1 -0
  53. package/lib/commonjs/hooks/useShapeGesture.js +181 -0
  54. package/lib/commonjs/hooks/useShapeGesture.js.map +1 -0
  55. package/lib/commonjs/hooks/useViewportGesture.js +238 -0
  56. package/lib/commonjs/hooks/useViewportGesture.js.map +1 -0
  57. package/lib/commonjs/index.js +104 -0
  58. package/lib/commonjs/index.js.map +1 -0
  59. package/lib/commonjs/package.json +1 -0
  60. package/lib/commonjs/renderers/ArrowRenderer.js +118 -0
  61. package/lib/commonjs/renderers/ArrowRenderer.js.map +1 -0
  62. package/lib/commonjs/renderers/CircleRenderer.js +51 -0
  63. package/lib/commonjs/renderers/CircleRenderer.js.map +1 -0
  64. package/lib/commonjs/renderers/FreehandRenderer.js +31 -0
  65. package/lib/commonjs/renderers/FreehandRenderer.js.map +1 -0
  66. package/lib/commonjs/renderers/InProgressRenderer.js +174 -0
  67. package/lib/commonjs/renderers/InProgressRenderer.js.map +1 -0
  68. package/lib/commonjs/renderers/LineRenderer.js +27 -0
  69. package/lib/commonjs/renderers/LineRenderer.js.map +1 -0
  70. package/lib/commonjs/renderers/MeasurementRenderer.js +134 -0
  71. package/lib/commonjs/renderers/MeasurementRenderer.js.map +1 -0
  72. package/lib/commonjs/renderers/ObjectRenderer.js +65 -0
  73. package/lib/commonjs/renderers/ObjectRenderer.js.map +1 -0
  74. package/lib/commonjs/renderers/PolygonRenderer.js +46 -0
  75. package/lib/commonjs/renderers/PolygonRenderer.js.map +1 -0
  76. package/lib/commonjs/renderers/RectRenderer.js +51 -0
  77. package/lib/commonjs/renderers/RectRenderer.js.map +1 -0
  78. package/lib/commonjs/renderers/SelectedObjectRenderer.js +592 -0
  79. package/lib/commonjs/renderers/SelectedObjectRenderer.js.map +1 -0
  80. package/lib/commonjs/renderers/SelectionOverlay.js +120 -0
  81. package/lib/commonjs/renderers/SelectionOverlay.js.map +1 -0
  82. package/lib/commonjs/store/useDrawingStore.js +354 -0
  83. package/lib/commonjs/store/useDrawingStore.js.map +1 -0
  84. package/lib/commonjs/types.js +6 -0
  85. package/lib/commonjs/types.js.map +1 -0
  86. package/lib/commonjs/utils/colors.js +44 -0
  87. package/lib/commonjs/utils/colors.js.map +1 -0
  88. package/lib/commonjs/utils/coordinates.js +81 -0
  89. package/lib/commonjs/utils/coordinates.js.map +1 -0
  90. package/lib/commonjs/utils/hitTesting.js +181 -0
  91. package/lib/commonjs/utils/hitTesting.js.map +1 -0
  92. package/lib/commonjs/utils/serialization.js +42 -0
  93. package/lib/commonjs/utils/serialization.js.map +1 -0
  94. package/lib/commonjs/utils/shapeDetection.js +151 -0
  95. package/lib/commonjs/utils/shapeDetection.js.map +1 -0
  96. package/lib/commonjs/utils/smoothing.js +85 -0
  97. package/lib/commonjs/utils/smoothing.js.map +1 -0
  98. package/lib/module/DrawingEditor.js +811 -0
  99. package/lib/module/DrawingEditor.js.map +1 -0
  100. package/lib/module/assets/toolbar-icons/arrow-disabled.png +0 -0
  101. package/lib/module/assets/toolbar-icons/arrow-enabled.png +0 -0
  102. package/lib/module/assets/toolbar-icons/arrow.png +0 -0
  103. package/lib/module/assets/toolbar-icons/circle-disabled.png +0 -0
  104. package/lib/module/assets/toolbar-icons/circle-enabled.png +0 -0
  105. package/lib/module/assets/toolbar-icons/circle.png +0 -0
  106. package/lib/module/assets/toolbar-icons/freehand-disabled.png +0 -0
  107. package/lib/module/assets/toolbar-icons/freehand-enabled.png +0 -0
  108. package/lib/module/assets/toolbar-icons/freehand.png +0 -0
  109. package/lib/module/assets/toolbar-icons/line-disabled.png +0 -0
  110. package/lib/module/assets/toolbar-icons/line-enabled.png +0 -0
  111. package/lib/module/assets/toolbar-icons/line.png +0 -0
  112. package/lib/module/assets/toolbar-icons/measure-disabled.png +0 -0
  113. package/lib/module/assets/toolbar-icons/measure-enabled.png +0 -0
  114. package/lib/module/assets/toolbar-icons/measure.png +0 -0
  115. package/lib/module/assets/toolbar-icons/move-disabled.png +0 -0
  116. package/lib/module/assets/toolbar-icons/move-enabled.png +0 -0
  117. package/lib/module/assets/toolbar-icons/move.png +0 -0
  118. package/lib/module/assets/toolbar-icons/polygon-disabled.png +0 -0
  119. package/lib/module/assets/toolbar-icons/polygon-enabled.png +0 -0
  120. package/lib/module/assets/toolbar-icons/polygon.png +0 -0
  121. package/lib/module/assets/toolbar-icons/rectangle-disabled.png +0 -0
  122. package/lib/module/assets/toolbar-icons/rectangle-enabled.png +0 -0
  123. package/lib/module/assets/toolbar-icons/rectangle.png +0 -0
  124. package/lib/module/assets/toolbar-icons/text-disabled.png +0 -0
  125. package/lib/module/assets/toolbar-icons/text-enabled.png +0 -0
  126. package/lib/module/assets/toolbar-icons/text.png +0 -0
  127. package/lib/module/components/ColorPalette.js +374 -0
  128. package/lib/module/components/ColorPalette.js.map +1 -0
  129. package/lib/module/components/LineWidthSlider.js +64 -0
  130. package/lib/module/components/LineWidthSlider.js.map +1 -0
  131. package/lib/module/components/MeasurementEditModal.js +148 -0
  132. package/lib/module/components/MeasurementEditModal.js.map +1 -0
  133. package/lib/module/components/MiniMap.js +239 -0
  134. package/lib/module/components/MiniMap.js.map +1 -0
  135. package/lib/module/components/TextAnnotation.js +157 -0
  136. package/lib/module/components/TextAnnotation.js.map +1 -0
  137. package/lib/module/components/TextEditModal.js +128 -0
  138. package/lib/module/components/TextEditModal.js.map +1 -0
  139. package/lib/module/components/Toolbar.js +193 -0
  140. package/lib/module/components/Toolbar.js.map +1 -0
  141. package/lib/module/components/ZoomBadge.js +155 -0
  142. package/lib/module/components/ZoomBadge.js.map +1 -0
  143. package/lib/module/hooks/useFreehandGesture.js +169 -0
  144. package/lib/module/hooks/useFreehandGesture.js.map +1 -0
  145. package/lib/module/hooks/usePolygonGesture.js +106 -0
  146. package/lib/module/hooks/usePolygonGesture.js.map +1 -0
  147. package/lib/module/hooks/useSelectionGesture.js +232 -0
  148. package/lib/module/hooks/useSelectionGesture.js.map +1 -0
  149. package/lib/module/hooks/useShapeGesture.js +177 -0
  150. package/lib/module/hooks/useShapeGesture.js.map +1 -0
  151. package/lib/module/hooks/useViewportGesture.js +234 -0
  152. package/lib/module/hooks/useViewportGesture.js.map +1 -0
  153. package/lib/module/index.js +20 -0
  154. package/lib/module/index.js.map +1 -0
  155. package/lib/module/package.json +1 -0
  156. package/lib/module/renderers/ArrowRenderer.js +113 -0
  157. package/lib/module/renderers/ArrowRenderer.js.map +1 -0
  158. package/lib/module/renderers/CircleRenderer.js +46 -0
  159. package/lib/module/renderers/CircleRenderer.js.map +1 -0
  160. package/lib/module/renderers/FreehandRenderer.js +26 -0
  161. package/lib/module/renderers/FreehandRenderer.js.map +1 -0
  162. package/lib/module/renderers/InProgressRenderer.js +169 -0
  163. package/lib/module/renderers/InProgressRenderer.js.map +1 -0
  164. package/lib/module/renderers/LineRenderer.js +22 -0
  165. package/lib/module/renderers/LineRenderer.js.map +1 -0
  166. package/lib/module/renderers/MeasurementRenderer.js +129 -0
  167. package/lib/module/renderers/MeasurementRenderer.js.map +1 -0
  168. package/lib/module/renderers/ObjectRenderer.js +60 -0
  169. package/lib/module/renderers/ObjectRenderer.js.map +1 -0
  170. package/lib/module/renderers/PolygonRenderer.js +41 -0
  171. package/lib/module/renderers/PolygonRenderer.js.map +1 -0
  172. package/lib/module/renderers/RectRenderer.js +46 -0
  173. package/lib/module/renderers/RectRenderer.js.map +1 -0
  174. package/lib/module/renderers/SelectedObjectRenderer.js +587 -0
  175. package/lib/module/renderers/SelectedObjectRenderer.js.map +1 -0
  176. package/lib/module/renderers/SelectionOverlay.js +116 -0
  177. package/lib/module/renderers/SelectionOverlay.js.map +1 -0
  178. package/lib/module/store/useDrawingStore.js +350 -0
  179. package/lib/module/store/useDrawingStore.js.map +1 -0
  180. package/lib/module/types.js +4 -0
  181. package/lib/module/types.js.map +1 -0
  182. package/lib/module/utils/colors.js +40 -0
  183. package/lib/module/utils/colors.js.map +1 -0
  184. package/lib/module/utils/coordinates.js +71 -0
  185. package/lib/module/utils/coordinates.js.map +1 -0
  186. package/lib/module/utils/hitTesting.js +171 -0
  187. package/lib/module/utils/hitTesting.js.map +1 -0
  188. package/lib/module/utils/serialization.js +36 -0
  189. package/lib/module/utils/serialization.js.map +1 -0
  190. package/lib/module/utils/shapeDetection.js +147 -0
  191. package/lib/module/utils/shapeDetection.js.map +1 -0
  192. package/lib/module/utils/smoothing.js +80 -0
  193. package/lib/module/utils/smoothing.js.map +1 -0
  194. package/lib/typescript/DrawingEditor.d.ts +3 -0
  195. package/lib/typescript/DrawingEditor.d.ts.map +1 -0
  196. package/lib/typescript/components/ColorPalette.d.ts +9 -0
  197. package/lib/typescript/components/ColorPalette.d.ts.map +1 -0
  198. package/lib/typescript/components/LineWidthSlider.d.ts +11 -0
  199. package/lib/typescript/components/LineWidthSlider.d.ts.map +1 -0
  200. package/lib/typescript/components/MeasurementEditModal.d.ts +11 -0
  201. package/lib/typescript/components/MeasurementEditModal.d.ts.map +1 -0
  202. package/lib/typescript/components/MiniMap.d.ts +23 -0
  203. package/lib/typescript/components/MiniMap.d.ts.map +1 -0
  204. package/lib/typescript/components/TextAnnotation.d.ts +22 -0
  205. package/lib/typescript/components/TextAnnotation.d.ts.map +1 -0
  206. package/lib/typescript/components/TextEditModal.d.ts +10 -0
  207. package/lib/typescript/components/TextEditModal.d.ts.map +1 -0
  208. package/lib/typescript/components/Toolbar.d.ts +13 -0
  209. package/lib/typescript/components/Toolbar.d.ts.map +1 -0
  210. package/lib/typescript/components/ZoomBadge.d.ts +9 -0
  211. package/lib/typescript/components/ZoomBadge.d.ts.map +1 -0
  212. package/lib/typescript/hooks/useFreehandGesture.d.ts +47 -0
  213. package/lib/typescript/hooks/useFreehandGesture.d.ts.map +1 -0
  214. package/lib/typescript/hooks/usePolygonGesture.d.ts +47 -0
  215. package/lib/typescript/hooks/usePolygonGesture.d.ts.map +1 -0
  216. package/lib/typescript/hooks/useSelectionGesture.d.ts +32 -0
  217. package/lib/typescript/hooks/useSelectionGesture.d.ts.map +1 -0
  218. package/lib/typescript/hooks/useShapeGesture.d.ts +54 -0
  219. package/lib/typescript/hooks/useShapeGesture.d.ts.map +1 -0
  220. package/lib/typescript/hooks/useViewportGesture.d.ts +37 -0
  221. package/lib/typescript/hooks/useViewportGesture.d.ts.map +1 -0
  222. package/lib/typescript/index.d.ts +11 -0
  223. package/lib/typescript/index.d.ts.map +1 -0
  224. package/lib/typescript/renderers/ArrowRenderer.d.ts +9 -0
  225. package/lib/typescript/renderers/ArrowRenderer.d.ts.map +1 -0
  226. package/lib/typescript/renderers/CircleRenderer.d.ts +9 -0
  227. package/lib/typescript/renderers/CircleRenderer.d.ts.map +1 -0
  228. package/lib/typescript/renderers/FreehandRenderer.d.ts +9 -0
  229. package/lib/typescript/renderers/FreehandRenderer.d.ts.map +1 -0
  230. package/lib/typescript/renderers/InProgressRenderer.d.ts +32 -0
  231. package/lib/typescript/renderers/InProgressRenderer.d.ts.map +1 -0
  232. package/lib/typescript/renderers/LineRenderer.d.ts +9 -0
  233. package/lib/typescript/renderers/LineRenderer.d.ts.map +1 -0
  234. package/lib/typescript/renderers/MeasurementRenderer.d.ts +9 -0
  235. package/lib/typescript/renderers/MeasurementRenderer.d.ts.map +1 -0
  236. package/lib/typescript/renderers/ObjectRenderer.d.ts +12 -0
  237. package/lib/typescript/renderers/ObjectRenderer.d.ts.map +1 -0
  238. package/lib/typescript/renderers/PolygonRenderer.d.ts +13 -0
  239. package/lib/typescript/renderers/PolygonRenderer.d.ts.map +1 -0
  240. package/lib/typescript/renderers/RectRenderer.d.ts +9 -0
  241. package/lib/typescript/renderers/RectRenderer.d.ts.map +1 -0
  242. package/lib/typescript/renderers/SelectedObjectRenderer.d.ts +18 -0
  243. package/lib/typescript/renderers/SelectedObjectRenderer.d.ts.map +1 -0
  244. package/lib/typescript/renderers/SelectionOverlay.d.ts +21 -0
  245. package/lib/typescript/renderers/SelectionOverlay.d.ts.map +1 -0
  246. package/lib/typescript/store/useDrawingStore.d.ts +30 -0
  247. package/lib/typescript/store/useDrawingStore.d.ts.map +1 -0
  248. package/lib/typescript/types.d.ts +130 -0
  249. package/lib/typescript/types.d.ts.map +1 -0
  250. package/lib/typescript/utils/colors.d.ts +11 -0
  251. package/lib/typescript/utils/colors.d.ts.map +1 -0
  252. package/lib/typescript/utils/coordinates.d.ts +34 -0
  253. package/lib/typescript/utils/coordinates.d.ts.map +1 -0
  254. package/lib/typescript/utils/hitTesting.d.ts +18 -0
  255. package/lib/typescript/utils/hitTesting.d.ts.map +1 -0
  256. package/lib/typescript/utils/serialization.d.ts +17 -0
  257. package/lib/typescript/utils/serialization.d.ts.map +1 -0
  258. package/lib/typescript/utils/shapeDetection.d.ts +22 -0
  259. package/lib/typescript/utils/shapeDetection.d.ts.map +1 -0
  260. package/lib/typescript/utils/smoothing.d.ts +16 -0
  261. package/lib/typescript/utils/smoothing.d.ts.map +1 -0
  262. package/package.json +108 -0
  263. package/src/DrawingEditor.tsx +1071 -0
  264. package/src/assets/toolbar-icons/arrow-disabled.png +0 -0
  265. package/src/assets/toolbar-icons/arrow-enabled.png +0 -0
  266. package/src/assets/toolbar-icons/arrow.png +0 -0
  267. package/src/assets/toolbar-icons/circle-disabled.png +0 -0
  268. package/src/assets/toolbar-icons/circle-enabled.png +0 -0
  269. package/src/assets/toolbar-icons/circle.png +0 -0
  270. package/src/assets/toolbar-icons/freehand-disabled.png +0 -0
  271. package/src/assets/toolbar-icons/freehand-enabled.png +0 -0
  272. package/src/assets/toolbar-icons/freehand.png +0 -0
  273. package/src/assets/toolbar-icons/line-disabled.png +0 -0
  274. package/src/assets/toolbar-icons/line-enabled.png +0 -0
  275. package/src/assets/toolbar-icons/line.png +0 -0
  276. package/src/assets/toolbar-icons/measure-disabled.png +0 -0
  277. package/src/assets/toolbar-icons/measure-enabled.png +0 -0
  278. package/src/assets/toolbar-icons/measure.png +0 -0
  279. package/src/assets/toolbar-icons/move-disabled.png +0 -0
  280. package/src/assets/toolbar-icons/move-enabled.png +0 -0
  281. package/src/assets/toolbar-icons/move.png +0 -0
  282. package/src/assets/toolbar-icons/polygon-disabled.png +0 -0
  283. package/src/assets/toolbar-icons/polygon-enabled.png +0 -0
  284. package/src/assets/toolbar-icons/polygon.png +0 -0
  285. package/src/assets/toolbar-icons/rectangle-disabled.png +0 -0
  286. package/src/assets/toolbar-icons/rectangle-enabled.png +0 -0
  287. package/src/assets/toolbar-icons/rectangle.png +0 -0
  288. package/src/assets/toolbar-icons/text-disabled.png +0 -0
  289. package/src/assets/toolbar-icons/text-enabled.png +0 -0
  290. package/src/assets/toolbar-icons/text.png +0 -0
  291. package/src/components/ColorPalette.tsx +497 -0
  292. package/src/components/LineWidthSlider.tsx +87 -0
  293. package/src/components/MeasurementEditModal.tsx +163 -0
  294. package/src/components/MiniMap.tsx +275 -0
  295. package/src/components/TextAnnotation.tsx +198 -0
  296. package/src/components/TextEditModal.tsx +139 -0
  297. package/src/components/Toolbar.tsx +254 -0
  298. package/src/components/ZoomBadge.tsx +166 -0
  299. package/src/hooks/useFreehandGesture.ts +249 -0
  300. package/src/hooks/usePolygonGesture.ts +162 -0
  301. package/src/hooks/useSelectionGesture.ts +293 -0
  302. package/src/hooks/useShapeGesture.ts +256 -0
  303. package/src/hooks/useViewportGesture.ts +337 -0
  304. package/src/index.tsx +51 -0
  305. package/src/renderers/ArrowRenderer.tsx +123 -0
  306. package/src/renderers/CircleRenderer.tsx +60 -0
  307. package/src/renderers/FreehandRenderer.tsx +33 -0
  308. package/src/renderers/InProgressRenderer.tsx +217 -0
  309. package/src/renderers/LineRenderer.tsx +34 -0
  310. package/src/renderers/MeasurementRenderer.tsx +179 -0
  311. package/src/renderers/ObjectRenderer.tsx +42 -0
  312. package/src/renderers/PolygonRenderer.tsx +66 -0
  313. package/src/renderers/RectRenderer.tsx +60 -0
  314. package/src/renderers/SelectedObjectRenderer.tsx +738 -0
  315. package/src/renderers/SelectionOverlay.tsx +170 -0
  316. package/src/store/useDrawingStore.ts +357 -0
  317. package/src/types.ts +186 -0
  318. package/src/utils/colors.ts +98 -0
  319. package/src/utils/coordinates.ts +75 -0
  320. package/src/utils/hitTesting.ts +242 -0
  321. package/src/utils/serialization.ts +45 -0
  322. package/src/utils/shapeDetection.ts +160 -0
  323. package/src/utils/smoothing.ts +84 -0
@@ -0,0 +1,1071 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ forwardRef,
9
+ } from "react";
10
+ import { View, StyleSheet, Platform, useWindowDimensions, type LayoutChangeEvent } from "react-native";
11
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
12
+ import {
13
+ Canvas,
14
+ Image,
15
+ useImage,
16
+ useCanvasRef,
17
+ Fill,
18
+ Group,
19
+ rect,
20
+ } from "@shopify/react-native-skia";
21
+ import {
22
+ GestureHandlerRootView,
23
+ GestureDetector,
24
+ Gesture,
25
+ } from "react-native-gesture-handler";
26
+ import {
27
+ useSharedValue,
28
+ useDerivedValue,
29
+ } from "react-native-reanimated";
30
+ import type { ViewStyle } from "react-native";
31
+
32
+ import type {
33
+ DrawingEditorProps,
34
+ DrawingEditorRef,
35
+ ToolType,
36
+ TextObject,
37
+ MeasureObject,
38
+ TwoPointShapeType,
39
+ Size,
40
+ } from "./types";
41
+ import { useDrawingStore } from "./store/useDrawingStore";
42
+ import { useFreehandGesture } from "./hooks/useFreehandGesture";
43
+ import { useShapeGesture } from "./hooks/useShapeGesture";
44
+ import { usePolygonGesture } from "./hooks/usePolygonGesture";
45
+ import { useSelectionGesture } from "./hooks/useSelectionGesture";
46
+ import { useViewportGesture } from "./hooks/useViewportGesture";
47
+ import { ObjectRenderer } from "./renderers/ObjectRenderer";
48
+ import { SelectionOverlay } from "./renderers/SelectionOverlay";
49
+ import { SelectedObjectRenderer } from "./renderers/SelectedObjectRenderer";
50
+ import {
51
+ FreehandInProgress,
52
+ ShapeInProgress,
53
+ PolygonInProgress,
54
+ } from "./renderers/InProgressRenderer";
55
+ import { Toolbar } from "./components/Toolbar";
56
+ import { ColorPalette } from "./components/ColorPalette";
57
+ import { LineWidthSlider } from "./components/LineWidthSlider";
58
+ import { TextAnnotation } from "./components/TextAnnotation";
59
+ import { TextEditModal } from "./components/TextEditModal";
60
+ import { MeasurementEditModal } from "./components/MeasurementEditModal";
61
+ import { ZoomBadge } from "./components/ZoomBadge";
62
+ import { MiniMap } from "./components/MiniMap";
63
+ import { normalize } from "./utils/coordinates";
64
+ import { generateId } from "./utils/serialization";
65
+
66
+ const TWO_POINT_TOOLS: TwoPointShapeType[] = [
67
+ "line",
68
+ "arrow",
69
+ "rectangle",
70
+ "circle",
71
+ "measure",
72
+ ];
73
+
74
+ const WEB_CURSOR_STYLES = {
75
+ default: { cursor: "default" } as unknown as ViewStyle,
76
+ crosshair: { cursor: "crosshair" } as unknown as ViewStyle,
77
+ grab: { cursor: "grab" } as unknown as ViewStyle,
78
+ grabbing: { cursor: "grabbing" } as unknown as ViewStyle,
79
+ text: { cursor: "text" } as unknown as ViewStyle,
80
+ };
81
+
82
+ interface WebWheelEventLike {
83
+ ctrlKey?: boolean;
84
+ deltaY?: number;
85
+ clientX?: number;
86
+ clientY?: number;
87
+ preventDefault?: () => void;
88
+ currentTarget?: {
89
+ getBoundingClientRect?: () => {
90
+ left: number;
91
+ top: number;
92
+ };
93
+ };
94
+ nativeEvent?: {
95
+ ctrlKey?: boolean;
96
+ deltaY?: number;
97
+ clientX?: number;
98
+ clientY?: number;
99
+ preventDefault?: () => void;
100
+ };
101
+ }
102
+
103
+ interface WebWheelTarget {
104
+ addEventListener?: (
105
+ type: "wheel",
106
+ listener: (event: WebWheelEventLike) => void,
107
+ options?: { passive: boolean },
108
+ ) => void;
109
+ removeEventListener?: (
110
+ type: "wheel",
111
+ listener: (event: WebWheelEventLike) => void,
112
+ options?: { passive: boolean },
113
+ ) => void;
114
+ }
115
+
116
+ export const DrawingEditor = forwardRef<DrawingEditorRef, DrawingEditorProps>(
117
+ function DrawingEditor(
118
+ {
119
+ imageSource,
120
+ initialObjects,
121
+ maxZoom,
122
+ onSave: _onSave,
123
+ onDismiss: _onDismiss,
124
+ style,
125
+ renderToolbar,
126
+ renderColorPalette,
127
+ },
128
+ ref,
129
+ ) {
130
+ // ─── Phone vs tablet detection ───────────────────────────────────────────
131
+ const { width: winWidth, height: winHeight } = useWindowDimensions();
132
+ const isPhone = Math.min(winWidth, winHeight) < 768;
133
+
134
+ const insets = useSafeAreaInsets();
135
+
136
+ // ─── Store ───────────────────────────────────────────────────────────────
137
+ const currentTool = useDrawingStore((s) => s.currentTool);
138
+ const strokeColor = useDrawingStore((s) => s.strokeColor);
139
+ const fillColor = useDrawingStore((s) => s.fillColor);
140
+ const fillAlpha = useDrawingStore((s) => s.fillAlpha);
141
+ const lineWidth = useDrawingStore((s) => s.lineWidth);
142
+ const objects = useDrawingStore((s) => s.objects);
143
+ const selectedObjectId = useDrawingStore((s) => s.selectedObjectId);
144
+ const canvasSize = useDrawingStore((s) => s.canvasSize);
145
+ const undoStack = useDrawingStore((s) => s.undoStack);
146
+
147
+ const setTool = useDrawingStore((s) => s.setTool);
148
+ const setStrokeColor = useDrawingStore((s) => s.setStrokeColor);
149
+ const setLineWidth = useDrawingStore((s) => s.setLineWidth);
150
+ const setCanvasSize = useDrawingStore((s) => s.setCanvasSize);
151
+ const addObject = useDrawingStore((s) => s.addObject);
152
+ const updateObject = useDrawingStore((s) => s.updateObject);
153
+ const deleteObject = useDrawingStore((s) => s.deleteObject);
154
+ const selectObject = useDrawingStore((s) => s.selectObject);
155
+ const moveObject = useDrawingStore((s) => s.moveObject);
156
+ const undo = useDrawingStore((s) => s.undo);
157
+ const clearAll = useDrawingStore((s) => s.clearAll);
158
+ const loadObjects = useDrawingStore((s) => s.loadObjects);
159
+
160
+ // ─── Image ───────────────────────────────────────────────────────────────
161
+ const skiaImage = useImage(
162
+ typeof imageSource === "string" ? imageSource : imageSource,
163
+ (err) => console.warn("[DrawingEditor] Failed to load image:", err.message),
164
+ );
165
+ const canvasRef = useCanvasRef();
166
+
167
+ // ─── Layout ──────────────────────────────────────────────────────────────
168
+ const [layoutSize, setLayoutSize] = useState<Size>({ width: 0, height: 0 });
169
+ const [isPointerDown, setIsPointerDown] = useState(false);
170
+ const wheelTargetRef = useRef<WebWheelTarget | null>(null);
171
+
172
+ const setWheelTargetRef = useCallback((node: unknown) => {
173
+ wheelTargetRef.current = node as WebWheelTarget | null;
174
+ }, []);
175
+
176
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
177
+ const { width, height } = event.nativeEvent.layout;
178
+ setLayoutSize({ width, height });
179
+ }, []);
180
+
181
+ // Reset store when component mounts (new image selected)
182
+ useEffect(() => {
183
+ clearAll();
184
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
185
+
186
+ // Load initial objects once canvas is sized
187
+ useEffect(() => {
188
+ if (initialObjects && canvasSize.width > 0) {
189
+ loadObjects(initialObjects);
190
+ }
191
+ }, [canvasSize.width > 0]); // eslint-disable-line react-hooks/exhaustive-deps
192
+
193
+ // ─── Shared values synced with store ─────────────────────────────────────
194
+ const colorSV = useSharedValue(strokeColor);
195
+ const lineWidthSV = useSharedValue(lineWidth);
196
+ const fillColorSV = useSharedValue(fillColor);
197
+ const fillAlphaSV = useSharedValue(fillAlpha);
198
+ const toolSV = useSharedValue<ToolType | null>(currentTool);
199
+
200
+ useEffect(() => {
201
+ colorSV.value = strokeColor;
202
+ }, [strokeColor]);
203
+ useEffect(() => {
204
+ lineWidthSV.value = lineWidth;
205
+ }, [lineWidth]);
206
+ useEffect(() => {
207
+ fillColorSV.value = fillColor;
208
+ }, [fillColor]);
209
+ useEffect(() => {
210
+ fillAlphaSV.value = fillAlpha;
211
+ }, [fillAlpha]);
212
+ useEffect(() => {
213
+ toolSV.value = currentTool;
214
+ }, [currentTool]);
215
+
216
+ // ─── Determine active tool shape type ───────────────────────────────────
217
+ const isFreehand = currentTool === "freehand";
218
+ const isPolygon = currentTool === "polygon";
219
+ const isTwoPointShape = TWO_POINT_TOOLS.includes(
220
+ currentTool as TwoPointShapeType,
221
+ );
222
+ const isTextMode = currentTool === "text";
223
+
224
+ // ─── Image fit calculation ───────────────────────────────────────────────
225
+ const imageRect = useMemo(() => {
226
+ if (!skiaImage || layoutSize.width === 0 || layoutSize.height === 0) {
227
+ return {
228
+ x: 0,
229
+ y: 0,
230
+ width: layoutSize.width,
231
+ height: layoutSize.height,
232
+ };
233
+ }
234
+ const imgW = skiaImage.width();
235
+ const imgH = skiaImage.height();
236
+ const scaleX = layoutSize.width / imgW;
237
+ const scaleY = layoutSize.height / imgH;
238
+ const scale = Math.min(scaleX, scaleY);
239
+ const w = imgW * scale;
240
+ const h = imgH * scale;
241
+ return {
242
+ x: (layoutSize.width - w) / 2,
243
+ y: (layoutSize.height - h) / 2,
244
+ width: w,
245
+ height: h,
246
+ };
247
+ }, [skiaImage, layoutSize]);
248
+
249
+ // ─── Effective canvas size = image rect ─────────────────────────────────
250
+ const effectiveCanvasSize = useMemo<Size>(
251
+ () => ({
252
+ width: imageRect.width,
253
+ height: imageRect.height,
254
+ }),
255
+ [imageRect.width, imageRect.height],
256
+ );
257
+
258
+ // Sync to store for external consumers
259
+ useEffect(() => {
260
+ if (imageRect.width > 0 && imageRect.height > 0) {
261
+ setCanvasSize({ width: imageRect.width, height: imageRect.height });
262
+ }
263
+ }, [imageRect.width, imageRect.height, setCanvasSize]);
264
+
265
+ // ─── Image bounds shared values (for worklet access) ─────────────────────
266
+ const imageOffsetXSV = useSharedValue(0);
267
+ const imageOffsetYSV = useSharedValue(0);
268
+ const imageWidthSV = useSharedValue(0);
269
+ const imageHeightSV = useSharedValue(0);
270
+
271
+ useEffect(() => {
272
+ imageOffsetXSV.value = imageRect.x;
273
+ imageOffsetYSV.value = imageRect.y;
274
+ imageWidthSV.value = imageRect.width;
275
+ imageHeightSV.value = imageRect.height;
276
+ }, [imageRect]);
277
+
278
+ // ─── Viewport (zoom/pan) ─────────────────────────────────────────────────
279
+ const [isZoomed, setIsZoomed] = useState(false);
280
+
281
+ const layoutWidthSV = useSharedValue(0);
282
+ const layoutHeightSV = useSharedValue(0);
283
+
284
+ useEffect(() => {
285
+ layoutWidthSV.value = layoutSize.width;
286
+ layoutHeightSV.value = layoutSize.height;
287
+ }, [layoutSize]);
288
+
289
+ const handlePinchEnd = useCallback(() => {
290
+ setTool(null);
291
+ }, [setTool]);
292
+
293
+ const viewport = useViewportGesture({
294
+ maxZoom,
295
+ layoutWidth: layoutWidthSV,
296
+ layoutHeight: layoutHeightSV,
297
+ imageOffsetX: imageOffsetXSV,
298
+ imageOffsetY: imageOffsetYSV,
299
+ imageWidth: imageWidthSV,
300
+ imageHeight: imageHeightSV,
301
+ onPinchEnd: handlePinchEnd,
302
+ });
303
+
304
+ // Track isZoomed state for UI.
305
+ // Only tracks zoom level — tool deactivation is handled by onPinchEnd.
306
+ useEffect(() => {
307
+ const id = setInterval(() => {
308
+ const zoomed = viewport.scale.value > 1.01;
309
+ setIsZoomed((prev) => {
310
+ if (prev !== zoomed) return zoomed;
311
+ return prev;
312
+ });
313
+ }, 200);
314
+ return () => clearInterval(id);
315
+ }, [viewport.scale, viewport.isPinching]);
316
+
317
+ // ─── Measurement editing modal ───────────────────────────────────────────
318
+ const [measureEditVisible, setMeasureEditVisible] = useState(false);
319
+ const [measureEditTarget, setMeasureEditTarget] = useState<string | null>(
320
+ null,
321
+ );
322
+ const lastUsedUnit = useRef("cm");
323
+
324
+ const handleMeasureCreated = useCallback(
325
+ (id: string) => {
326
+ setMeasureEditTarget(id);
327
+ setMeasureEditVisible(true);
328
+ },
329
+ [],
330
+ );
331
+
332
+ const handleMeasureSubmit = useCallback(
333
+ (value: string, unit: string) => {
334
+ if (measureEditTarget) {
335
+ updateObject(measureEditTarget, {
336
+ text: value,
337
+ unit,
338
+ } as Partial<MeasureObject>);
339
+ }
340
+ lastUsedUnit.current = unit;
341
+ setMeasureEditVisible(false);
342
+ setMeasureEditTarget(null);
343
+ },
344
+ [measureEditTarget, updateObject],
345
+ );
346
+
347
+ const handleMeasureCancel = useCallback(() => {
348
+ if (measureEditTarget) {
349
+ deleteObject(measureEditTarget);
350
+ }
351
+ setMeasureEditVisible(false);
352
+ setMeasureEditTarget(null);
353
+ }, [measureEditTarget, deleteObject]);
354
+
355
+ // ─── Gesture: Freehand ───────────────────────────────────────────────────
356
+ const freehand = useFreehandGesture({
357
+ colorSV,
358
+ lineWidthSV,
359
+ fillColorSV,
360
+ fillAlphaSV,
361
+ canvasSize: effectiveCanvasSize,
362
+ enabled: isFreehand && !selectedObjectId,
363
+ imageOffsetX: imageOffsetXSV,
364
+ imageOffsetY: imageOffsetYSV,
365
+ imageWidth: imageWidthSV,
366
+ imageHeight: imageHeightSV,
367
+ viewScale: viewport.scale,
368
+ viewTranslateX: viewport.translateX,
369
+ viewTranslateY: viewport.translateY,
370
+ });
371
+
372
+ // ─── Gesture: Shape ──────────────────────────────────────────────────────
373
+ const shape = useShapeGesture({
374
+ shapeType: (isTwoPointShape ? currentTool : "line") as TwoPointShapeType,
375
+ colorSV,
376
+ lineWidthSV,
377
+ fillColorSV,
378
+ fillAlphaSV,
379
+ canvasSize: effectiveCanvasSize,
380
+ enabled: isTwoPointShape && !selectedObjectId,
381
+ imageOffsetX: imageOffsetXSV,
382
+ imageOffsetY: imageOffsetYSV,
383
+ imageWidth: imageWidthSV,
384
+ imageHeight: imageHeightSV,
385
+ viewScale: viewport.scale,
386
+ viewTranslateX: viewport.translateX,
387
+ viewTranslateY: viewport.translateY,
388
+ onMeasureCreated: handleMeasureCreated,
389
+ });
390
+
391
+ // ─── Gesture: Polygon ────────────────────────────────────────────────────
392
+ const polygon = usePolygonGesture({
393
+ colorSV,
394
+ lineWidthSV,
395
+ fillColorSV,
396
+ fillAlphaSV,
397
+ canvasSize: effectiveCanvasSize,
398
+ enabled: isPolygon && !selectedObjectId,
399
+ imageOffsetX: imageOffsetXSV,
400
+ imageOffsetY: imageOffsetYSV,
401
+ imageWidth: imageWidthSV,
402
+ imageHeight: imageHeightSV,
403
+ viewScale: viewport.scale,
404
+ viewTranslateX: viewport.translateX,
405
+ viewTranslateY: viewport.translateY,
406
+ });
407
+
408
+ // Cancel polygon construction when switching away from polygon tool
409
+ const prevToolRef = useRef(currentTool);
410
+ useEffect(() => {
411
+ if (prevToolRef.current === "polygon" && currentTool !== "polygon") {
412
+ polygon.cancel();
413
+ }
414
+ prevToolRef.current = currentTool;
415
+ }, [currentTool]); // eslint-disable-line react-hooks/exhaustive-deps
416
+
417
+ // ─── Gesture: Text placement (on empty space tap) ────────────────────────
418
+ const handleTextTap = useCallback(
419
+ (x: number, y: number) => {
420
+ const pos = normalize({ x, y }, effectiveCanvasSize);
421
+ const newText: TextObject = {
422
+ id: generateId(),
423
+ type: "text",
424
+ position: pos,
425
+ width: 0.2, // 20% of canvas
426
+ height: 0.08,
427
+ value: "Text",
428
+ color: strokeColor,
429
+ lineWidth,
430
+ };
431
+ addObject(newText);
432
+ setTextEditTarget(newText.id);
433
+ setTextEditValue("Text");
434
+ setTextEditVisible(true);
435
+ },
436
+ [effectiveCanvasSize, strokeColor, lineWidth, addObject],
437
+ );
438
+
439
+ // ─── Handle tap on empty space (deselect or place text) ──────────────────
440
+ const handleTapEmpty = useCallback(
441
+ (x: number, y: number) => {
442
+ if (isTextMode) {
443
+ handleTextTap(x, y);
444
+ } else {
445
+ selectObject(null);
446
+ }
447
+ },
448
+ [isTextMode, handleTextTap, selectObject],
449
+ );
450
+
451
+ // ─── Gesture: Selection (always active) ──────────────────────────────────
452
+ const selection = useSelectionGesture({
453
+ canvasSize: effectiveCanvasSize,
454
+ imageOffsetX: imageOffsetXSV,
455
+ imageOffsetY: imageOffsetYSV,
456
+ viewScale: viewport.scale,
457
+ viewTranslateX: viewport.translateX,
458
+ viewTranslateY: viewport.translateY,
459
+ onTapEmpty: handleTapEmpty,
460
+ });
461
+
462
+ // ─── Compose all gestures ────────────────────────────────────────────────
463
+ const composedGesture = useMemo(() => {
464
+ // Navigation gestures (pinch + two-finger pan) run simultaneously
465
+ const navigationGestures = Gesture.Simultaneous(
466
+ viewport.pinchGesture,
467
+ viewport.twoFingerPanGesture,
468
+ );
469
+
470
+ // Drawing/selection pan gestures — these must NOT be delayed by tap recognition
471
+ const noToolActive = currentTool === null;
472
+ const drawingPanGestures = selectedObjectId
473
+ ? selection.panGesture
474
+ : noToolActive && isZoomed
475
+ ? viewport.singleFingerPanGesture
476
+ : isFreehand
477
+ ? freehand.gesture
478
+ : isTwoPointShape
479
+ ? shape.gesture
480
+ : selection.panGesture;
481
+
482
+ // Tap gestures (double-tap and single-tap) — run in Exclusive so
483
+ // double-tap has priority over single-tap, but they don't block pan
484
+ const tapGestures = Gesture.Exclusive(
485
+ viewport.doubleTapResetGesture,
486
+ // Polygon tap has priority over selection tap when polygon tool is active
487
+ ...(isPolygon
488
+ ? [polygon.gesture]
489
+ : [selection.doubleTapGesture, selection.tapGesture]),
490
+ );
491
+
492
+ // Pan and tap gestures run simultaneously — pan starts immediately
493
+ // without waiting for tap recognition timeout
494
+ const interactionGestures = Gesture.Simultaneous(
495
+ drawingPanGestures,
496
+ tapGestures,
497
+ );
498
+
499
+ // Navigation and interaction run simultaneously
500
+ return Gesture.Simultaneous(navigationGestures, interactionGestures);
501
+ }, [
502
+ viewport.pinchGesture,
503
+ viewport.twoFingerPanGesture,
504
+ viewport.doubleTapResetGesture,
505
+ viewport.singleFingerPanGesture,
506
+ selection.doubleTapGesture,
507
+ selection.panGesture,
508
+ selection.tapGesture,
509
+ polygon.gesture,
510
+ freehand.gesture,
511
+ shape.gesture,
512
+ currentTool,
513
+ selectedObjectId,
514
+ isZoomed,
515
+ isFreehand,
516
+ isTwoPointShape,
517
+ isPolygon,
518
+ ]);
519
+
520
+ // ─── Text editing modal ──────────────────────────────────────────────────
521
+ const [textEditVisible, setTextEditVisible] = useState(false);
522
+ const [textEditTarget, setTextEditTarget] = useState<string | null>(null);
523
+ const [textEditValue, setTextEditValue] = useState("");
524
+
525
+ const handleTextSubmit = useCallback(
526
+ (value: string) => {
527
+ if (textEditTarget) {
528
+ updateObject(textEditTarget, { value } as Partial<TextObject>);
529
+ }
530
+ setTextEditVisible(false);
531
+ setTextEditTarget(null);
532
+ },
533
+ [textEditTarget, updateObject],
534
+ );
535
+
536
+ const handleTextDoubleTap = useCallback(
537
+ (id: string) => {
538
+ const obj = objects.find((o) => o.id === id);
539
+ if (obj?.type === "text") {
540
+ setTextEditTarget(id);
541
+ setTextEditValue(obj.value);
542
+ setTextEditVisible(true);
543
+ }
544
+ },
545
+ [objects],
546
+ );
547
+
548
+ // ─── Computed values ─────────────────────────────────────────────────────
549
+ const selectedObject = useMemo(
550
+ () =>
551
+ selectedObjectId
552
+ ? (objects.find((o) => o.id === selectedObjectId) ?? null)
553
+ : null,
554
+ [objects, selectedObjectId],
555
+ );
556
+
557
+ const textObjects = useMemo(
558
+ () => objects.filter((o): o is TextObject => o.type === "text"),
559
+ [objects],
560
+ );
561
+
562
+ const nonTextObjects = useMemo(
563
+ () => objects.filter((o) => o.type !== "text"),
564
+ [objects],
565
+ );
566
+
567
+ const unselectedNonTextObjects = useMemo(
568
+ () => nonTextObjects.filter((o) => o.id !== selectedObjectId),
569
+ [nonTextObjects, selectedObjectId],
570
+ );
571
+
572
+ // ─── Imperative ref ──────────────────────────────────────────────────────
573
+ useImperativeHandle(
574
+ ref,
575
+ () => ({
576
+ undo,
577
+ setTool,
578
+ exportImage: async () => {
579
+ // Reset viewport to identity for a clean capture
580
+ const prevScale = viewport.scale.value;
581
+ const prevTx = viewport.translateX.value;
582
+ const prevTy = viewport.translateY.value;
583
+ viewport.scale.value = 1;
584
+ viewport.translateX.value = 0;
585
+ viewport.translateY.value = 0;
586
+
587
+ // Wait a frame for Skia to re-render
588
+ await new Promise((r) => requestAnimationFrame(r));
589
+
590
+ const bounds = {
591
+ x: Math.round(imageRect.x),
592
+ y: Math.round(imageRect.y),
593
+ width: Math.round(imageRect.width),
594
+ height: Math.round(imageRect.height),
595
+ };
596
+ const image = canvasRef.current?.makeImageSnapshot(bounds);
597
+
598
+ // Restore viewport
599
+ viewport.scale.value = prevScale;
600
+ viewport.translateX.value = prevTx;
601
+ viewport.translateY.value = prevTy;
602
+
603
+ if (!image) return null;
604
+ return image.encodeToBytes();
605
+ },
606
+ exportImageBase64: async () => {
607
+ // Reset viewport to identity for a clean capture
608
+ const prevScale = viewport.scale.value;
609
+ const prevTx = viewport.translateX.value;
610
+ const prevTy = viewport.translateY.value;
611
+ viewport.scale.value = 1;
612
+ viewport.translateX.value = 0;
613
+ viewport.translateY.value = 0;
614
+
615
+ await new Promise((r) => requestAnimationFrame(r));
616
+
617
+ const bounds = {
618
+ x: Math.round(imageRect.x),
619
+ y: Math.round(imageRect.y),
620
+ width: Math.round(imageRect.width),
621
+ height: Math.round(imageRect.height),
622
+ };
623
+ const image = canvasRef.current?.makeImageSnapshot(bounds);
624
+
625
+ viewport.scale.value = prevScale;
626
+ viewport.translateX.value = prevTx;
627
+ viewport.translateY.value = prevTy;
628
+
629
+ if (!image) return null;
630
+ return image.encodeToBase64();
631
+ },
632
+ getObjects: () => objects,
633
+ clearAll,
634
+ resetZoom: viewport.resetViewport,
635
+ }),
636
+ [undo, setTool, objects, clearAll, imageRect, viewport],
637
+ );
638
+
639
+ // ─── Toolbar callbacks ───────────────────────────────────────────────────
640
+ const handleSelectTool = useCallback(
641
+ (tool: ToolType | null) => {
642
+ setTool(tool);
643
+ },
644
+ [setTool],
645
+ );
646
+
647
+ const handleUndo = useCallback(() => {
648
+ undo();
649
+ }, [undo]);
650
+
651
+ const handleResetZoom = useCallback(() => {
652
+ viewport.resetViewport();
653
+ }, [viewport]);
654
+
655
+ // ─── Viewport transform for Skia (animated) ─────────────────────────────
656
+ const viewportTransform = useDerivedValue(() => [
657
+ { translateX: viewport.translateX.value },
658
+ { translateY: viewport.translateY.value },
659
+ { scale: viewport.scale.value },
660
+ ]);
661
+
662
+ const usesMoveCursor = selectedObjectId !== null || currentTool === null;
663
+
664
+ const handlePointerDown = useCallback(() => {
665
+ if (Platform.OS === "web" && usesMoveCursor) {
666
+ setIsPointerDown(true);
667
+ }
668
+ }, [usesMoveCursor]);
669
+
670
+ const handlePointerRelease = useCallback(() => {
671
+ if (Platform.OS === "web") {
672
+ setIsPointerDown(false);
673
+ }
674
+ }, []);
675
+
676
+ const handleWheel = useCallback(
677
+ (event: WebWheelEventLike) => {
678
+ const wheelEvent = event.nativeEvent ?? event;
679
+ if (!wheelEvent.ctrlKey || wheelEvent.deltaY === undefined) return;
680
+
681
+ event.preventDefault?.();
682
+ wheelEvent.preventDefault?.();
683
+
684
+ const bounds = event.currentTarget?.getBoundingClientRect?.();
685
+ if (!bounds) return;
686
+
687
+ const clientX = wheelEvent.clientX ?? event.clientX;
688
+ const clientY = wheelEvent.clientY ?? event.clientY;
689
+ if (clientX === undefined || clientY === undefined) return;
690
+
691
+ const localX = clientX - bounds.left;
692
+ const localY = clientY - bounds.top;
693
+ const scaleFactor = Math.min(
694
+ 1.25,
695
+ Math.max(0.8, Math.exp(-wheelEvent.deltaY * 0.01)),
696
+ );
697
+
698
+ viewport.zoomAt(localX, localY, scaleFactor);
699
+ },
700
+ [viewport],
701
+ );
702
+
703
+ useEffect(() => {
704
+ if (Platform.OS !== "web") return;
705
+
706
+ const target = wheelTargetRef.current;
707
+ if (!target?.addEventListener || !target.removeEventListener) return;
708
+
709
+ const options = { passive: false };
710
+ target.addEventListener("wheel", handleWheel, options);
711
+ return () => {
712
+ target.removeEventListener?.("wheel", handleWheel, options);
713
+ };
714
+ }, [handleWheel]);
715
+
716
+ const webPointerHandlers =
717
+ Platform.OS === "web"
718
+ ? {
719
+ onPointerDown: handlePointerDown,
720
+ onPointerUp: handlePointerRelease,
721
+ onPointerCancel: handlePointerRelease,
722
+ onPointerLeave: handlePointerRelease,
723
+ }
724
+ : undefined;
725
+
726
+ const canvasCursorStyle = useMemo<ViewStyle | undefined>(() => {
727
+ if (Platform.OS !== "web") return undefined;
728
+ if (usesMoveCursor && isPointerDown) {
729
+ return WEB_CURSOR_STYLES.grabbing;
730
+ }
731
+ if (usesMoveCursor) {
732
+ return WEB_CURSOR_STYLES.grab;
733
+ }
734
+ if (isTextMode) return WEB_CURSOR_STYLES.text;
735
+ if (currentTool) return WEB_CURSOR_STYLES.crosshair;
736
+ return WEB_CURSOR_STYLES.default;
737
+ }, [currentTool, isPointerDown, isTextMode, usesMoveCursor]);
738
+
739
+ // ─── Render ──────────────────────────────────────────────────────────────
740
+ return (
741
+ <GestureHandlerRootView style={[styles.root, style]}>
742
+ <View style={[styles.editorContainer, isPhone ? styles.editorContainerPhone : styles.editorContainerTablet]}>
743
+ {/* ── Left: Color Palette (tablet only) ──────────────────────── */}
744
+ {!isPhone && (
745
+ <View style={styles.sidebarLeft}>
746
+ {renderColorPalette ? (
747
+ renderColorPalette({
748
+ strokeColor,
749
+ fillColor,
750
+ fillAlpha,
751
+ onSelectStrokeColor: setStrokeColor,
752
+ onSelectFillColor: useDrawingStore.getState().setFillColor,
753
+ onSelectFillAlpha: useDrawingStore.getState().setFillAlpha,
754
+ })
755
+ ) : (
756
+ <ColorPalette
757
+ strokeColor={strokeColor}
758
+ onSelectColor={setStrokeColor}
759
+ direction="vertical"
760
+ />
761
+ )}
762
+ </View>
763
+ )}
764
+
765
+ {/* ── Center: Canvas ──────────────────────────────────────────── */}
766
+ <View style={styles.canvasContainer} onLayout={handleLayout}>
767
+ <GestureDetector gesture={composedGesture}>
768
+ <View
769
+ {...webPointerHandlers}
770
+ ref={setWheelTargetRef}
771
+ style={[StyleSheet.absoluteFill, canvasCursorStyle]}
772
+ >
773
+ <Canvas
774
+ // On web, the Skia <canvas> element must not capture pointer events.
775
+ // RNGH attaches its listeners to the parent View and calls
776
+ // setPointerCapture on event.target; if the <canvas> is the topmost
777
+ // hit target it grabs the pointer and starves the gesture of the move
778
+ // stream (only stray points get through).
779
+ // The Skia web view drops the `pointerEvents` prop, so we must deliver
780
+ // `pointer-events: none` through `style` instead — it is inherited by
781
+ // the inner <canvas>, making it click-through.
782
+ style={
783
+ Platform.OS === "web"
784
+ ? { ...StyleSheet.absoluteFillObject, pointerEvents: "none" }
785
+ : StyleSheet.absoluteFill
786
+ }
787
+ ref={canvasRef}
788
+ >
789
+ {/* Background */}
790
+ <Fill color="black" />
791
+
792
+ {/* Viewport transform group (zoom/pan) */}
793
+ <Group
794
+ transform={viewportTransform}
795
+ >
796
+ {/* Background image */}
797
+ {skiaImage && (
798
+ <Image
799
+ image={skiaImage}
800
+ x={imageRect.x}
801
+ y={imageRect.y}
802
+ width={imageRect.width}
803
+ height={imageRect.height}
804
+ fit="contain"
805
+ />
806
+ )}
807
+
808
+ {/* Drawing group: offset to image rect and clip to bounds */}
809
+ <Group
810
+ transform={[
811
+ { translateX: imageRect.x },
812
+ { translateY: imageRect.y },
813
+ ]}
814
+ clip={rect(0, 0, imageRect.width, imageRect.height)}
815
+ >
816
+ {/* Non-selected objects (excluding text — rendered as native views) */}
817
+ {unselectedNonTextObjects.map((obj) => (
818
+ <ObjectRenderer
819
+ key={obj.id}
820
+ object={obj}
821
+ canvasSize={effectiveCanvasSize}
822
+ />
823
+ ))}
824
+
825
+ {/* Selected object with live animated drag/resize */}
826
+ {selectedObject && selectedObject.type !== "text" && (
827
+ <>
828
+ <SelectedObjectRenderer
829
+ object={selectedObject}
830
+ canvasSize={effectiveCanvasSize}
831
+ isDragging={selection.isDragging}
832
+ draggingAnchorIndex={
833
+ selection.draggingAnchorIndex
834
+ }
835
+ dragOffsetX={selection.dragOffsetX}
836
+ dragOffsetY={selection.dragOffsetY}
837
+ />
838
+ <SelectionOverlay
839
+ object={selectedObject}
840
+ canvasSize={effectiveCanvasSize}
841
+ draggingAnchorIndex={
842
+ selection.draggingAnchorIndex
843
+ }
844
+ dragOffsetX={selection.dragOffsetX}
845
+ dragOffsetY={selection.dragOffsetY}
846
+ isDragging={selection.isDragging}
847
+ />
848
+ </>
849
+ )}
850
+
851
+ {/* In-progress freehand */}
852
+ {isFreehand && (
853
+ <FreehandInProgress
854
+ path={freehand.inProgressPath}
855
+ color={freehand.inProgressColor}
856
+ lineWidth={freehand.inProgressWidth}
857
+ isDrawing={freehand.isDrawing}
858
+ />
859
+ )}
860
+
861
+ {/* In-progress shape */}
862
+ {isTwoPointShape && (
863
+ <ShapeInProgress
864
+ shapeType={currentTool as TwoPointShapeType}
865
+ startX={shape.startX}
866
+ startY={shape.startY}
867
+ currentX={shape.currentX}
868
+ currentY={shape.currentY}
869
+ color={shape.inProgressColor}
870
+ lineWidth={shape.inProgressWidth}
871
+ fillColor={shape.inProgressFillColor}
872
+ fillAlpha={shape.inProgressFillAlpha}
873
+ isDrawing={shape.isDrawing}
874
+ />
875
+ )}
876
+
877
+ {/* In-progress polygon: blue dots at each placed vertex */}
878
+ {isPolygon && (
879
+ <PolygonInProgress
880
+ pointsFlat={polygon.pointsFlat}
881
+ pointCount={polygon.pointCount}
882
+ isActive={polygon.isActive}
883
+ />
884
+ )}
885
+ </Group>
886
+ </Group>
887
+ </Canvas>
888
+
889
+ {/* Text annotations (native RN views overlaying the canvas) */}
890
+ {textObjects.map((textObj) => (
891
+ <TextAnnotation
892
+ key={textObj.id}
893
+ object={textObj}
894
+ canvasSize={effectiveCanvasSize}
895
+ imageOffset={{ x: imageRect.x, y: imageRect.y }}
896
+ isSelected={selectedObjectId === textObj.id}
897
+ onSelect={selectObject}
898
+ onMove={moveObject}
899
+ onDoubleTap={handleTextDoubleTap}
900
+ onDelete={deleteObject}
901
+ viewScale={viewport.scale}
902
+ viewTranslateX={viewport.translateX}
903
+ viewTranslateY={viewport.translateY}
904
+ />
905
+ ))}
906
+ </View>
907
+ </GestureDetector>
908
+
909
+ {/* ── Zoom UI overlays ─────────────────────────────────────── */}
910
+ <ZoomBadge
911
+ scale={viewport.scale}
912
+ onPress={handleResetZoom}
913
+ />
914
+ <MiniMap
915
+ image={skiaImage}
916
+ imageRect={imageRect}
917
+ scale={viewport.scale}
918
+ translateX={viewport.translateX}
919
+ translateY={viewport.translateY}
920
+ visible={isZoomed}
921
+ onPanViewport={viewport.panViewport}
922
+ />
923
+ </View>
924
+
925
+ {/* ── Right: Line Width Slider (tablet only) ─────────────────── */}
926
+ {!isPhone && (
927
+ <View style={styles.sidebarRight}>
928
+ <LineWidthSlider
929
+ value={lineWidth}
930
+ onValueChange={setLineWidth}
931
+ orientation="vertical"
932
+ />
933
+ </View>
934
+ )}
935
+ </View>
936
+
937
+ {/* ── Bottom: phone layout (flex) or tablet (absolute) ──────── */}
938
+ {isPhone ? (
939
+ <View style={[styles.phoneBottomArea, { paddingBottom: insets.bottom }]}>
940
+ {/* Controls bar */}
941
+ <View style={styles.phoneControlsBar}>
942
+ {renderColorPalette ? (
943
+ renderColorPalette({
944
+ strokeColor,
945
+ fillColor,
946
+ fillAlpha,
947
+ onSelectStrokeColor: setStrokeColor,
948
+ onSelectFillColor: useDrawingStore.getState().setFillColor,
949
+ onSelectFillAlpha: useDrawingStore.getState().setFillAlpha,
950
+ })
951
+ ) : (
952
+ <ColorPalette
953
+ strokeColor={strokeColor}
954
+ onSelectColor={setStrokeColor}
955
+ direction="horizontal"
956
+ />
957
+ )}
958
+ <LineWidthSlider
959
+ value={lineWidth}
960
+ onValueChange={setLineWidth}
961
+ orientation="horizontal"
962
+ />
963
+ </View>
964
+ {/* Toolbar */}
965
+ {renderToolbar ? (
966
+ renderToolbar({
967
+ currentTool,
968
+ onSelectTool: handleSelectTool,
969
+ onUndo: handleUndo,
970
+ canUndo: undoStack.length > 0,
971
+ isZoomed,
972
+ onResetZoom: handleResetZoom,
973
+ })
974
+ ) : (
975
+ <Toolbar
976
+ currentTool={currentTool}
977
+ onSelectTool={handleSelectTool}
978
+ onUndo={handleUndo}
979
+ canUndo={undoStack.length > 0}
980
+ isZoomed={isZoomed}
981
+ onResetZoom={handleResetZoom}
982
+ />
983
+ )}
984
+ </View>
985
+ ) : (
986
+ <View pointerEvents="box-none" style={styles.toolbarOverlay}>
987
+ {renderToolbar ? (
988
+ renderToolbar({
989
+ currentTool,
990
+ onSelectTool: handleSelectTool,
991
+ onUndo: handleUndo,
992
+ canUndo: undoStack.length > 0,
993
+ isZoomed,
994
+ onResetZoom: handleResetZoom,
995
+ })
996
+ ) : (
997
+ <Toolbar
998
+ currentTool={currentTool}
999
+ onSelectTool={handleSelectTool}
1000
+ onUndo={handleUndo}
1001
+ canUndo={undoStack.length > 0}
1002
+ isZoomed={isZoomed}
1003
+ onResetZoom={handleResetZoom}
1004
+ />
1005
+ )}
1006
+ </View>
1007
+ )}
1008
+
1009
+ {/* ── Modals ──────────────────────────────────────────────────── */}
1010
+ <TextEditModal
1011
+ visible={textEditVisible}
1012
+ initialValue={textEditValue}
1013
+ onSubmit={handleTextSubmit}
1014
+ onCancel={() => setTextEditVisible(false)}
1015
+ />
1016
+ <MeasurementEditModal
1017
+ visible={measureEditVisible}
1018
+ initialValue=""
1019
+ initialUnit={lastUsedUnit.current}
1020
+ onSubmit={handleMeasureSubmit}
1021
+ onCancel={handleMeasureCancel}
1022
+ />
1023
+ </GestureHandlerRootView>
1024
+ );
1025
+ },
1026
+ );
1027
+
1028
+ const styles = StyleSheet.create({
1029
+ root: {
1030
+ flex: 1,
1031
+ backgroundColor: "#000000",
1032
+ position: "relative",
1033
+ },
1034
+ editorContainer: {
1035
+ flex: 1,
1036
+ flexDirection: "row",
1037
+ },
1038
+ editorContainerTablet: {
1039
+ paddingBottom: 92,
1040
+ },
1041
+ editorContainerPhone: {
1042
+ flexDirection: "column",
1043
+ },
1044
+ sidebarLeft: {
1045
+ justifyContent: "center",
1046
+ paddingHorizontal: 4,
1047
+ },
1048
+ canvasContainer: {
1049
+ flex: 1,
1050
+ },
1051
+ sidebarRight: {
1052
+ justifyContent: "center",
1053
+ paddingHorizontal: 4,
1054
+ },
1055
+ phoneBottomArea: {
1056
+ backgroundColor: "#000000",
1057
+ },
1058
+ phoneControlsBar: {
1059
+ flexDirection: "row",
1060
+ alignItems: "center",
1061
+ justifyContent: "center",
1062
+ paddingHorizontal: 4,
1063
+ },
1064
+ toolbarOverlay: {
1065
+ position: "absolute",
1066
+ left: 0,
1067
+ right: 0,
1068
+ bottom: 12,
1069
+ alignItems: "center",
1070
+ },
1071
+ });