@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,174 @@
1
+ import type { Editor } from "../editor/Editor";
2
+ import { getEditorStore } from "../editor/internal";
3
+ import {
4
+ isBindingRecord,
5
+ type BindingId,
6
+ type ShapeId,
7
+ } from "../schema/records";
8
+ import type { ShapeSvgExporterRegistry } from "./svg";
9
+
10
+ export type ExportIntegrityIssueKind =
11
+ | "missing-shape"
12
+ | "missing-bounds"
13
+ | "missing-exporter"
14
+ | "missing-binding-endpoint"
15
+ | "invalid-binding"
16
+ | "duplicate-exclusive-binding";
17
+
18
+ export interface ExportIntegrityIssue {
19
+ kind: ExportIntegrityIssueKind;
20
+ id: ShapeId | BindingId;
21
+ canRepair: boolean;
22
+ }
23
+
24
+ export interface ExportIntegrityReport {
25
+ requestedShapeIds: ShapeId[];
26
+ issues: ExportIntegrityIssue[];
27
+ repairableIssueCount: number;
28
+ fatalIssueCount: number;
29
+ canExport: boolean;
30
+ }
31
+
32
+ export interface ExportIntegrityRepairResult {
33
+ before: ExportIntegrityReport;
34
+ after: ExportIntegrityReport;
35
+ repaired: boolean;
36
+ }
37
+
38
+ export function countExportIssueKinds(
39
+ issues: readonly ExportIntegrityIssue[],
40
+ ): Partial<Record<ExportIntegrityIssueKind, number>> {
41
+ const counts: Partial<Record<ExportIntegrityIssueKind, number>> = {};
42
+ for (const issue of issues) {
43
+ counts[issue.kind] = (counts[issue.kind] ?? 0) + 1;
44
+ }
45
+ return counts;
46
+ }
47
+
48
+ export function getExportIntegrityReport({
49
+ editor,
50
+ exporters,
51
+ shapeIds,
52
+ }: {
53
+ editor: Editor;
54
+ exporters: ShapeSvgExporterRegistry;
55
+ shapeIds: Iterable<ShapeId>;
56
+ }): ExportIntegrityReport {
57
+ const requestedShapeIds = Array.from(shapeIds);
58
+ const traversalShapeIds = editor.getShapeExportTraversalIds(requestedShapeIds);
59
+ const issues: ExportIntegrityIssue[] = [];
60
+
61
+ for (const id of traversalShapeIds) {
62
+ const shape = editor.getShape(id);
63
+ if (!shape) {
64
+ issues.push({ kind: "missing-shape", id, canRepair: false });
65
+ continue;
66
+ }
67
+
68
+ if (!isValidExportBounds(editor.computeShapeExportPageBounds(shape))) {
69
+ issues.push({ kind: "missing-bounds", id: shape.id, canRepair: false });
70
+ }
71
+
72
+ if (!exporters.get(shape.type)) {
73
+ issues.push({ kind: "missing-exporter", id: shape.id, canRepair: false });
74
+ }
75
+ }
76
+
77
+ const requestedShapeIdSet = new Set(traversalShapeIds);
78
+ const seenBindingIds = new Set<BindingId>();
79
+ for (const binding of getEditorStore(editor).getAll().filter(isBindingRecord)) {
80
+ if (
81
+ !requestedShapeIdSet.has(binding.fromId) &&
82
+ !requestedShapeIdSet.has(binding.toId)
83
+ ) {
84
+ continue;
85
+ }
86
+
87
+ const fromShape = editor.getShape(binding.fromId);
88
+ const toShape = editor.getShape(binding.toId);
89
+ if (!fromShape || !toShape) {
90
+ issues.push({
91
+ kind: "missing-binding-endpoint",
92
+ id: binding.id,
93
+ canRepair: true,
94
+ });
95
+ continue;
96
+ }
97
+
98
+ if (!editor.bindings.canBind(binding)) {
99
+ issues.push({
100
+ kind: "invalid-binding",
101
+ id: binding.id,
102
+ canRepair: true,
103
+ });
104
+ continue;
105
+ }
106
+
107
+ const duplicatesExistingBinding = editor.bindings
108
+ .getConflictingBindings(binding)
109
+ .some((candidate) => seenBindingIds.has(candidate.id));
110
+ if (duplicatesExistingBinding) {
111
+ issues.push({
112
+ kind: "duplicate-exclusive-binding",
113
+ id: binding.id,
114
+ canRepair: true,
115
+ });
116
+ }
117
+ seenBindingIds.add(binding.id);
118
+ }
119
+
120
+ const repairableIssueCount = issues.filter((issue) => issue.canRepair).length;
121
+ const fatalIssueCount = issues.length - repairableIssueCount;
122
+
123
+ return {
124
+ requestedShapeIds,
125
+ issues,
126
+ repairableIssueCount,
127
+ fatalIssueCount,
128
+ canExport: fatalIssueCount === 0,
129
+ };
130
+ }
131
+
132
+ function isValidExportBounds(bounds: { x: number; y: number; w: number; h: number }): boolean {
133
+ return (
134
+ Number.isFinite(bounds.x) &&
135
+ Number.isFinite(bounds.y) &&
136
+ Number.isFinite(bounds.w) &&
137
+ Number.isFinite(bounds.h) &&
138
+ bounds.w >= 0 &&
139
+ bounds.h >= 0
140
+ );
141
+ }
142
+
143
+ export function repairExportIntegrity({
144
+ editor,
145
+ exporters,
146
+ shapeIds,
147
+ }: {
148
+ editor: Editor;
149
+ exporters: ShapeSvgExporterRegistry;
150
+ shapeIds: Iterable<ShapeId>;
151
+ }): ExportIntegrityRepairResult {
152
+ const requestedShapeIds = Array.from(shapeIds);
153
+ const before = getExportIntegrityReport({
154
+ editor,
155
+ exporters,
156
+ shapeIds: requestedShapeIds,
157
+ });
158
+
159
+ if (before.repairableIssueCount > 0) {
160
+ editor.bindings.repairLoadedBindings("system");
161
+ }
162
+
163
+ const after = getExportIntegrityReport({
164
+ editor,
165
+ exporters,
166
+ shapeIds: requestedShapeIds,
167
+ });
168
+
169
+ return {
170
+ before,
171
+ after,
172
+ repaired: before.issues.length !== after.issues.length,
173
+ };
174
+ }
@@ -0,0 +1,494 @@
1
+ import { Box } from "../geometry/Box";
2
+ import { Vec } from "../geometry/Vec";
3
+ import type { Editor } from "../editor/Editor";
4
+ import type {
5
+ AssetId,
6
+ ShapeId,
7
+ ShapeRecordBase,
8
+ ShapeRecord,
9
+ } from "../schema/records";
10
+
11
+ export type SvgAttributeValue = string | number | boolean | null | undefined;
12
+
13
+ export interface SvgElementNode {
14
+ tag: string;
15
+ attrs?: Record<string, SvgAttributeValue>;
16
+ children?: SvgNode[];
17
+ }
18
+
19
+ export type SvgNode = SvgElementNode | string;
20
+
21
+ export interface SvgExportContext {
22
+ editor: Editor;
23
+ bounds: Box;
24
+ addDef(def: SvgElementNode): string;
25
+ resolveAssetUrl(assetId: AssetId): string | null;
26
+ }
27
+
28
+ export interface ShapeSvgExporter<S extends ShapeRecordBase = ShapeRecordBase> {
29
+ type: S["type"];
30
+ toSvg(shape: S, context: SvgExportContext): SvgNode | null;
31
+ }
32
+
33
+ export interface SvgExportOptions {
34
+ padding?: number;
35
+ background?: string;
36
+ }
37
+
38
+ export interface SvgExportResult {
39
+ svg: string;
40
+ bounds: Box;
41
+ width: number;
42
+ height: number;
43
+ requestedShapeIds: ShapeId[];
44
+ exportedShapeIds: ShapeId[];
45
+ skippedShapes: SvgExportSkippedShape[];
46
+ }
47
+
48
+ export type SvgExportSkipReason =
49
+ | "missing-shape"
50
+ | "missing-bounds"
51
+ | "missing-exporter"
52
+ | "empty-output";
53
+
54
+ export interface SvgExportSkippedShape {
55
+ id: ShapeId;
56
+ reason: SvgExportSkipReason;
57
+ }
58
+
59
+ export class ShapeSvgExporterRegistry {
60
+ private readonly registry = new Map<string, ShapeSvgExporter>();
61
+
62
+ register<S extends ShapeRecordBase>(exporter: ShapeSvgExporter<S>): void {
63
+ this.registry.set(exporter.type, exporter as ShapeSvgExporter);
64
+ }
65
+
66
+ get(type: ShapeRecordBase["type"]): ShapeSvgExporter | null {
67
+ return this.registry.get(type) ?? null;
68
+ }
69
+
70
+ has(type: ShapeRecordBase["type"]): boolean {
71
+ return this.registry.has(type);
72
+ }
73
+
74
+ unregister(type: ShapeRecordBase["type"]): boolean {
75
+ return this.registry.delete(type);
76
+ }
77
+
78
+ values(): IterableIterator<ShapeSvgExporter> {
79
+ return this.registry.values();
80
+ }
81
+ }
82
+
83
+ export function exportShapesToSvgString(
84
+ editor: Editor,
85
+ shapeIds: Iterable<ShapeId>,
86
+ exporters: ShapeSvgExporterRegistry,
87
+ options: SvgExportOptions = {},
88
+ ): SvgExportResult | null {
89
+ const requestedShapeIds = Array.from(shapeIds);
90
+ const traversalShapeIds = editor.getShapeExportTraversalIds(requestedShapeIds);
91
+ const traversalShapeIdSet = new Set<ShapeId>(traversalShapeIds);
92
+ const skippedShapes: SvgExportSkippedShape[] = [];
93
+ const shapes: ShapeRecord[] = [];
94
+
95
+ for (const id of traversalShapeIds) {
96
+ const shape = editor.getShape(id);
97
+ if (!shape) {
98
+ skippedShapes.push({ id, reason: "missing-shape" });
99
+ continue;
100
+ }
101
+ shapes.push(shape);
102
+ }
103
+
104
+ if (shapes.length === 0) {
105
+ return null;
106
+ }
107
+
108
+ const shapeBounds: Box[] = [];
109
+ for (const shape of shapes) {
110
+ const bounds = getClippedShapeExportPageBounds(
111
+ editor,
112
+ shape,
113
+ traversalShapeIdSet,
114
+ );
115
+ if (!bounds) {
116
+ continue;
117
+ }
118
+ if (!isValidBox(bounds)) {
119
+ skippedShapes.push({ id: shape.id, reason: "missing-bounds" });
120
+ continue;
121
+ }
122
+ shapeBounds.push(bounds);
123
+ }
124
+
125
+ if (shapeBounds.length === 0) {
126
+ return null;
127
+ }
128
+
129
+ const rawBounds = unionBoxes(shapeBounds);
130
+ const padding = options.padding ?? 16;
131
+ const bounds = rawBounds.expand(padding);
132
+ const defs: SvgElementNode[] = [];
133
+ const context: SvgExportContext = {
134
+ editor,
135
+ bounds,
136
+ addDef(def) {
137
+ const id = `land-svg-def-${defs.length + 1}`;
138
+ defs.push({
139
+ ...def,
140
+ attrs: { ...def.attrs, id },
141
+ });
142
+ return id;
143
+ },
144
+ resolveAssetUrl(assetId) {
145
+ return editor.resolveAssetUrl(assetId);
146
+ },
147
+ };
148
+
149
+ const exportedShapeIds: ShapeId[] = [];
150
+ const shapeNodes: SvgElementNode[] = [];
151
+ const clipPathIdByShapeId = new Map<ShapeId, string | null>();
152
+ for (const shape of shapes) {
153
+ const exporter = exporters.get(shape.type);
154
+ if (!exporter) {
155
+ skippedShapes.push({ id: shape.id, reason: "missing-exporter" });
156
+ continue;
157
+ }
158
+
159
+ const node = exporter.toSvg(shape, context);
160
+ if (!node) {
161
+ skippedShapes.push({ id: shape.id, reason: "empty-output" });
162
+ continue;
163
+ }
164
+
165
+ const exportBounds = editor.computeShapeExportBounds(shape);
166
+ if (!isValidBox(exportBounds)) {
167
+ skippedShapes.push({ id: shape.id, reason: "missing-bounds" });
168
+ continue;
169
+ }
170
+ const transform = editor.computeShapeExportToPageTransform(shape);
171
+
172
+ exportedShapeIds.push(shape.id);
173
+ const clipPathIds = getAncestorExportClipPathIds(
174
+ editor,
175
+ shape,
176
+ traversalShapeIdSet,
177
+ clipPathIdByShapeId,
178
+ context,
179
+ );
180
+ let shapeNode: SvgElementNode = {
181
+ tag: "g",
182
+ attrs: {
183
+ transform: transform.toSvgString(),
184
+ opacity: shape.opacity === 1 ? undefined : shape.opacity,
185
+ },
186
+ children: [node],
187
+ };
188
+
189
+ for (let index = clipPathIds.length - 1; index >= 0; index -= 1) {
190
+ shapeNode = {
191
+ tag: "g",
192
+ attrs: {
193
+ "clip-path": `url(#${clipPathIds[index]})`,
194
+ },
195
+ children: [shapeNode],
196
+ };
197
+ }
198
+
199
+ shapeNodes.push(shapeNode);
200
+ }
201
+
202
+ if (exportedShapeIds.length === 0) {
203
+ return null;
204
+ }
205
+
206
+ const width = Math.max(1, bounds.w);
207
+ const height = Math.max(1, bounds.h);
208
+ const children: SvgNode[] = [];
209
+ if (defs.length > 0) {
210
+ children.push({ tag: "defs", children: defs });
211
+ }
212
+ if (options.background) {
213
+ children.push({
214
+ tag: "rect",
215
+ attrs: {
216
+ x: bounds.x,
217
+ y: bounds.y,
218
+ width: bounds.w,
219
+ height: bounds.h,
220
+ fill: options.background,
221
+ },
222
+ });
223
+ }
224
+ children.push(...shapeNodes);
225
+
226
+ const svg = renderSvgNode({
227
+ tag: "svg",
228
+ attrs: {
229
+ xmlns: "http://www.w3.org/2000/svg",
230
+ width,
231
+ height,
232
+ viewBox: `${formatNumber(bounds.x)} ${formatNumber(
233
+ bounds.y,
234
+ )} ${formatNumber(width)} ${formatNumber(height)}`,
235
+ },
236
+ children,
237
+ });
238
+
239
+ return {
240
+ svg,
241
+ bounds,
242
+ width,
243
+ height,
244
+ requestedShapeIds,
245
+ exportedShapeIds,
246
+ skippedShapes,
247
+ };
248
+ }
249
+
250
+ function getClippedShapeExportPageBounds(
251
+ editor: Editor,
252
+ shape: ShapeRecord,
253
+ traversalShapeIdSet: Set<ShapeId>,
254
+ ): Box | null {
255
+ let bounds = editor.computeShapeExportPageBounds(shape);
256
+ if (!isValidBox(bounds)) {
257
+ return bounds;
258
+ }
259
+
260
+ for (const clipBounds of getAncestorExportClipPageBounds(
261
+ editor,
262
+ shape,
263
+ traversalShapeIdSet,
264
+ )) {
265
+ const clippedBounds = intersectBoxes(bounds, clipBounds);
266
+ if (!clippedBounds) {
267
+ return null;
268
+ }
269
+ bounds = clippedBounds;
270
+ }
271
+
272
+ return bounds;
273
+ }
274
+
275
+ function getAncestorExportClipPageBounds(
276
+ editor: Editor,
277
+ shape: ShapeRecord,
278
+ traversalShapeIdSet: Set<ShapeId>,
279
+ ): Box[] {
280
+ const bounds: Box[] = [];
281
+ for (const ancestor of getExportClipAncestors(
282
+ editor,
283
+ shape,
284
+ traversalShapeIdSet,
285
+ )) {
286
+ const clipBounds = getShapeChildExportClipPageBounds(editor, ancestor);
287
+ if (clipBounds && isValidBox(clipBounds)) {
288
+ bounds.push(clipBounds);
289
+ }
290
+ }
291
+ return bounds;
292
+ }
293
+
294
+ function getAncestorExportClipPathIds(
295
+ editor: Editor,
296
+ shape: ShapeRecord,
297
+ traversalShapeIdSet: Set<ShapeId>,
298
+ clipPathIdByShapeId: Map<ShapeId, string | null>,
299
+ context: SvgExportContext,
300
+ ): string[] {
301
+ const clipPathIds: string[] = [];
302
+ for (const ancestor of getExportClipAncestors(
303
+ editor,
304
+ shape,
305
+ traversalShapeIdSet,
306
+ )) {
307
+ const clipPathId = getShapeChildExportClipPathId(
308
+ editor,
309
+ ancestor,
310
+ clipPathIdByShapeId,
311
+ context,
312
+ );
313
+ if (clipPathId) {
314
+ clipPathIds.push(clipPathId);
315
+ }
316
+ }
317
+ return clipPathIds;
318
+ }
319
+
320
+ function getExportClipAncestors(
321
+ editor: Editor,
322
+ shape: ShapeRecord,
323
+ traversalShapeIdSet: Set<ShapeId>,
324
+ ): ShapeRecord[] {
325
+ const ancestors: ShapeRecord[] = [];
326
+ const seen = new Set<ShapeId>();
327
+ let parentId = shape.parentId;
328
+
329
+ while (parentId.startsWith("shape:")) {
330
+ const ancestorId = parentId as ShapeId;
331
+ if (seen.has(ancestorId)) {
332
+ break;
333
+ }
334
+ seen.add(ancestorId);
335
+
336
+ const ancestor = editor.getShape(ancestorId);
337
+ if (!ancestor) {
338
+ break;
339
+ }
340
+ if (traversalShapeIdSet.has(ancestor.id)) {
341
+ ancestors.push(ancestor);
342
+ }
343
+ parentId = ancestor.parentId;
344
+ }
345
+
346
+ return ancestors.reverse();
347
+ }
348
+
349
+ function getShapeChildExportClipPathId(
350
+ editor: Editor,
351
+ shape: ShapeRecord,
352
+ clipPathIdByShapeId: Map<ShapeId, string | null>,
353
+ context: SvgExportContext,
354
+ ): string | null {
355
+ if (clipPathIdByShapeId.has(shape.id)) {
356
+ return clipPathIdByShapeId.get(shape.id) ?? null;
357
+ }
358
+
359
+ const util = editor.getShapeUtil(shape);
360
+ const clipBounds = util.getChildExportClipLocalBounds(shape);
361
+ if (!clipBounds || !isValidBox(clipBounds)) {
362
+ clipPathIdByShapeId.set(shape.id, null);
363
+ return null;
364
+ }
365
+
366
+ const id = context.addDef({
367
+ tag: "clipPath",
368
+ attrs: {
369
+ clipPathUnits: "userSpaceOnUse",
370
+ },
371
+ children: [
372
+ {
373
+ tag: "rect",
374
+ attrs: {
375
+ x: clipBounds.x,
376
+ y: clipBounds.y,
377
+ width: clipBounds.w,
378
+ height: clipBounds.h,
379
+ transform: editor
380
+ .computeShapeExportToPageTransform(shape)
381
+ .toSvgString(),
382
+ },
383
+ },
384
+ ],
385
+ });
386
+ clipPathIdByShapeId.set(shape.id, id);
387
+ return id;
388
+ }
389
+
390
+ function getShapeChildExportClipPageBounds(
391
+ editor: Editor,
392
+ shape: ShapeRecord,
393
+ ): Box | null {
394
+ const util = editor.getShapeUtil(shape);
395
+ const clipBounds = util.getChildExportClipLocalBounds(shape);
396
+ if (!clipBounds || !isValidBox(clipBounds)) {
397
+ return null;
398
+ }
399
+
400
+ const transform = editor.computeShapeExportToPageTransform(shape);
401
+ return Box.FromPoints(
402
+ [
403
+ new Vec(clipBounds.minX, clipBounds.minY),
404
+ new Vec(clipBounds.maxX, clipBounds.minY),
405
+ new Vec(clipBounds.maxX, clipBounds.maxY),
406
+ new Vec(clipBounds.minX, clipBounds.maxY),
407
+ ].map((point) => transform.applyToPoint(point)),
408
+ );
409
+ }
410
+
411
+ function intersectBoxes(a: Box, b: Box): Box | null {
412
+ const minX = Math.max(a.minX, b.minX);
413
+ const minY = Math.max(a.minY, b.minY);
414
+ const maxX = Math.min(a.maxX, b.maxX);
415
+ const maxY = Math.min(a.maxY, b.maxY);
416
+ if (maxX < minX || maxY < minY) {
417
+ return null;
418
+ }
419
+ return new Box(minX, minY, maxX - minX, maxY - minY);
420
+ }
421
+
422
+ export function renderSvgNode(node: SvgNode): string {
423
+ if (typeof node === "string") {
424
+ return escapeText(node);
425
+ }
426
+
427
+ const attrs = renderSvgAttributes(node.attrs ?? {});
428
+ const children = node.children?.map(renderSvgNode).join("") ?? "";
429
+ if (!children) {
430
+ return `<${node.tag}${attrs}/>`;
431
+ }
432
+ return `<${node.tag}${attrs}>${children}</${node.tag}>`;
433
+ }
434
+
435
+ function unionBoxes(boxes: Box[]): Box {
436
+ let minX = Infinity;
437
+ let minY = Infinity;
438
+ let maxX = -Infinity;
439
+ let maxY = -Infinity;
440
+
441
+ for (const box of boxes) {
442
+ minX = Math.min(minX, box.minX);
443
+ minY = Math.min(minY, box.minY);
444
+ maxX = Math.max(maxX, box.maxX);
445
+ maxY = Math.max(maxY, box.maxY);
446
+ }
447
+
448
+ return new Box(minX, minY, maxX - minX, maxY - minY);
449
+ }
450
+
451
+ function isValidBox(box: Box): boolean {
452
+ return (
453
+ Number.isFinite(box.x) &&
454
+ Number.isFinite(box.y) &&
455
+ Number.isFinite(box.w) &&
456
+ Number.isFinite(box.h) &&
457
+ box.w >= 0 &&
458
+ box.h >= 0
459
+ );
460
+ }
461
+
462
+ function renderSvgAttributes(attrs: Record<string, SvgAttributeValue>): string {
463
+ const parts: string[] = [];
464
+ for (const [key, value] of Object.entries(attrs)) {
465
+ if (value === undefined || value === null || value === false) {
466
+ continue;
467
+ }
468
+ parts.push(
469
+ `${key}="${escapeAttribute(
470
+ typeof value === "number" ? formatNumber(value) : String(value),
471
+ )}"`,
472
+ );
473
+ }
474
+ return parts.length === 0 ? "" : ` ${parts.join(" ")}`;
475
+ }
476
+
477
+ function escapeAttribute(value: string): string {
478
+ return value
479
+ .replaceAll("&", "&amp;")
480
+ .replaceAll("\"", "&quot;")
481
+ .replaceAll("<", "&lt;")
482
+ .replaceAll(">", "&gt;");
483
+ }
484
+
485
+ function escapeText(value: string): string {
486
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
487
+ }
488
+
489
+ function formatNumber(value: number): string {
490
+ if (!Number.isFinite(value)) {
491
+ return "0";
492
+ }
493
+ return Number(value.toFixed(4)).toString();
494
+ }