@yuzhouu/canvas-kit 0.1.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.
Files changed (263) hide show
  1. package/README.md +145 -0
  2. package/_vendor/canvas-core/bindings/BindingUtil.ts +92 -0
  3. package/_vendor/canvas-core/bindings/BindingUtilRegistry.ts +22 -0
  4. package/_vendor/canvas-core/bindings/arrow/arrowBindingHelpers.ts +52 -0
  5. package/_vendor/canvas-core/contextMenu.ts +85 -0
  6. package/_vendor/canvas-core/editor/Editor.ts +2614 -0
  7. package/_vendor/canvas-core/editor/HistoryManager.ts +83 -0
  8. package/_vendor/canvas-core/editor/StateNode.ts +162 -0
  9. package/_vendor/canvas-core/editor/camera.ts +6 -0
  10. package/_vendor/canvas-core/editor/clipboard.ts +478 -0
  11. package/_vendor/canvas-core/editor/commands.ts +833 -0
  12. package/_vendor/canvas-core/editor/edgeScroll.ts +37 -0
  13. package/_vendor/canvas-core/editor/editorComputedHelpers.ts +257 -0
  14. package/_vendor/canvas-core/editor/editorSelectionTransforms.ts +116 -0
  15. package/_vendor/canvas-core/editor/events.ts +93 -0
  16. package/_vendor/canvas-core/editor/index.ts +12 -0
  17. package/_vendor/canvas-core/editor/interactions.ts +101 -0
  18. package/_vendor/canvas-core/editor/internal.ts +22 -0
  19. package/_vendor/canvas-core/editor/managers/BindingManager.ts +262 -0
  20. package/_vendor/canvas-core/editor/managers/CameraManager.ts +147 -0
  21. package/_vendor/canvas-core/editor/managers/ClipboardManager.ts +32 -0
  22. package/_vendor/canvas-core/editor/managers/DocumentSnapshotManager.ts +256 -0
  23. package/_vendor/canvas-core/editor/managers/InputsManager.ts +319 -0
  24. package/_vendor/canvas-core/editor/managers/InteractionTransientManager.ts +218 -0
  25. package/_vendor/canvas-core/editor/managers/QueryManager.ts +837 -0
  26. package/_vendor/canvas-core/editor/managers/RenderWindowManager.ts +347 -0
  27. package/_vendor/canvas-core/editor/managers/SelectionLayoutManager.ts +196 -0
  28. package/_vendor/canvas-core/editor/managers/SelectionManager.ts +98 -0
  29. package/_vendor/canvas-core/editor/managers/SelectionOverlayManager.ts +308 -0
  30. package/_vendor/canvas-core/editor/managers/ShapeEditingManager.ts +226 -0
  31. package/_vendor/canvas-core/editor/managers/ShapeGeometryManager.ts +162 -0
  32. package/_vendor/canvas-core/editor/managers/ShapeHierarchyManager.ts +797 -0
  33. package/_vendor/canvas-core/editor/managers/SideEffectManager.ts +83 -0
  34. package/_vendor/canvas-core/editor/managers/SnapManager.ts +433 -0
  35. package/_vendor/canvas-core/editor/managers/SnapSnapshotBuilder.ts +430 -0
  36. package/_vendor/canvas-core/editor/managers/SpatialIndexManager.ts +354 -0
  37. package/_vendor/canvas-core/editor/managers/ToolManager.ts +81 -0
  38. package/_vendor/canvas-core/editor/managers/queryState.ts +583 -0
  39. package/_vendor/canvas-core/editor/managers/selectionGeometry.ts +287 -0
  40. package/_vendor/canvas-core/editor/managers/shapeOrder.ts +350 -0
  41. package/_vendor/canvas-core/editor/managers/shapeReparenting.ts +160 -0
  42. package/_vendor/canvas-core/editor/managers/snapCalculations.ts +412 -0
  43. package/_vendor/canvas-core/editor/managers/snapGeometry.ts +202 -0
  44. package/_vendor/canvas-core/editor/managers/snapIndicators.ts +403 -0
  45. package/_vendor/canvas-core/editor/managers/snapTypes.ts +122 -0
  46. package/_vendor/canvas-core/editor/queries/shapeTransforms.ts +171 -0
  47. package/_vendor/canvas-core/editor/renderWindow.ts +439 -0
  48. package/_vendor/canvas-core/editor/resizeGeometry.ts +654 -0
  49. package/_vendor/canvas-core/editor/selection.ts +489 -0
  50. package/_vendor/canvas-core/editor/selectionHandleInteractions.ts +43 -0
  51. package/_vendor/canvas-core/editor/selectionOverlay.ts +567 -0
  52. package/_vendor/canvas-core/exports/integrity.ts +174 -0
  53. package/_vendor/canvas-core/exports/svg.ts +494 -0
  54. package/_vendor/canvas-core/geometry/Box.ts +80 -0
  55. package/_vendor/canvas-core/geometry/Ellipse2d.ts +67 -0
  56. package/_vendor/canvas-core/geometry/Geometry2d.ts +52 -0
  57. package/_vendor/canvas-core/geometry/Mat.ts +92 -0
  58. package/_vendor/canvas-core/geometry/Polyline2d.ts +63 -0
  59. package/_vendor/canvas-core/geometry/Rectangle2d.ts +64 -0
  60. package/_vendor/canvas-core/geometry/Vec.ts +123 -0
  61. package/_vendor/canvas-core/geometry/index.ts +13 -0
  62. package/_vendor/canvas-core/geometry/segments.ts +65 -0
  63. package/_vendor/canvas-core/index.ts +391 -0
  64. package/_vendor/canvas-core/plugins/index.ts +5 -0
  65. package/_vendor/canvas-core/plugins/types.ts +53 -0
  66. package/_vendor/canvas-core/reactivity.ts +16 -0
  67. package/_vendor/canvas-core/schema/records.ts +221 -0
  68. package/_vendor/canvas-core/schema/snapshots.ts +527 -0
  69. package/_vendor/canvas-core/shapes/ShapeUtil.ts +516 -0
  70. package/_vendor/canvas-core/shapes/ShapeUtilRegistry.ts +26 -0
  71. package/_vendor/canvas-core/shapes/capabilities.ts +67 -0
  72. package/_vendor/canvas-core/shapes/types.ts +20 -0
  73. package/_vendor/canvas-core/toolbar.ts +148 -0
  74. package/_vendor/canvas-core/tools/hand/HandTool.ts +75 -0
  75. package/_vendor/canvas-core/tools/index.ts +2 -0
  76. package/_vendor/canvas-core/tools/select/SelectTool.ts +33 -0
  77. package/_vendor/canvas-core/tools/select/selectContainment.ts +104 -0
  78. package/_vendor/canvas-core/tools/select/selectToolHelpers.ts +801 -0
  79. package/_vendor/canvas-core/tools/select/states/BrushingState.ts +145 -0
  80. package/_vendor/canvas-core/tools/select/states/DraggingSelectionHandleInteractionState.ts +116 -0
  81. package/_vendor/canvas-core/tools/select/states/IdleState.ts +159 -0
  82. package/_vendor/canvas-core/tools/select/states/PointingCanvasState.ts +41 -0
  83. package/_vendor/canvas-core/tools/select/states/PointingResizeHandleState.ts +78 -0
  84. package/_vendor/canvas-core/tools/select/states/PointingRotateHandleState.ts +64 -0
  85. package/_vendor/canvas-core/tools/select/states/PointingSelectionHandleInteractionState.ts +34 -0
  86. package/_vendor/canvas-core/tools/select/states/PointingShapeState.ts +140 -0
  87. package/_vendor/canvas-core/tools/select/states/ResizingState.ts +171 -0
  88. package/_vendor/canvas-core/tools/select/states/RotatingState.ts +124 -0
  89. package/_vendor/canvas-core/tools/select/states/SelectStateNode.ts +128 -0
  90. package/_vendor/canvas-core/tools/select/states/TranslatingState.ts +139 -0
  91. package/_vendor/canvas-core/tools/select/states/index.ts +11 -0
  92. package/_vendor/canvas-plugin-api/feature.ts +204 -0
  93. package/_vendor/canvas-plugin-api/index.ts +142 -0
  94. package/_vendor/canvas-product-react/api/apiRegistry.ts +35 -0
  95. package/_vendor/canvas-product-react/api/canvasProductApi.ts +422 -0
  96. package/_vendor/canvas-product-react/api/index.ts +31 -0
  97. package/_vendor/canvas-product-react/capabilities/clipboard/clipboardService.ts +36 -0
  98. package/_vendor/canvas-product-react/capabilities/clipboard/systemClipboard.ts +245 -0
  99. package/_vendor/canvas-product-react/capabilities/context-menu/systemContextMenu.ts +396 -0
  100. package/_vendor/canvas-product-react/capabilities/context-menu/useContextMenuSurface.ts +59 -0
  101. package/_vendor/canvas-product-react/capabilities/export/useSvgExport.ts +156 -0
  102. package/_vendor/canvas-product-react/capabilities/history/useHistoryControls.ts +23 -0
  103. package/_vendor/canvas-product-react/capabilities/pages/usePageControls.ts +41 -0
  104. package/_vendor/canvas-product-react/capabilities/toolbar/systemToolbar.ts +490 -0
  105. package/_vendor/canvas-product-react/capabilities/toolbar/useToolbarSurfaces.ts +92 -0
  106. package/_vendor/canvas-product-react/capabilities/zoom/useZoomControls.ts +27 -0
  107. package/_vendor/canvas-product-react/index.ts +84 -0
  108. package/_vendor/canvas-product-react/persistence/index.ts +18 -0
  109. package/_vendor/canvas-product-react/persistence/localSnapshotPersistence.ts +294 -0
  110. package/_vendor/canvas-product-react/runtime/CanvasProductRoot.tsx +250 -0
  111. package/_vendor/canvas-product-react/runtime/productDefinition.ts +72 -0
  112. package/_vendor/canvas-product-react/runtime/productServices.ts +96 -0
  113. package/_vendor/canvas-react/components/CanvasView.tsx +796 -0
  114. package/_vendor/canvas-react/components/OverlayLayer.tsx +879 -0
  115. package/_vendor/canvas-react/components/PageSpace.tsx +34 -0
  116. package/_vendor/canvas-react/components/ShapeLayer.tsx +428 -0
  117. package/_vendor/canvas-react/components/index.ts +4 -0
  118. package/_vendor/canvas-react/hooks/useEditor.ts +11 -0
  119. package/_vendor/canvas-react/hooks/useValue.ts +14 -0
  120. package/_vendor/canvas-react/host/CanvasRendererRegistryContext.tsx +14 -0
  121. package/_vendor/canvas-react/host/contextMenuRegistry.ts +61 -0
  122. package/_vendor/canvas-react/host/domEventGuards.ts +61 -0
  123. package/_vendor/canvas-react/host/index.ts +38 -0
  124. package/_vendor/canvas-react/host/pluginHost.ts +635 -0
  125. package/_vendor/canvas-react/host/rendererRegistry.tsx +58 -0
  126. package/_vendor/canvas-react/host/toolbarRegistry.ts +100 -0
  127. package/_vendor/canvas-react/index.ts +3 -0
  128. package/_vendor/plugin-arrow/ArrowBindingUtil.ts +239 -0
  129. package/_vendor/plugin-arrow/ArrowShapeUtil.ts +311 -0
  130. package/_vendor/plugin-arrow/ArrowTool.ts +208 -0
  131. package/_vendor/plugin-arrow/arrowGeometry.ts +441 -0
  132. package/_vendor/plugin-arrow/arrowTerminalBindingResolver.ts +253 -0
  133. package/_vendor/plugin-arrow/arrowTerminalCommands.ts +230 -0
  134. package/_vendor/plugin-arrow/canvas.tsx +92 -0
  135. package/_vendor/plugin-arrow/core.ts +409 -0
  136. package/_vendor/plugin-arrow/feature.ts +20 -0
  137. package/_vendor/plugin-arrow/index.ts +28 -0
  138. package/_vendor/plugin-arrow/selectionHandleInteractions.ts +154 -0
  139. package/_vendor/plugin-arrow/types.ts +63 -0
  140. package/_vendor/plugin-draw/DrawShapeRenderer.tsx +60 -0
  141. package/_vendor/plugin-draw/DrawShapeUtil.ts +372 -0
  142. package/_vendor/plugin-draw/DrawTool.ts +444 -0
  143. package/_vendor/plugin-draw/canvas.tsx +24 -0
  144. package/_vendor/plugin-draw/core.ts +515 -0
  145. package/_vendor/plugin-draw/drawGeometry.ts +491 -0
  146. package/_vendor/plugin-draw/drawHitTest.ts +271 -0
  147. package/_vendor/plugin-draw/drawInterpolation.ts +250 -0
  148. package/_vendor/plugin-draw/drawPath.ts +550 -0
  149. package/_vendor/plugin-draw/drawSegments.ts +106 -0
  150. package/_vendor/plugin-draw/drawSelectionTransform.ts +67 -0
  151. package/_vendor/plugin-draw/drawShapeGeometry.ts +307 -0
  152. package/_vendor/plugin-draw/drawStrokeGeometry.ts +454 -0
  153. package/_vendor/plugin-draw/feature.ts +20 -0
  154. package/_vendor/plugin-draw/index.ts +31 -0
  155. package/_vendor/plugin-draw/types.ts +36 -0
  156. package/_vendor/plugin-eraser/EraserTool.ts +297 -0
  157. package/_vendor/plugin-eraser/canvas.tsx +616 -0
  158. package/_vendor/plugin-eraser/core.ts +49 -0
  159. package/_vendor/plugin-eraser/feature.ts +20 -0
  160. package/_vendor/plugin-eraser/index.ts +4 -0
  161. package/_vendor/plugin-frame/FrameShapeUtil.ts +173 -0
  162. package/_vendor/plugin-frame/FrameTool.ts +225 -0
  163. package/_vendor/plugin-frame/canvas.tsx +75 -0
  164. package/_vendor/plugin-frame/core.ts +534 -0
  165. package/_vendor/plugin-frame/feature.ts +20 -0
  166. package/_vendor/plugin-frame/index.ts +6 -0
  167. package/_vendor/plugin-geo/GeoShapeUtil.ts +463 -0
  168. package/_vendor/plugin-geo/GeoTools.ts +247 -0
  169. package/_vendor/plugin-geo/canvas.tsx +90 -0
  170. package/_vendor/plugin-geo/core.ts +495 -0
  171. package/_vendor/plugin-geo/feature.ts +20 -0
  172. package/_vendor/plugin-geo/index.ts +7 -0
  173. package/_vendor/plugin-geo/types.ts +27 -0
  174. package/_vendor/plugin-group/GroupShapeUtil.ts +59 -0
  175. package/_vendor/plugin-group/canvas.tsx +59 -0
  176. package/_vendor/plugin-group/core.ts +40 -0
  177. package/_vendor/plugin-group/feature.ts +20 -0
  178. package/_vendor/plugin-group/index.ts +5 -0
  179. package/_vendor/plugin-image/ImageShapeUtil.ts +95 -0
  180. package/_vendor/plugin-image/canvas.tsx +212 -0
  181. package/_vendor/plugin-image/core.ts +542 -0
  182. package/_vendor/plugin-image/crop.ts +770 -0
  183. package/_vendor/plugin-image/cropInteractions.ts +350 -0
  184. package/_vendor/plugin-image/cropSession.ts +38 -0
  185. package/_vendor/plugin-image/feature.ts +20 -0
  186. package/_vendor/plugin-image/index.ts +29 -0
  187. package/_vendor/plugin-image/types.ts +55 -0
  188. package/_vendor/plugin-image-product-react/ImageImportController.tsx +143 -0
  189. package/_vendor/plugin-image-product-react/imageAssetService.ts +52 -0
  190. package/_vendor/plugin-image-product-react/imageBlobStore.ts +315 -0
  191. package/_vendor/plugin-image-product-react/imageHostActions.ts +438 -0
  192. package/_vendor/plugin-image-product-react/imageProductApi.ts +88 -0
  193. package/_vendor/plugin-image-product-react/imageToolbarHostBridge.ts +48 -0
  194. package/_vendor/plugin-image-product-react/imageToolbarRuntime.ts +21 -0
  195. package/_vendor/plugin-image-product-react/index.ts +60 -0
  196. package/_vendor/plugin-image-product-react/snapshotGuards.ts +33 -0
  197. package/_vendor/plugin-image-product-react/svgImageAssetInliner.ts +54 -0
  198. package/_vendor/plugin-image-product-react/useImageImport.ts +64 -0
  199. package/_vendor/plugin-laser/LaserTool.ts +154 -0
  200. package/_vendor/plugin-laser/canvas.tsx +645 -0
  201. package/_vendor/plugin-laser/core.ts +49 -0
  202. package/_vendor/plugin-laser/feature.ts +20 -0
  203. package/_vendor/plugin-laser/index.ts +4 -0
  204. package/_vendor/plugin-text/ArrowLabelBindingUtil.ts +336 -0
  205. package/_vendor/plugin-text/GeoLabelBindingUtil.ts +391 -0
  206. package/_vendor/plugin-text/TextShapeSvgExporter.ts +101 -0
  207. package/_vendor/plugin-text/TextShapeUtil.ts +284 -0
  208. package/_vendor/plugin-text/TextTool.ts +228 -0
  209. package/_vendor/plugin-text/canvas.tsx +115 -0
  210. package/_vendor/plugin-text/core.ts +422 -0
  211. package/_vendor/plugin-text/feature.ts +20 -0
  212. package/_vendor/plugin-text/host/RichTextEditingBox.tsx +144 -0
  213. package/_vendor/plugin-text/host/RichTextToolbar.tsx +266 -0
  214. package/_vendor/plugin-text/host/TiptapTextEditor.tsx +191 -0
  215. package/_vendor/plugin-text/host/richTextRendering.tsx +49 -0
  216. package/_vendor/plugin-text/host/textDomSelection.ts +93 -0
  217. package/_vendor/plugin-text/host/textEditingBoxStyle.ts +36 -0
  218. package/_vendor/plugin-text/host/textEntryFocus.ts +307 -0
  219. package/_vendor/plugin-text/host/textFontLoader.ts +149 -0
  220. package/_vendor/plugin-text/host/textMeasurement.ts +258 -0
  221. package/_vendor/plugin-text/host/textMeasurementSync.ts +77 -0
  222. package/_vendor/plugin-text/host/textRangeSelection.ts +134 -0
  223. package/_vendor/plugin-text/host/textShapeRendererRuntime.tsx +89 -0
  224. package/_vendor/plugin-text/host/textShapeSync.ts +173 -0
  225. package/_vendor/plugin-text/host/tiptapTextEditorController.ts +148 -0
  226. package/_vendor/plugin-text/index.ts +18 -0
  227. package/_vendor/plugin-text/richTextUtils.ts +259 -0
  228. package/_vendor/plugin-text/textInteractions.ts +366 -0
  229. package/_vendor/plugin-text/textLabelHelpers.ts +235 -0
  230. package/_vendor/plugin-text/textShapeHelpers.ts +123 -0
  231. package/_vendor/plugin-text/textStyle.ts +96 -0
  232. package/_vendor/plugin-text/textTypes.ts +48 -0
  233. package/_vendor/signals/atom.ts +74 -0
  234. package/_vendor/signals/computed.ts +132 -0
  235. package/_vendor/signals/core.ts +130 -0
  236. package/_vendor/signals/effect.ts +117 -0
  237. package/_vendor/signals/index.ts +9 -0
  238. package/_vendor/signals/types.ts +17 -0
  239. package/_vendor/store/Store.ts +171 -0
  240. package/_vendor/store/diff.ts +229 -0
  241. package/_vendor/store/history.ts +177 -0
  242. package/_vendor/store/index.ts +32 -0
  243. package/_vendor/store/types.ts +37 -0
  244. package/api/canvasKitApi.d.ts +29 -0
  245. package/index.css +3 -0
  246. package/index.d.ts +16 -0
  247. package/index.mjs +1 -0
  248. package/internal-modules.d.ts +187 -0
  249. package/package.json +48 -0
  250. package/product/Shell.d.ts +19 -0
  251. package/product/chromeContext.d.ts +70 -0
  252. package/product/createCanvasKitEditor.d.ts +6 -0
  253. package/product/index.d.ts +7 -0
  254. package/product/persistence.d.ts +3 -0
  255. package/product/product.d.ts +22 -0
  256. package/product/useCanvasClipboardShortcuts.d.ts +6 -0
  257. package/product-iyBbEPIs.mjs +4 -0
  258. package/product.mjs +1 -0
  259. package/standard/extensions.d.ts +1 -0
  260. package/standard/index.d.ts +3 -0
  261. package/standard/standardApi.d.ts +91 -0
  262. package/standard-BmrxwqpJ.mjs +44 -0
  263. package/standard.mjs +1 -0
@@ -0,0 +1,770 @@
1
+ import {
2
+ Vec,
3
+ type SelectionResizeHandleId,
4
+ type ShapeUpdate,
5
+ } from "@land/canvas-core";
6
+ import type { ImageShapeCrop, ImageShapeRecord } from "./types";
7
+
8
+ const MIN_NORMALIZED_CROP_SIZE = 0.0001;
9
+ const MIN_CROP_SIZE = 8;
10
+ const IMAGE_CROP_ZOOM_FACTOR = 0.8;
11
+ const IMAGE_CROP_EPSILON = 0.0001;
12
+ export const IMAGE_CROP_HANDLE_TYPE = "image.crop";
13
+ export const IMAGE_CROP_HISTORY_MARK_PREFIX = "image.crop:";
14
+ export const IMAGE_CROP_HANDLE_HISTORY_MARK = "image.crop.handle";
15
+ export const IMAGE_CROP_TRANSLATE_HISTORY_MARK = "image.crop.translate";
16
+ export const IMAGE_CROP_MODE_ENTRY_MARK_PREFIX = `${IMAGE_CROP_HISTORY_MARK_PREFIX}edit:`;
17
+ export const IMAGE_CROP_ASPECT_RATIO_PRESETS = [
18
+ { label: "1:1", value: "1:1", ratio: 1 },
19
+ { label: "4:3", value: "4:3", ratio: 4 / 3 },
20
+ { label: "16:9", value: "16:9", ratio: 16 / 9 },
21
+ ] as const;
22
+
23
+ export type ImageCropAspectRatioPresetValue =
24
+ (typeof IMAGE_CROP_ASPECT_RATIO_PRESETS)[number]["value"];
25
+
26
+ const IMAGE_CROP_HANDLE_CURSORS: Record<SelectionResizeHandleId, string> = {
27
+ top_left: "nwse-resize",
28
+ top: "ns-resize",
29
+ top_right: "nesw-resize",
30
+ right: "ew-resize",
31
+ bottom_right: "nwse-resize",
32
+ bottom: "ns-resize",
33
+ bottom_left: "nesw-resize",
34
+ left: "ew-resize",
35
+ };
36
+
37
+ const IMAGE_CROP_HANDLE_IDS = [
38
+ "top_left",
39
+ "top",
40
+ "top_right",
41
+ "right",
42
+ "bottom_right",
43
+ "bottom",
44
+ "bottom_left",
45
+ "left",
46
+ ] as const satisfies readonly SelectionResizeHandleId[];
47
+
48
+ export interface ImageCropRect {
49
+ left: number;
50
+ top: number;
51
+ right: number;
52
+ bottom: number;
53
+ width: number;
54
+ height: number;
55
+ }
56
+
57
+ export interface ImageCropPlacement {
58
+ imageX: number;
59
+ imageY: number;
60
+ imageWidth: number;
61
+ imageHeight: number;
62
+ rect: ImageCropRect;
63
+ }
64
+
65
+ export interface ImageCropCustomHandleAnchor {
66
+ kind: "custom";
67
+ type: typeof IMAGE_CROP_HANDLE_TYPE;
68
+ id: SelectionResizeHandleId;
69
+ point: Vec;
70
+ cursor: string;
71
+ }
72
+
73
+ interface UncroppedImageSpace {
74
+ w: number;
75
+ h: number;
76
+ }
77
+
78
+ function clamp01(value: number): number {
79
+ return Math.min(1, Math.max(0, value));
80
+ }
81
+
82
+ function clamp(value: number, min: number, max: number): number {
83
+ return Math.min(max, Math.max(min, value));
84
+ }
85
+
86
+ function isCropPoint(value: unknown): value is { x: number; y: number } {
87
+ return (
88
+ !!value &&
89
+ typeof value === "object" &&
90
+ typeof (value as { x?: unknown }).x === "number" &&
91
+ Number.isFinite((value as { x: number }).x) &&
92
+ typeof (value as { y?: unknown }).y === "number" &&
93
+ Number.isFinite((value as { y: number }).y)
94
+ );
95
+ }
96
+
97
+ function normalizeRange(
98
+ first: number,
99
+ second: number,
100
+ ): { min: number; max: number } {
101
+ const clampedFirst = clamp01(first);
102
+ const clampedSecond = clamp01(second);
103
+ let min = Math.min(clampedFirst, clampedSecond);
104
+ let max = Math.max(clampedFirst, clampedSecond);
105
+
106
+ if (max - min < MIN_NORMALIZED_CROP_SIZE) {
107
+ const center = (min + max) / 2;
108
+ min = Math.max(
109
+ 0,
110
+ Math.min(
111
+ 1 - MIN_NORMALIZED_CROP_SIZE,
112
+ center - MIN_NORMALIZED_CROP_SIZE / 2,
113
+ ),
114
+ );
115
+ max = min + MIN_NORMALIZED_CROP_SIZE;
116
+ }
117
+
118
+ return { min, max };
119
+ }
120
+
121
+ export function normalizeImageShapeCrop(value: unknown): ImageShapeCrop | null {
122
+ if (!value || typeof value !== "object") {
123
+ return null;
124
+ }
125
+ const candidate = value as {
126
+ bottomRight?: unknown;
127
+ topLeft?: unknown;
128
+ };
129
+ if (!isCropPoint(candidate.topLeft) || !isCropPoint(candidate.bottomRight)) {
130
+ return null;
131
+ }
132
+
133
+ const x = normalizeRange(candidate.topLeft.x, candidate.bottomRight.x);
134
+ const y = normalizeRange(candidate.topLeft.y, candidate.bottomRight.y);
135
+ return {
136
+ topLeft: { x: x.min, y: y.min },
137
+ bottomRight: { x: x.max, y: y.max },
138
+ };
139
+ }
140
+
141
+ export function getImageCropRect(
142
+ crop: ImageShapeCrop | null | undefined,
143
+ ): ImageCropRect | null {
144
+ const normalized = normalizeImageShapeCrop(crop);
145
+ if (!normalized) {
146
+ return null;
147
+ }
148
+
149
+ const left = normalized.topLeft.x;
150
+ const top = normalized.topLeft.y;
151
+ const right = normalized.bottomRight.x;
152
+ const bottom = normalized.bottomRight.y;
153
+ return {
154
+ left,
155
+ top,
156
+ right,
157
+ bottom,
158
+ width: right - left,
159
+ height: bottom - top,
160
+ };
161
+ }
162
+
163
+ export function getDefaultImageCrop(): ImageShapeCrop {
164
+ return {
165
+ topLeft: { x: 0, y: 0 },
166
+ bottomRight: { x: 1, y: 1 },
167
+ };
168
+ }
169
+
170
+ export function getImageCropRectOrDefault(
171
+ crop: ImageShapeCrop | null | undefined,
172
+ ): ImageCropRect {
173
+ return getImageCropRect(crop) ?? {
174
+ left: 0,
175
+ top: 0,
176
+ right: 1,
177
+ bottom: 1,
178
+ width: 1,
179
+ height: 1,
180
+ };
181
+ }
182
+
183
+ function isCropRectApproximatelyFull(rect: ImageCropRect): boolean {
184
+ return (
185
+ Math.abs(rect.left) <= IMAGE_CROP_EPSILON &&
186
+ Math.abs(rect.top) <= IMAGE_CROP_EPSILON &&
187
+ Math.abs(1 - rect.right) <= IMAGE_CROP_EPSILON &&
188
+ Math.abs(1 - rect.bottom) <= IMAGE_CROP_EPSILON
189
+ );
190
+ }
191
+
192
+ function areCropRectsEqual(left: ImageCropRect, right: ImageCropRect): boolean {
193
+ return (
194
+ Math.abs(left.left - right.left) <= IMAGE_CROP_EPSILON &&
195
+ Math.abs(left.top - right.top) <= IMAGE_CROP_EPSILON &&
196
+ Math.abs(left.right - right.right) <= IMAGE_CROP_EPSILON &&
197
+ Math.abs(left.bottom - right.bottom) <= IMAGE_CROP_EPSILON
198
+ );
199
+ }
200
+
201
+ function getImageCropFromRect(
202
+ rect: ImageCropRect,
203
+ ): ImageShapeCrop | null {
204
+ if (isCropRectApproximatelyFull(rect)) {
205
+ return null;
206
+ }
207
+
208
+ return normalizeImageShapeCrop({
209
+ topLeft: {
210
+ x: rect.left,
211
+ y: rect.top,
212
+ },
213
+ bottomRight: {
214
+ x: rect.right,
215
+ y: rect.bottom,
216
+ },
217
+ });
218
+ }
219
+
220
+ function getCenteredCropRect({
221
+ centerX,
222
+ centerY,
223
+ height,
224
+ width,
225
+ }: {
226
+ centerX: number;
227
+ centerY: number;
228
+ height: number;
229
+ width: number;
230
+ }): ImageCropRect {
231
+ const nextWidth = clamp(width, MIN_NORMALIZED_CROP_SIZE, 1);
232
+ const nextHeight = clamp(height, MIN_NORMALIZED_CROP_SIZE, 1);
233
+ const left = clamp(centerX - nextWidth / 2, 0, 1 - nextWidth);
234
+ const top = clamp(centerY - nextHeight / 2, 0, 1 - nextHeight);
235
+ return {
236
+ left,
237
+ top,
238
+ right: left + nextWidth,
239
+ bottom: top + nextHeight,
240
+ width: nextWidth,
241
+ height: nextHeight,
242
+ };
243
+ }
244
+
245
+ function getImageCropRectCenter(rect: ImageCropRect): { x: number; y: number } {
246
+ return {
247
+ x: rect.left + rect.width / 2,
248
+ y: rect.top + rect.height / 2,
249
+ };
250
+ }
251
+
252
+ export function getUncroppedImageSize({
253
+ crop,
254
+ shape,
255
+ }: {
256
+ crop: ImageShapeCrop | null | undefined;
257
+ shape: ImageShapeRecord;
258
+ }): UncroppedImageSpace {
259
+ const rect = getImageCropRectOrDefault(crop);
260
+ return {
261
+ w: shape.props.w / rect.width,
262
+ h: shape.props.h / rect.height,
263
+ };
264
+ }
265
+
266
+ export function getImageCropAspectRatioPresetValue(
267
+ shape: ImageShapeRecord,
268
+ ): ImageCropAspectRatioPresetValue | null {
269
+ const shapeAspectRatio = shape.props.w / shape.props.h;
270
+ for (const preset of IMAGE_CROP_ASPECT_RATIO_PRESETS) {
271
+ if (Math.abs(shapeAspectRatio - preset.ratio) <= 0.01) {
272
+ return preset.value;
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+
278
+ export function isImageCropModeForShape(
279
+ shape: ImageShapeRecord,
280
+ markId: string | null | undefined,
281
+ ): boolean {
282
+ return markId === getImageCropModeMarkId(shape.id);
283
+ }
284
+
285
+ export function getImageCropModeMarkId(shapeId: ImageShapeRecord["id"]): string {
286
+ return `${IMAGE_CROP_MODE_ENTRY_MARK_PREFIX}${shapeId}`;
287
+ }
288
+
289
+ export function getImageCropHandleIds(): readonly SelectionResizeHandleId[] {
290
+ return IMAGE_CROP_HANDLE_IDS;
291
+ }
292
+
293
+ export function isImageCropHandleId(
294
+ value: string,
295
+ ): value is SelectionResizeHandleId {
296
+ return IMAGE_CROP_HANDLE_IDS.some((id) => id === value);
297
+ }
298
+
299
+ export function getImageCropHandleCursor(
300
+ handleId: SelectionResizeHandleId,
301
+ ): string {
302
+ return IMAGE_CROP_HANDLE_CURSORS[handleId];
303
+ }
304
+
305
+ function movesLeftCropEdge(handleId: SelectionResizeHandleId): boolean {
306
+ return (
307
+ handleId === "top_left" ||
308
+ handleId === "left" ||
309
+ handleId === "bottom_left"
310
+ );
311
+ }
312
+
313
+ function movesRightCropEdge(handleId: SelectionResizeHandleId): boolean {
314
+ return (
315
+ handleId === "top_right" ||
316
+ handleId === "right" ||
317
+ handleId === "bottom_right"
318
+ );
319
+ }
320
+
321
+ function movesTopCropEdge(handleId: SelectionResizeHandleId): boolean {
322
+ return (
323
+ handleId === "top_left" ||
324
+ handleId === "top" ||
325
+ handleId === "top_right"
326
+ );
327
+ }
328
+
329
+ function movesBottomCropEdge(handleId: SelectionResizeHandleId): boolean {
330
+ return (
331
+ handleId === "bottom_left" ||
332
+ handleId === "bottom" ||
333
+ handleId === "bottom_right"
334
+ );
335
+ }
336
+
337
+ export function getImageCropHandleAnchors({
338
+ localToPage,
339
+ shape,
340
+ }: {
341
+ crop: ImageShapeCrop | null | undefined;
342
+ localToPage: { applyToPoint(point: Vec): Vec };
343
+ shape: ImageShapeRecord;
344
+ }): ImageCropCustomHandleAnchor[] {
345
+ const left = 0;
346
+ const right = shape.props.w;
347
+ const top = 0;
348
+ const bottom = shape.props.h;
349
+ const centerX = (left + right) / 2;
350
+ const centerY = (top + bottom) / 2;
351
+ const points: Record<SelectionResizeHandleId, Vec> = {
352
+ top_left: new Vec(left, top),
353
+ top: new Vec(centerX, top),
354
+ top_right: new Vec(right, top),
355
+ right: new Vec(right, centerY),
356
+ bottom_right: new Vec(right, bottom),
357
+ bottom: new Vec(centerX, bottom),
358
+ bottom_left: new Vec(left, bottom),
359
+ left: new Vec(left, centerY),
360
+ };
361
+
362
+ return IMAGE_CROP_HANDLE_IDS.map((id) => ({
363
+ kind: "custom" as const,
364
+ type: IMAGE_CROP_HANDLE_TYPE,
365
+ id,
366
+ point: localToPage.applyToPoint(points[id]),
367
+ cursor: getImageCropHandleCursor(id),
368
+ }));
369
+ }
370
+
371
+ export function getImageCropUpdateFromHandleDrag({
372
+ handleId,
373
+ pageToLocal,
374
+ shape,
375
+ startCrop,
376
+ startPagePoint,
377
+ currentPagePoint,
378
+ }: {
379
+ handleId: SelectionResizeHandleId;
380
+ pageToLocal: { applyToPoint(point: Vec): Vec };
381
+ shape: ImageShapeRecord;
382
+ startCrop: ImageShapeCrop | null | undefined;
383
+ startPagePoint: Vec;
384
+ currentPagePoint: Vec;
385
+ }): ShapeUpdate | null {
386
+ const crop = normalizeImageShapeCrop(startCrop) ?? getDefaultImageCrop();
387
+ const uncroppedSize = getUncroppedImageSize({ crop, shape });
388
+ if (
389
+ uncroppedSize.w < MIN_CROP_SIZE ||
390
+ uncroppedSize.h < MIN_CROP_SIZE
391
+ ) {
392
+ return null;
393
+ }
394
+
395
+ const originLocalPoint = pageToLocal.applyToPoint(startPagePoint);
396
+ const currentLocalPoint = pageToLocal.applyToPoint(currentPagePoint);
397
+ const change = currentLocalPoint.sub(originLocalPoint);
398
+ if (change.x === 0 && change.y === 0) {
399
+ return null;
400
+ }
401
+
402
+ const previousCropBox = {
403
+ x: crop.topLeft.x * uncroppedSize.w,
404
+ y: crop.topLeft.y * uncroppedSize.h,
405
+ w: (crop.bottomRight.x - crop.topLeft.x) * uncroppedSize.w,
406
+ h: (crop.bottomRight.y - crop.topLeft.y) * uncroppedSize.h,
407
+ };
408
+ const previousMaxX = previousCropBox.x + previousCropBox.w;
409
+ const previousMaxY = previousCropBox.y + previousCropBox.h;
410
+ let nextX = previousCropBox.x;
411
+ let nextY = previousCropBox.y;
412
+ let nextW = previousCropBox.w;
413
+ let nextH = previousCropBox.h;
414
+
415
+ if (movesLeftCropEdge(handleId)) {
416
+ nextX = clamp(nextX + change.x, 0, previousMaxX - MIN_CROP_SIZE);
417
+ nextW = previousMaxX - nextX;
418
+ } else if (movesRightCropEdge(handleId)) {
419
+ const nextRight = clamp(
420
+ previousMaxX + change.x,
421
+ previousCropBox.x + MIN_CROP_SIZE,
422
+ uncroppedSize.w,
423
+ );
424
+ nextW = nextRight - nextX;
425
+ }
426
+
427
+ if (movesTopCropEdge(handleId)) {
428
+ nextY = clamp(nextY + change.y, 0, previousMaxY - MIN_CROP_SIZE);
429
+ nextH = previousMaxY - nextY;
430
+ } else if (movesBottomCropEdge(handleId)) {
431
+ const nextBottom = clamp(
432
+ previousMaxY + change.y,
433
+ previousCropBox.y + MIN_CROP_SIZE,
434
+ uncroppedSize.h,
435
+ );
436
+ nextH = nextBottom - nextY;
437
+ }
438
+
439
+ const nextCrop = normalizeImageShapeCrop({
440
+ topLeft: {
441
+ x: nextX / uncroppedSize.w,
442
+ y: nextY / uncroppedSize.h,
443
+ },
444
+ bottomRight: {
445
+ x: (nextX + nextW) / uncroppedSize.w,
446
+ y: (nextY + nextH) / uncroppedSize.h,
447
+ },
448
+ });
449
+ if (!nextCrop) {
450
+ return null;
451
+ }
452
+
453
+ const delta = new Vec(
454
+ nextX - crop.topLeft.x * uncroppedSize.w,
455
+ nextY - crop.topLeft.y * uncroppedSize.h,
456
+ );
457
+ const pageDelta = delta.rot(shape.rotation);
458
+
459
+ return {
460
+ id: shape.id,
461
+ x: shape.x + pageDelta.x,
462
+ y: shape.y + pageDelta.y,
463
+ props: {
464
+ w: nextW,
465
+ h: nextH,
466
+ crop: nextCrop,
467
+ },
468
+ };
469
+ }
470
+
471
+ export function getImageCropUpdateFromContentDrag({
472
+ currentPagePoint,
473
+ pageToLocal,
474
+ shape,
475
+ startCrop,
476
+ startPagePoint,
477
+ shiftKey,
478
+ }: {
479
+ currentPagePoint: Vec;
480
+ pageToLocal: { applyToPoint(point: Vec): Vec };
481
+ shape: ImageShapeRecord;
482
+ startCrop: ImageShapeCrop | null | undefined;
483
+ startPagePoint: Vec;
484
+ shiftKey: boolean;
485
+ }): ShapeUpdate | null {
486
+ const crop = normalizeImageShapeCrop(startCrop);
487
+ if (!crop) {
488
+ return null;
489
+ }
490
+
491
+ const originLocalPoint = pageToLocal.applyToPoint(startPagePoint);
492
+ const currentLocalPoint = pageToLocal.applyToPoint(currentPagePoint);
493
+ let change = currentLocalPoint.sub(originLocalPoint);
494
+ if (shiftKey) {
495
+ change =
496
+ Math.abs(change.x) < Math.abs(change.y)
497
+ ? new Vec(0, change.y)
498
+ : new Vec(change.x, 0);
499
+ }
500
+ if (change.x === 0 && change.y === 0) {
501
+ return null;
502
+ }
503
+
504
+ return getImageCropUpdateFromContentLocalChange({
505
+ change,
506
+ crop,
507
+ shape,
508
+ });
509
+ }
510
+
511
+ export function getImageCropUpdateFromContentNudge({
512
+ pageDelta,
513
+ shape,
514
+ startCrop,
515
+ }: {
516
+ pageDelta: Vec;
517
+ shape: ImageShapeRecord;
518
+ startCrop: ImageShapeCrop | null | undefined;
519
+ }): ShapeUpdate | null {
520
+ const crop = normalizeImageShapeCrop(startCrop);
521
+ if (!crop || pageDelta.equals(Vec.Zero())) {
522
+ return null;
523
+ }
524
+
525
+ return getImageCropUpdateFromContentLocalChange({
526
+ change: pageDelta.rot(-shape.rotation),
527
+ crop,
528
+ shape,
529
+ });
530
+ }
531
+
532
+ function getImageCropUpdateFromContentLocalChange({
533
+ change,
534
+ crop,
535
+ shape,
536
+ }: {
537
+ change: Vec;
538
+ crop: ImageShapeCrop;
539
+ shape: ImageShapeRecord;
540
+ }): ShapeUpdate | null {
541
+ const uncroppedSize = getUncroppedImageSize({ crop, shape });
542
+ const cropWidth = crop.bottomRight.x - crop.topLeft.x;
543
+ const cropHeight = crop.bottomRight.y - crop.topLeft.y;
544
+ const nextTopLeftX = clamp(
545
+ crop.topLeft.x - change.x / uncroppedSize.w,
546
+ 0,
547
+ 1 - cropWidth,
548
+ );
549
+ const nextTopLeftY = clamp(
550
+ crop.topLeft.y - change.y / uncroppedSize.h,
551
+ 0,
552
+ 1 - cropHeight,
553
+ );
554
+
555
+ const nextCrop = normalizeImageShapeCrop({
556
+ topLeft: {
557
+ x: nextTopLeftX,
558
+ y: nextTopLeftY,
559
+ },
560
+ bottomRight: {
561
+ x: nextTopLeftX + cropWidth,
562
+ y: nextTopLeftY + cropHeight,
563
+ },
564
+ });
565
+ if (!nextCrop) {
566
+ return null;
567
+ }
568
+
569
+ return {
570
+ id: shape.id,
571
+ props: {
572
+ crop: nextCrop,
573
+ },
574
+ };
575
+ }
576
+
577
+ export function getImageCropUpdateFromZoom({
578
+ direction,
579
+ shape,
580
+ }: {
581
+ direction: "in" | "out";
582
+ shape: ImageShapeRecord;
583
+ }): ShapeUpdate | null {
584
+ const rect = getImageCropRectOrDefault(shape.props.crop);
585
+ const center = getImageCropRectCenter(rect);
586
+ const rawFactor =
587
+ direction === "in" ? IMAGE_CROP_ZOOM_FACTOR : 1 / IMAGE_CROP_ZOOM_FACTOR;
588
+ const minFactor = Math.max(
589
+ MIN_NORMALIZED_CROP_SIZE / rect.width,
590
+ MIN_NORMALIZED_CROP_SIZE / rect.height,
591
+ );
592
+ const maxFactor = Math.min(1 / rect.width, 1 / rect.height);
593
+ const factor = clamp(rawFactor, minFactor, maxFactor);
594
+ const nextRect = getCenteredCropRect({
595
+ centerX: center.x,
596
+ centerY: center.y,
597
+ width: rect.width * factor,
598
+ height: rect.height * factor,
599
+ });
600
+ if (areCropRectsEqual(rect, nextRect)) {
601
+ return null;
602
+ }
603
+
604
+ return {
605
+ id: shape.id,
606
+ props: {
607
+ crop: getImageCropFromRect(nextRect),
608
+ },
609
+ };
610
+ }
611
+
612
+ function getImageCropUpdateWithLockedImagePlacement({
613
+ nextRect,
614
+ shape,
615
+ }: {
616
+ nextRect: ImageCropRect;
617
+ shape: ImageShapeRecord;
618
+ }): ShapeUpdate | null {
619
+ const currentRect = getImageCropRectOrDefault(shape.props.crop);
620
+ if (areCropRectsEqual(currentRect, nextRect)) {
621
+ return null;
622
+ }
623
+
624
+ const uncroppedSize = getUncroppedImageSize({
625
+ crop: shape.props.crop,
626
+ shape,
627
+ });
628
+ const localDelta = new Vec(
629
+ (nextRect.left - currentRect.left) * uncroppedSize.w,
630
+ (nextRect.top - currentRect.top) * uncroppedSize.h,
631
+ );
632
+ const pageDelta = localDelta.rot(shape.rotation);
633
+
634
+ return {
635
+ id: shape.id,
636
+ x: shape.x + pageDelta.x,
637
+ y: shape.y + pageDelta.y,
638
+ props: {
639
+ w: nextRect.width * uncroppedSize.w,
640
+ h: nextRect.height * uncroppedSize.h,
641
+ crop: getImageCropFromRect(nextRect),
642
+ },
643
+ };
644
+ }
645
+
646
+ export function getImageCropUpdateFromAspectRatioPreset({
647
+ preset,
648
+ shape,
649
+ }: {
650
+ preset: ImageCropAspectRatioPresetValue;
651
+ shape: ImageShapeRecord;
652
+ }): ShapeUpdate | null {
653
+ const currentRect = getImageCropRectOrDefault(shape.props.crop);
654
+ const center = getImageCropRectCenter(currentRect);
655
+ const uncroppedSize = getUncroppedImageSize({
656
+ crop: shape.props.crop,
657
+ shape,
658
+ });
659
+ const presetRatio =
660
+ IMAGE_CROP_ASPECT_RATIO_PRESETS.find((candidate) => candidate.value === preset)
661
+ ?.ratio ?? null;
662
+ if (!presetRatio) {
663
+ return null;
664
+ }
665
+
666
+ const normalizedCropAspectRatio =
667
+ presetRatio * (uncroppedSize.h / uncroppedSize.w);
668
+ let nextWidth = currentRect.width;
669
+ let nextHeight = nextWidth / normalizedCropAspectRatio;
670
+ if (nextHeight > currentRect.height) {
671
+ nextHeight = currentRect.height;
672
+ nextWidth = nextHeight * normalizedCropAspectRatio;
673
+ }
674
+
675
+ const nextRect = getCenteredCropRect({
676
+ centerX: center.x,
677
+ centerY: center.y,
678
+ width: nextWidth,
679
+ height: nextHeight,
680
+ });
681
+ return getImageCropUpdateWithLockedImagePlacement({
682
+ nextRect,
683
+ shape,
684
+ });
685
+ }
686
+
687
+ export function getImageCropResetUpdate({
688
+ baselineShape,
689
+ shape,
690
+ }: {
691
+ baselineShape: ImageShapeRecord;
692
+ shape: ImageShapeRecord;
693
+ }): ShapeUpdate | null {
694
+ const nextX = baselineShape.x;
695
+ const nextY = baselineShape.y;
696
+ const nextW = baselineShape.props.w;
697
+ const nextH = baselineShape.props.h;
698
+ const nextCrop = normalizeImageShapeCrop(baselineShape.props.crop);
699
+ if (
700
+ Math.abs(shape.x - nextX) <= IMAGE_CROP_EPSILON &&
701
+ Math.abs(shape.y - nextY) <= IMAGE_CROP_EPSILON &&
702
+ Math.abs(shape.props.w - nextW) <= IMAGE_CROP_EPSILON &&
703
+ Math.abs(shape.props.h - nextH) <= IMAGE_CROP_EPSILON &&
704
+ ((shape.props.crop === null && nextCrop === null) ||
705
+ (shape.props.crop !== null &&
706
+ nextCrop !== null &&
707
+ Math.abs(shape.props.crop.topLeft.x - nextCrop.topLeft.x) <=
708
+ IMAGE_CROP_EPSILON &&
709
+ Math.abs(shape.props.crop.topLeft.y - nextCrop.topLeft.y) <=
710
+ IMAGE_CROP_EPSILON &&
711
+ Math.abs(shape.props.crop.bottomRight.x - nextCrop.bottomRight.x) <=
712
+ IMAGE_CROP_EPSILON &&
713
+ Math.abs(shape.props.crop.bottomRight.y - nextCrop.bottomRight.y) <=
714
+ IMAGE_CROP_EPSILON))
715
+ ) {
716
+ return null;
717
+ }
718
+
719
+ return {
720
+ id: shape.id,
721
+ x: nextX,
722
+ y: nextY,
723
+ props: {
724
+ w: nextW,
725
+ h: nextH,
726
+ crop: nextCrop,
727
+ },
728
+ };
729
+ }
730
+
731
+ export function getImageCropPlacement({
732
+ crop,
733
+ shapeHeight,
734
+ shapeWidth,
735
+ }: {
736
+ crop: ImageShapeCrop | null | undefined;
737
+ shapeHeight: number;
738
+ shapeWidth: number;
739
+ }): ImageCropPlacement | null {
740
+ const rect = getImageCropRect(crop);
741
+ if (!rect) {
742
+ return null;
743
+ }
744
+
745
+ const imageWidth = shapeWidth / rect.width;
746
+ const imageHeight = shapeHeight / rect.height;
747
+ return {
748
+ imageX: -rect.left * imageWidth,
749
+ imageY: -rect.top * imageHeight,
750
+ imageWidth,
751
+ imageHeight,
752
+ rect,
753
+ };
754
+ }
755
+
756
+ export function formatImageCropViewBox(rect: ImageCropRect): string {
757
+ return [
758
+ formatCropNumber(rect.left),
759
+ formatCropNumber(rect.top),
760
+ formatCropNumber(rect.width),
761
+ formatCropNumber(rect.height),
762
+ ].join(" ");
763
+ }
764
+
765
+ function formatCropNumber(value: number): string {
766
+ if (!Number.isFinite(value)) {
767
+ return "0";
768
+ }
769
+ return Number(value.toFixed(4)).toString();
770
+ }