@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.
- package/README.md +145 -0
- package/_vendor/canvas-core/bindings/BindingUtil.ts +92 -0
- package/_vendor/canvas-core/bindings/BindingUtilRegistry.ts +22 -0
- package/_vendor/canvas-core/bindings/arrow/arrowBindingHelpers.ts +52 -0
- package/_vendor/canvas-core/contextMenu.ts +85 -0
- package/_vendor/canvas-core/editor/Editor.ts +2614 -0
- package/_vendor/canvas-core/editor/HistoryManager.ts +83 -0
- package/_vendor/canvas-core/editor/StateNode.ts +162 -0
- package/_vendor/canvas-core/editor/camera.ts +6 -0
- package/_vendor/canvas-core/editor/clipboard.ts +478 -0
- package/_vendor/canvas-core/editor/commands.ts +833 -0
- package/_vendor/canvas-core/editor/edgeScroll.ts +37 -0
- package/_vendor/canvas-core/editor/editorComputedHelpers.ts +257 -0
- package/_vendor/canvas-core/editor/editorSelectionTransforms.ts +116 -0
- package/_vendor/canvas-core/editor/events.ts +93 -0
- package/_vendor/canvas-core/editor/index.ts +12 -0
- package/_vendor/canvas-core/editor/interactions.ts +101 -0
- package/_vendor/canvas-core/editor/internal.ts +22 -0
- package/_vendor/canvas-core/editor/managers/BindingManager.ts +262 -0
- package/_vendor/canvas-core/editor/managers/CameraManager.ts +147 -0
- package/_vendor/canvas-core/editor/managers/ClipboardManager.ts +32 -0
- package/_vendor/canvas-core/editor/managers/DocumentSnapshotManager.ts +256 -0
- package/_vendor/canvas-core/editor/managers/InputsManager.ts +319 -0
- package/_vendor/canvas-core/editor/managers/InteractionTransientManager.ts +218 -0
- package/_vendor/canvas-core/editor/managers/QueryManager.ts +837 -0
- package/_vendor/canvas-core/editor/managers/RenderWindowManager.ts +347 -0
- package/_vendor/canvas-core/editor/managers/SelectionLayoutManager.ts +196 -0
- package/_vendor/canvas-core/editor/managers/SelectionManager.ts +98 -0
- package/_vendor/canvas-core/editor/managers/SelectionOverlayManager.ts +308 -0
- package/_vendor/canvas-core/editor/managers/ShapeEditingManager.ts +226 -0
- package/_vendor/canvas-core/editor/managers/ShapeGeometryManager.ts +162 -0
- package/_vendor/canvas-core/editor/managers/ShapeHierarchyManager.ts +797 -0
- package/_vendor/canvas-core/editor/managers/SideEffectManager.ts +83 -0
- package/_vendor/canvas-core/editor/managers/SnapManager.ts +433 -0
- package/_vendor/canvas-core/editor/managers/SnapSnapshotBuilder.ts +430 -0
- package/_vendor/canvas-core/editor/managers/SpatialIndexManager.ts +354 -0
- package/_vendor/canvas-core/editor/managers/ToolManager.ts +81 -0
- package/_vendor/canvas-core/editor/managers/queryState.ts +583 -0
- package/_vendor/canvas-core/editor/managers/selectionGeometry.ts +287 -0
- package/_vendor/canvas-core/editor/managers/shapeOrder.ts +350 -0
- package/_vendor/canvas-core/editor/managers/shapeReparenting.ts +160 -0
- package/_vendor/canvas-core/editor/managers/snapCalculations.ts +412 -0
- package/_vendor/canvas-core/editor/managers/snapGeometry.ts +202 -0
- package/_vendor/canvas-core/editor/managers/snapIndicators.ts +403 -0
- package/_vendor/canvas-core/editor/managers/snapTypes.ts +122 -0
- package/_vendor/canvas-core/editor/queries/shapeTransforms.ts +171 -0
- package/_vendor/canvas-core/editor/renderWindow.ts +439 -0
- package/_vendor/canvas-core/editor/resizeGeometry.ts +654 -0
- package/_vendor/canvas-core/editor/selection.ts +489 -0
- package/_vendor/canvas-core/editor/selectionHandleInteractions.ts +43 -0
- package/_vendor/canvas-core/editor/selectionOverlay.ts +567 -0
- package/_vendor/canvas-core/exports/integrity.ts +174 -0
- package/_vendor/canvas-core/exports/svg.ts +494 -0
- package/_vendor/canvas-core/geometry/Box.ts +80 -0
- package/_vendor/canvas-core/geometry/Ellipse2d.ts +67 -0
- package/_vendor/canvas-core/geometry/Geometry2d.ts +52 -0
- package/_vendor/canvas-core/geometry/Mat.ts +92 -0
- package/_vendor/canvas-core/geometry/Polyline2d.ts +63 -0
- package/_vendor/canvas-core/geometry/Rectangle2d.ts +64 -0
- package/_vendor/canvas-core/geometry/Vec.ts +123 -0
- package/_vendor/canvas-core/geometry/index.ts +13 -0
- package/_vendor/canvas-core/geometry/segments.ts +65 -0
- package/_vendor/canvas-core/index.ts +391 -0
- package/_vendor/canvas-core/plugins/index.ts +5 -0
- package/_vendor/canvas-core/plugins/types.ts +53 -0
- package/_vendor/canvas-core/reactivity.ts +16 -0
- package/_vendor/canvas-core/schema/records.ts +221 -0
- package/_vendor/canvas-core/schema/snapshots.ts +527 -0
- package/_vendor/canvas-core/shapes/ShapeUtil.ts +516 -0
- package/_vendor/canvas-core/shapes/ShapeUtilRegistry.ts +26 -0
- package/_vendor/canvas-core/shapes/capabilities.ts +67 -0
- package/_vendor/canvas-core/shapes/types.ts +20 -0
- package/_vendor/canvas-core/toolbar.ts +148 -0
- package/_vendor/canvas-core/tools/hand/HandTool.ts +75 -0
- package/_vendor/canvas-core/tools/index.ts +2 -0
- package/_vendor/canvas-core/tools/select/SelectTool.ts +33 -0
- package/_vendor/canvas-core/tools/select/selectContainment.ts +104 -0
- package/_vendor/canvas-core/tools/select/selectToolHelpers.ts +801 -0
- package/_vendor/canvas-core/tools/select/states/BrushingState.ts +145 -0
- package/_vendor/canvas-core/tools/select/states/DraggingSelectionHandleInteractionState.ts +116 -0
- package/_vendor/canvas-core/tools/select/states/IdleState.ts +159 -0
- package/_vendor/canvas-core/tools/select/states/PointingCanvasState.ts +41 -0
- package/_vendor/canvas-core/tools/select/states/PointingResizeHandleState.ts +78 -0
- package/_vendor/canvas-core/tools/select/states/PointingRotateHandleState.ts +64 -0
- package/_vendor/canvas-core/tools/select/states/PointingSelectionHandleInteractionState.ts +34 -0
- package/_vendor/canvas-core/tools/select/states/PointingShapeState.ts +140 -0
- package/_vendor/canvas-core/tools/select/states/ResizingState.ts +171 -0
- package/_vendor/canvas-core/tools/select/states/RotatingState.ts +124 -0
- package/_vendor/canvas-core/tools/select/states/SelectStateNode.ts +128 -0
- package/_vendor/canvas-core/tools/select/states/TranslatingState.ts +139 -0
- package/_vendor/canvas-core/tools/select/states/index.ts +11 -0
- package/_vendor/canvas-plugin-api/feature.ts +204 -0
- package/_vendor/canvas-plugin-api/index.ts +142 -0
- package/_vendor/canvas-product-react/api/apiRegistry.ts +35 -0
- package/_vendor/canvas-product-react/api/canvasProductApi.ts +422 -0
- package/_vendor/canvas-product-react/api/index.ts +31 -0
- package/_vendor/canvas-product-react/capabilities/clipboard/clipboardService.ts +36 -0
- package/_vendor/canvas-product-react/capabilities/clipboard/systemClipboard.ts +245 -0
- package/_vendor/canvas-product-react/capabilities/context-menu/systemContextMenu.ts +396 -0
- package/_vendor/canvas-product-react/capabilities/context-menu/useContextMenuSurface.ts +59 -0
- package/_vendor/canvas-product-react/capabilities/export/useSvgExport.ts +156 -0
- package/_vendor/canvas-product-react/capabilities/history/useHistoryControls.ts +23 -0
- package/_vendor/canvas-product-react/capabilities/pages/usePageControls.ts +41 -0
- package/_vendor/canvas-product-react/capabilities/toolbar/systemToolbar.ts +490 -0
- package/_vendor/canvas-product-react/capabilities/toolbar/useToolbarSurfaces.ts +92 -0
- package/_vendor/canvas-product-react/capabilities/zoom/useZoomControls.ts +27 -0
- package/_vendor/canvas-product-react/index.ts +84 -0
- package/_vendor/canvas-product-react/persistence/index.ts +18 -0
- package/_vendor/canvas-product-react/persistence/localSnapshotPersistence.ts +294 -0
- package/_vendor/canvas-product-react/runtime/CanvasProductRoot.tsx +250 -0
- package/_vendor/canvas-product-react/runtime/productDefinition.ts +72 -0
- package/_vendor/canvas-product-react/runtime/productServices.ts +96 -0
- package/_vendor/canvas-react/components/CanvasView.tsx +796 -0
- package/_vendor/canvas-react/components/OverlayLayer.tsx +879 -0
- package/_vendor/canvas-react/components/PageSpace.tsx +34 -0
- package/_vendor/canvas-react/components/ShapeLayer.tsx +428 -0
- package/_vendor/canvas-react/components/index.ts +4 -0
- package/_vendor/canvas-react/hooks/useEditor.ts +11 -0
- package/_vendor/canvas-react/hooks/useValue.ts +14 -0
- package/_vendor/canvas-react/host/CanvasRendererRegistryContext.tsx +14 -0
- package/_vendor/canvas-react/host/contextMenuRegistry.ts +61 -0
- package/_vendor/canvas-react/host/domEventGuards.ts +61 -0
- package/_vendor/canvas-react/host/index.ts +38 -0
- package/_vendor/canvas-react/host/pluginHost.ts +635 -0
- package/_vendor/canvas-react/host/rendererRegistry.tsx +58 -0
- package/_vendor/canvas-react/host/toolbarRegistry.ts +100 -0
- package/_vendor/canvas-react/index.ts +3 -0
- package/_vendor/plugin-arrow/ArrowBindingUtil.ts +239 -0
- package/_vendor/plugin-arrow/ArrowShapeUtil.ts +311 -0
- package/_vendor/plugin-arrow/ArrowTool.ts +208 -0
- package/_vendor/plugin-arrow/arrowGeometry.ts +441 -0
- package/_vendor/plugin-arrow/arrowTerminalBindingResolver.ts +253 -0
- package/_vendor/plugin-arrow/arrowTerminalCommands.ts +230 -0
- package/_vendor/plugin-arrow/canvas.tsx +92 -0
- package/_vendor/plugin-arrow/core.ts +409 -0
- package/_vendor/plugin-arrow/feature.ts +20 -0
- package/_vendor/plugin-arrow/index.ts +28 -0
- package/_vendor/plugin-arrow/selectionHandleInteractions.ts +154 -0
- package/_vendor/plugin-arrow/types.ts +63 -0
- package/_vendor/plugin-draw/DrawShapeRenderer.tsx +60 -0
- package/_vendor/plugin-draw/DrawShapeUtil.ts +372 -0
- package/_vendor/plugin-draw/DrawTool.ts +444 -0
- package/_vendor/plugin-draw/canvas.tsx +24 -0
- package/_vendor/plugin-draw/core.ts +515 -0
- package/_vendor/plugin-draw/drawGeometry.ts +491 -0
- package/_vendor/plugin-draw/drawHitTest.ts +271 -0
- package/_vendor/plugin-draw/drawInterpolation.ts +250 -0
- package/_vendor/plugin-draw/drawPath.ts +550 -0
- package/_vendor/plugin-draw/drawSegments.ts +106 -0
- package/_vendor/plugin-draw/drawSelectionTransform.ts +67 -0
- package/_vendor/plugin-draw/drawShapeGeometry.ts +307 -0
- package/_vendor/plugin-draw/drawStrokeGeometry.ts +454 -0
- package/_vendor/plugin-draw/feature.ts +20 -0
- package/_vendor/plugin-draw/index.ts +31 -0
- package/_vendor/plugin-draw/types.ts +36 -0
- package/_vendor/plugin-eraser/EraserTool.ts +297 -0
- package/_vendor/plugin-eraser/canvas.tsx +616 -0
- package/_vendor/plugin-eraser/core.ts +49 -0
- package/_vendor/plugin-eraser/feature.ts +20 -0
- package/_vendor/plugin-eraser/index.ts +4 -0
- package/_vendor/plugin-frame/FrameShapeUtil.ts +173 -0
- package/_vendor/plugin-frame/FrameTool.ts +225 -0
- package/_vendor/plugin-frame/canvas.tsx +75 -0
- package/_vendor/plugin-frame/core.ts +534 -0
- package/_vendor/plugin-frame/feature.ts +20 -0
- package/_vendor/plugin-frame/index.ts +6 -0
- package/_vendor/plugin-geo/GeoShapeUtil.ts +463 -0
- package/_vendor/plugin-geo/GeoTools.ts +247 -0
- package/_vendor/plugin-geo/canvas.tsx +90 -0
- package/_vendor/plugin-geo/core.ts +495 -0
- package/_vendor/plugin-geo/feature.ts +20 -0
- package/_vendor/plugin-geo/index.ts +7 -0
- package/_vendor/plugin-geo/types.ts +27 -0
- package/_vendor/plugin-group/GroupShapeUtil.ts +59 -0
- package/_vendor/plugin-group/canvas.tsx +59 -0
- package/_vendor/plugin-group/core.ts +40 -0
- package/_vendor/plugin-group/feature.ts +20 -0
- package/_vendor/plugin-group/index.ts +5 -0
- package/_vendor/plugin-image/ImageShapeUtil.ts +95 -0
- package/_vendor/plugin-image/canvas.tsx +212 -0
- package/_vendor/plugin-image/core.ts +542 -0
- package/_vendor/plugin-image/crop.ts +770 -0
- package/_vendor/plugin-image/cropInteractions.ts +350 -0
- package/_vendor/plugin-image/cropSession.ts +38 -0
- package/_vendor/plugin-image/feature.ts +20 -0
- package/_vendor/plugin-image/index.ts +29 -0
- package/_vendor/plugin-image/types.ts +55 -0
- package/_vendor/plugin-image-product-react/ImageImportController.tsx +143 -0
- package/_vendor/plugin-image-product-react/imageAssetService.ts +52 -0
- package/_vendor/plugin-image-product-react/imageBlobStore.ts +315 -0
- package/_vendor/plugin-image-product-react/imageHostActions.ts +438 -0
- package/_vendor/plugin-image-product-react/imageProductApi.ts +88 -0
- package/_vendor/plugin-image-product-react/imageToolbarHostBridge.ts +48 -0
- package/_vendor/plugin-image-product-react/imageToolbarRuntime.ts +21 -0
- package/_vendor/plugin-image-product-react/index.ts +60 -0
- package/_vendor/plugin-image-product-react/snapshotGuards.ts +33 -0
- package/_vendor/plugin-image-product-react/svgImageAssetInliner.ts +54 -0
- package/_vendor/plugin-image-product-react/useImageImport.ts +64 -0
- package/_vendor/plugin-laser/LaserTool.ts +154 -0
- package/_vendor/plugin-laser/canvas.tsx +645 -0
- package/_vendor/plugin-laser/core.ts +49 -0
- package/_vendor/plugin-laser/feature.ts +20 -0
- package/_vendor/plugin-laser/index.ts +4 -0
- package/_vendor/plugin-text/ArrowLabelBindingUtil.ts +336 -0
- package/_vendor/plugin-text/GeoLabelBindingUtil.ts +391 -0
- package/_vendor/plugin-text/TextShapeSvgExporter.ts +101 -0
- package/_vendor/plugin-text/TextShapeUtil.ts +284 -0
- package/_vendor/plugin-text/TextTool.ts +228 -0
- package/_vendor/plugin-text/canvas.tsx +115 -0
- package/_vendor/plugin-text/core.ts +422 -0
- package/_vendor/plugin-text/feature.ts +20 -0
- package/_vendor/plugin-text/host/RichTextEditingBox.tsx +144 -0
- package/_vendor/plugin-text/host/RichTextToolbar.tsx +266 -0
- package/_vendor/plugin-text/host/TiptapTextEditor.tsx +191 -0
- package/_vendor/plugin-text/host/richTextRendering.tsx +49 -0
- package/_vendor/plugin-text/host/textDomSelection.ts +93 -0
- package/_vendor/plugin-text/host/textEditingBoxStyle.ts +36 -0
- package/_vendor/plugin-text/host/textEntryFocus.ts +307 -0
- package/_vendor/plugin-text/host/textFontLoader.ts +149 -0
- package/_vendor/plugin-text/host/textMeasurement.ts +258 -0
- package/_vendor/plugin-text/host/textMeasurementSync.ts +77 -0
- package/_vendor/plugin-text/host/textRangeSelection.ts +134 -0
- package/_vendor/plugin-text/host/textShapeRendererRuntime.tsx +89 -0
- package/_vendor/plugin-text/host/textShapeSync.ts +173 -0
- package/_vendor/plugin-text/host/tiptapTextEditorController.ts +148 -0
- package/_vendor/plugin-text/index.ts +18 -0
- package/_vendor/plugin-text/richTextUtils.ts +259 -0
- package/_vendor/plugin-text/textInteractions.ts +366 -0
- package/_vendor/plugin-text/textLabelHelpers.ts +235 -0
- package/_vendor/plugin-text/textShapeHelpers.ts +123 -0
- package/_vendor/plugin-text/textStyle.ts +96 -0
- package/_vendor/plugin-text/textTypes.ts +48 -0
- package/_vendor/signals/atom.ts +74 -0
- package/_vendor/signals/computed.ts +132 -0
- package/_vendor/signals/core.ts +130 -0
- package/_vendor/signals/effect.ts +117 -0
- package/_vendor/signals/index.ts +9 -0
- package/_vendor/signals/types.ts +17 -0
- package/_vendor/store/Store.ts +171 -0
- package/_vendor/store/diff.ts +229 -0
- package/_vendor/store/history.ts +177 -0
- package/_vendor/store/index.ts +32 -0
- package/_vendor/store/types.ts +37 -0
- package/api/canvasKitApi.d.ts +29 -0
- package/index.css +3 -0
- package/index.d.ts +16 -0
- package/index.mjs +1 -0
- package/internal-modules.d.ts +187 -0
- package/package.json +48 -0
- package/product/Shell.d.ts +19 -0
- package/product/chromeContext.d.ts +70 -0
- package/product/createCanvasKitEditor.d.ts +6 -0
- package/product/index.d.ts +7 -0
- package/product/persistence.d.ts +3 -0
- package/product/product.d.ts +22 -0
- package/product/useCanvasClipboardShortcuts.d.ts +6 -0
- package/product-iyBbEPIs.mjs +4 -0
- package/product.mjs +1 -0
- package/standard/extensions.d.ts +1 -0
- package/standard/index.d.ts +3 -0
- package/standard/standardApi.d.ts +91 -0
- package/standard-BmrxwqpJ.mjs +44 -0
- package/standard.mjs +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { Editor, ShapeId, ShapeRecord } from "@land/canvas-core";
|
|
2
|
+
import {
|
|
3
|
+
createTextShape,
|
|
4
|
+
getDefaultTextShapeProps,
|
|
5
|
+
isTextShapeRecord,
|
|
6
|
+
} from "./textShapeHelpers";
|
|
7
|
+
import type { TextShapeProps, TextShapeRecord } from "./textTypes";
|
|
8
|
+
|
|
9
|
+
export const ARROW_LABEL_BINDING_TYPE = "arrow-label";
|
|
10
|
+
export const GEO_LABEL_BINDING_TYPE = "geo-label";
|
|
11
|
+
|
|
12
|
+
export type TextLabelBindingType =
|
|
13
|
+
| typeof ARROW_LABEL_BINDING_TYPE
|
|
14
|
+
| typeof GEO_LABEL_BINDING_TYPE;
|
|
15
|
+
|
|
16
|
+
export interface TextLabelBehavior {
|
|
17
|
+
selectionTarget: "label" | "owner";
|
|
18
|
+
translateTarget: "label" | "owner";
|
|
19
|
+
contributesToSelectionFrame: boolean;
|
|
20
|
+
canUseSelectionFrameHandles: boolean;
|
|
21
|
+
canPlaceCaretWhenOwnerSelected: boolean;
|
|
22
|
+
showsCompanionFrameWhenOwnerSelected: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Bound text labels need four independent answers:
|
|
26
|
+
// what gets selected on click, what actually moves on drag, whether the
|
|
27
|
+
// label can still enter caret editing while its owner is the current selection,
|
|
28
|
+
// and whether owner selection should still surface a companion border for the label.
|
|
29
|
+
// Keeping that policy in one table avoids split-brain behavior between
|
|
30
|
+
// TextShapeUtil and textInteractions.
|
|
31
|
+
const TEXT_LABEL_BEHAVIOR_BY_BINDING_TYPE = {
|
|
32
|
+
[ARROW_LABEL_BINDING_TYPE]: {
|
|
33
|
+
selectionTarget: "owner",
|
|
34
|
+
translateTarget: "owner",
|
|
35
|
+
contributesToSelectionFrame: true,
|
|
36
|
+
canUseSelectionFrameHandles: false,
|
|
37
|
+
canPlaceCaretWhenOwnerSelected: true,
|
|
38
|
+
showsCompanionFrameWhenOwnerSelected: true,
|
|
39
|
+
},
|
|
40
|
+
[GEO_LABEL_BINDING_TYPE]: {
|
|
41
|
+
selectionTarget: "owner",
|
|
42
|
+
translateTarget: "owner",
|
|
43
|
+
contributesToSelectionFrame: false,
|
|
44
|
+
canUseSelectionFrameHandles: true,
|
|
45
|
+
canPlaceCaretWhenOwnerSelected: true,
|
|
46
|
+
showsCompanionFrameWhenOwnerSelected: false,
|
|
47
|
+
},
|
|
48
|
+
} satisfies Record<TextLabelBindingType, TextLabelBehavior>;
|
|
49
|
+
|
|
50
|
+
export interface TextLabelTargetInfo {
|
|
51
|
+
label: TextShapeRecord;
|
|
52
|
+
ownerId: ShapeId;
|
|
53
|
+
bindingType: TextLabelBindingType;
|
|
54
|
+
behavior: TextLabelBehavior;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TextLabelCreateOptions {
|
|
58
|
+
bindingType: TextLabelBindingType;
|
|
59
|
+
owner: ShapeRecord;
|
|
60
|
+
historyMarkId: string;
|
|
61
|
+
index: string;
|
|
62
|
+
layout: Pick<TextShapeRecord, "x" | "y" | "rotation"> & {
|
|
63
|
+
props: Pick<TextShapeProps, "w"> & Partial<TextShapeProps>;
|
|
64
|
+
};
|
|
65
|
+
bindingProps?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getTextLabelShapeForOwner(
|
|
69
|
+
editor: Editor,
|
|
70
|
+
ownerId: ShapeId,
|
|
71
|
+
bindingType: TextLabelBindingType,
|
|
72
|
+
): TextShapeRecord | null {
|
|
73
|
+
const binding = editor
|
|
74
|
+
.getBindingsToShape(ownerId)
|
|
75
|
+
.find((candidate) => candidate.type === bindingType);
|
|
76
|
+
if (!binding) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const label = editor.getShape(binding.fromId);
|
|
81
|
+
return isTextShapeRecord(label) ? label : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createBoundTextLabelShape(
|
|
85
|
+
editor: Editor,
|
|
86
|
+
options: TextLabelCreateOptions,
|
|
87
|
+
): TextShapeRecord {
|
|
88
|
+
const defaults = getDefaultTextShapeProps(editor);
|
|
89
|
+
editor.history.mark(options.historyMarkId);
|
|
90
|
+
const label = createTextShape(
|
|
91
|
+
editor,
|
|
92
|
+
{
|
|
93
|
+
x: options.layout.x,
|
|
94
|
+
y: options.layout.y,
|
|
95
|
+
parentId: options.owner.parentId,
|
|
96
|
+
index: options.index,
|
|
97
|
+
rotation: options.layout.rotation,
|
|
98
|
+
props: {
|
|
99
|
+
...defaults,
|
|
100
|
+
...options.layout.props,
|
|
101
|
+
autoSize: false,
|
|
102
|
+
textAlign: "middle",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
"user",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
editor.commands.createBinding(
|
|
109
|
+
{
|
|
110
|
+
type: options.bindingType,
|
|
111
|
+
fromId: label.id,
|
|
112
|
+
toId: options.owner.id,
|
|
113
|
+
props: options.bindingProps,
|
|
114
|
+
},
|
|
115
|
+
{ source: "user" },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return label;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getTextLabelTargetInfo(
|
|
122
|
+
editor: Editor,
|
|
123
|
+
shapeId: ShapeId,
|
|
124
|
+
): TextLabelTargetInfo | null {
|
|
125
|
+
const label = editor.getShape(shapeId);
|
|
126
|
+
if (!isTextShapeRecord(label)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ownerBinding = editor
|
|
131
|
+
.getBindingsFromShape(shapeId)
|
|
132
|
+
.find((binding) => binding.type in TEXT_LABEL_BEHAVIOR_BY_BINDING_TYPE);
|
|
133
|
+
if (!ownerBinding) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const bindingType = ownerBinding.type as TextLabelBindingType;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
label,
|
|
140
|
+
ownerId: ownerBinding.toId,
|
|
141
|
+
bindingType,
|
|
142
|
+
behavior: TEXT_LABEL_BEHAVIOR_BY_BINDING_TYPE[bindingType],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getTextLabelBehavior(
|
|
147
|
+
bindingType: TextLabelBindingType,
|
|
148
|
+
): TextLabelBehavior {
|
|
149
|
+
return TEXT_LABEL_BEHAVIOR_BY_BINDING_TYPE[bindingType];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getTextLabelSelectionTargetId(
|
|
153
|
+
editor: Editor,
|
|
154
|
+
shapeId: ShapeId,
|
|
155
|
+
): ShapeId | null {
|
|
156
|
+
const target = getTextLabelTargetInfo(editor, shapeId);
|
|
157
|
+
if (!target) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return target.behavior.selectionTarget === "owner" ? target.ownerId : shapeId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getTextLabelTranslateTargetId(
|
|
164
|
+
editor: Editor,
|
|
165
|
+
shapeId: ShapeId,
|
|
166
|
+
): ShapeId | null {
|
|
167
|
+
const target = getTextLabelTargetInfo(editor, shapeId);
|
|
168
|
+
if (!target) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return target.behavior.translateTarget === "owner" ? target.ownerId : shapeId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function textLabelContributesToSelectionFrame(
|
|
175
|
+
editor: Editor,
|
|
176
|
+
shapeId: ShapeId,
|
|
177
|
+
): boolean | null {
|
|
178
|
+
return getTextLabelTargetInfo(editor, shapeId)?.behavior
|
|
179
|
+
.contributesToSelectionFrame ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function textLabelCanUseSelectionFrameHandles(
|
|
183
|
+
editor: Editor,
|
|
184
|
+
shapeId: ShapeId,
|
|
185
|
+
): boolean | null {
|
|
186
|
+
return getTextLabelTargetInfo(editor, shapeId)?.behavior
|
|
187
|
+
.canUseSelectionFrameHandles ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function textLabelShowsCompanionFrameWhenOwnerSelected(
|
|
191
|
+
editor: Editor,
|
|
192
|
+
shapeId: ShapeId,
|
|
193
|
+
): boolean | null {
|
|
194
|
+
return getTextLabelTargetInfo(editor, shapeId)?.behavior
|
|
195
|
+
.showsCompanionFrameWhenOwnerSelected ?? null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function canPlaceCaretInTextShapeFromSelection(
|
|
199
|
+
editor: Editor,
|
|
200
|
+
shapeId: ShapeId,
|
|
201
|
+
): boolean {
|
|
202
|
+
const selectedShapeIds = editor.getSelectedShapeIds();
|
|
203
|
+
if (selectedShapeIds.length !== 1) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
if (selectedShapeIds[0] === shapeId) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const target = getTextLabelTargetInfo(editor, shapeId);
|
|
211
|
+
return target
|
|
212
|
+
? target.behavior.canPlaceCaretWhenOwnerSelected &&
|
|
213
|
+
selectedShapeIds[0] === target.ownerId
|
|
214
|
+
: false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function startEditingTextLabel(
|
|
218
|
+
editor: Editor,
|
|
219
|
+
label: TextShapeRecord,
|
|
220
|
+
options: {
|
|
221
|
+
editMarkId: string;
|
|
222
|
+
createMarkId: string;
|
|
223
|
+
isExisting: boolean;
|
|
224
|
+
},
|
|
225
|
+
): void {
|
|
226
|
+
editor.startEditingShape(
|
|
227
|
+
{
|
|
228
|
+
shapeId: label.id,
|
|
229
|
+
entryMode: "selectAll",
|
|
230
|
+
markId: options.isExisting ? options.editMarkId : options.createMarkId,
|
|
231
|
+
reuseExistingMark: !options.isExisting,
|
|
232
|
+
},
|
|
233
|
+
"user",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BaseRecord,
|
|
3
|
+
ChangeSource,
|
|
4
|
+
Editor,
|
|
5
|
+
ShapeId,
|
|
6
|
+
ShapeParentId,
|
|
7
|
+
} from "@land/canvas-core";
|
|
8
|
+
import { isShapeRecord } from "@land/canvas-core";
|
|
9
|
+
import { createRichTextFromPlainText } from "./richTextUtils";
|
|
10
|
+
import type {
|
|
11
|
+
RichTextDocument,
|
|
12
|
+
TextShapeProps,
|
|
13
|
+
TextShapeRecord,
|
|
14
|
+
} from "./textTypes";
|
|
15
|
+
|
|
16
|
+
export interface CreateTextShapeInput {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
props?: Partial<TextShapeProps>;
|
|
20
|
+
parentId?: ShapeParentId;
|
|
21
|
+
index?: string;
|
|
22
|
+
rotation?: number;
|
|
23
|
+
scaleX?: number;
|
|
24
|
+
scaleY?: number;
|
|
25
|
+
meta?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isTextShapeRecord(
|
|
29
|
+
record: BaseRecord | null | undefined,
|
|
30
|
+
): record is TextShapeRecord {
|
|
31
|
+
return isShapeRecord(record) && record.type === "text";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function cloneTextShapeProps(props: TextShapeProps): TextShapeProps {
|
|
35
|
+
return {
|
|
36
|
+
richText: props.richText,
|
|
37
|
+
w: props.w,
|
|
38
|
+
h: props.h,
|
|
39
|
+
autoSize: props.autoSize,
|
|
40
|
+
scale: props.scale,
|
|
41
|
+
font: props.font,
|
|
42
|
+
size: props.size,
|
|
43
|
+
color: props.color,
|
|
44
|
+
textAlign: props.textAlign,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getDefaultTextShapeProps(editor: Editor): TextShapeProps {
|
|
49
|
+
return cloneTextShapeProps(editor.getDefaultShapeProps<TextShapeProps>("text"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createTextShape(
|
|
53
|
+
editor: Editor,
|
|
54
|
+
input: CreateTextShapeInput,
|
|
55
|
+
source: ChangeSource = "user",
|
|
56
|
+
): TextShapeRecord {
|
|
57
|
+
const defaults = getDefaultTextShapeProps(editor);
|
|
58
|
+
const props: TextShapeProps = {
|
|
59
|
+
...defaults,
|
|
60
|
+
...input.props,
|
|
61
|
+
richText: input.props?.richText ?? createRichTextFromPlainText(""),
|
|
62
|
+
};
|
|
63
|
+
const [created] = editor.commands.createShapes(
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
x: input.x,
|
|
68
|
+
y: input.y,
|
|
69
|
+
parentId: input.parentId,
|
|
70
|
+
index: input.index,
|
|
71
|
+
rotation: input.rotation,
|
|
72
|
+
scaleX: input.scaleX,
|
|
73
|
+
scaleY: input.scaleY,
|
|
74
|
+
meta: input.meta,
|
|
75
|
+
props: {
|
|
76
|
+
richText: props.richText,
|
|
77
|
+
w: props.w,
|
|
78
|
+
h: props.h,
|
|
79
|
+
autoSize: props.autoSize,
|
|
80
|
+
scale: props.scale,
|
|
81
|
+
font: props.font,
|
|
82
|
+
size: props.size,
|
|
83
|
+
color: props.color,
|
|
84
|
+
textAlign: props.textAlign,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
{ source },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!isTextShapeRecord(created)) {
|
|
92
|
+
throw new Error("Failed to create text shape");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return created;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function updateTextShapeRichText(
|
|
99
|
+
editor: Editor,
|
|
100
|
+
shapeId: ShapeId,
|
|
101
|
+
richText: RichTextDocument,
|
|
102
|
+
options?: { w?: number; h?: number },
|
|
103
|
+
source: ChangeSource = "user",
|
|
104
|
+
): void {
|
|
105
|
+
const shape = editor.getShape(shapeId);
|
|
106
|
+
if (!isTextShapeRecord(shape)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
editor.commands.updateShapes(
|
|
111
|
+
[
|
|
112
|
+
{
|
|
113
|
+
id: shape.id,
|
|
114
|
+
props: {
|
|
115
|
+
richText,
|
|
116
|
+
...(typeof options?.w === "number" ? { w: options.w } : null),
|
|
117
|
+
...(typeof options?.h === "number" ? { h: options.h } : null),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
{ source },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TextShapeAlign,
|
|
3
|
+
TextShapeFont,
|
|
4
|
+
TextShapeRecord,
|
|
5
|
+
TextShapeSize,
|
|
6
|
+
} from "./textTypes";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_TEXT_FONT: TextShapeFont = "noto-sans-sc";
|
|
9
|
+
|
|
10
|
+
const TEXT_SIZE_METRICS = {
|
|
11
|
+
s: {
|
|
12
|
+
fontSize: 14,
|
|
13
|
+
lineHeight: 20,
|
|
14
|
+
},
|
|
15
|
+
m: {
|
|
16
|
+
fontSize: 16,
|
|
17
|
+
lineHeight: 24,
|
|
18
|
+
},
|
|
19
|
+
l: {
|
|
20
|
+
fontSize: 20,
|
|
21
|
+
lineHeight: 28,
|
|
22
|
+
},
|
|
23
|
+
xl: {
|
|
24
|
+
fontSize: 28,
|
|
25
|
+
lineHeight: 36,
|
|
26
|
+
},
|
|
27
|
+
} as const satisfies Record<
|
|
28
|
+
TextShapeSize,
|
|
29
|
+
{
|
|
30
|
+
fontSize: number;
|
|
31
|
+
lineHeight: number;
|
|
32
|
+
}
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
export function getTextFontFamily(font: TextShapeFont): string {
|
|
36
|
+
switch (font) {
|
|
37
|
+
case "noto-sans-sc":
|
|
38
|
+
return "'Noto Sans SC', Inter, ui-sans-serif, system-ui, sans-serif";
|
|
39
|
+
case "noto-serif-sc":
|
|
40
|
+
return "'Noto Serif SC', Georgia, Cambria, serif";
|
|
41
|
+
case "noto-sans-mono":
|
|
42
|
+
return "'Noto Sans Mono', 'SFMono-Regular', Consolas, monospace";
|
|
43
|
+
case "ma-shan-zheng":
|
|
44
|
+
return "'Ma Shan Zheng', 'Comic Sans MS', 'Marker Felt', cursive";
|
|
45
|
+
default:
|
|
46
|
+
return "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getTextFontSize(size: TextShapeSize): number {
|
|
51
|
+
return TEXT_SIZE_METRICS[size].fontSize;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getTextLineHeight(size: TextShapeSize): number {
|
|
55
|
+
return TEXT_SIZE_METRICS[size].lineHeight;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getTextMinHeight(
|
|
59
|
+
size: TextShapeSize,
|
|
60
|
+
scale = 1,
|
|
61
|
+
): number {
|
|
62
|
+
return Math.max(1, Math.ceil(getTextLineHeight(size) * scale));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getTextAlign(align: TextShapeAlign): "left" | "center" | "right" {
|
|
66
|
+
switch (align) {
|
|
67
|
+
case "middle":
|
|
68
|
+
return "center";
|
|
69
|
+
case "end":
|
|
70
|
+
return "right";
|
|
71
|
+
case "start":
|
|
72
|
+
default:
|
|
73
|
+
return "left";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getTextAnchorX(
|
|
78
|
+
align: TextShapeRecord["props"]["textAlign"],
|
|
79
|
+
width: number,
|
|
80
|
+
): number {
|
|
81
|
+
switch (align) {
|
|
82
|
+
case "middle":
|
|
83
|
+
return width / 2;
|
|
84
|
+
case "end":
|
|
85
|
+
return width;
|
|
86
|
+
case "start":
|
|
87
|
+
default:
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getSvgTextAnchor(
|
|
93
|
+
align: TextShapeRecord["props"]["textAlign"],
|
|
94
|
+
): "start" | "middle" | "end" {
|
|
95
|
+
return align;
|
|
96
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ShapeRecordBase } from "@land/canvas-core";
|
|
2
|
+
import type { JSONContent } from "@tiptap/core";
|
|
3
|
+
|
|
4
|
+
export type RichTextJsonValue =
|
|
5
|
+
| string
|
|
6
|
+
| number
|
|
7
|
+
| boolean
|
|
8
|
+
| null
|
|
9
|
+
| RichTextJsonValue[]
|
|
10
|
+
| { [key: string]: RichTextJsonValue };
|
|
11
|
+
|
|
12
|
+
export interface RichTextDocument {
|
|
13
|
+
type: "doc";
|
|
14
|
+
attrs?: Record<string, RichTextJsonValue>;
|
|
15
|
+
content: JSONContent[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type AssertTiptapJsonContent<T extends JSONContent> = T;
|
|
19
|
+
type _RichTextDocumentTiptapCompatibilityCheck =
|
|
20
|
+
AssertTiptapJsonContent<RichTextDocument>;
|
|
21
|
+
|
|
22
|
+
export type TextShapeFont =
|
|
23
|
+
| "noto-sans-sc"
|
|
24
|
+
| "noto-serif-sc"
|
|
25
|
+
| "noto-sans-mono"
|
|
26
|
+
| "ma-shan-zheng";
|
|
27
|
+
export type TextShapeSize = "s" | "m" | "l" | "xl";
|
|
28
|
+
export type TextShapeAlign = "start" | "middle" | "end";
|
|
29
|
+
|
|
30
|
+
export interface TextShapeProps {
|
|
31
|
+
richText: RichTextDocument;
|
|
32
|
+
w: number;
|
|
33
|
+
h: number;
|
|
34
|
+
autoSize: boolean;
|
|
35
|
+
scale: number;
|
|
36
|
+
font: TextShapeFont;
|
|
37
|
+
size: TextShapeSize;
|
|
38
|
+
color: string;
|
|
39
|
+
textAlign: TextShapeAlign;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TextShapeRecord extends ShapeRecordBase<
|
|
43
|
+
"text",
|
|
44
|
+
TextShapeProps
|
|
45
|
+
> {
|
|
46
|
+
type: "text";
|
|
47
|
+
props: TextShapeProps;
|
|
48
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Child, Signal } from "./types";
|
|
2
|
+
import {
|
|
3
|
+
advanceGlobalEpoch,
|
|
4
|
+
atomDidChange,
|
|
5
|
+
getGlobalEpoch,
|
|
6
|
+
maybeCaptureParent,
|
|
7
|
+
noteInitialValue,
|
|
8
|
+
getCurrentTransaction,
|
|
9
|
+
} from "./core";
|
|
10
|
+
|
|
11
|
+
export interface Atom<T> extends Signal<T> {
|
|
12
|
+
set(value: T): T;
|
|
13
|
+
update(updater: (value: T) => T): T;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class _Atom<T> implements Atom<T> {
|
|
17
|
+
lastChangedEpoch: number;
|
|
18
|
+
children = new Set<Child>();
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
public readonly name: string,
|
|
22
|
+
private current: T,
|
|
23
|
+
private isEqual?: (a: T, b: T) => boolean,
|
|
24
|
+
) {
|
|
25
|
+
this.lastChangedEpoch = getGlobalEpoch();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get(): T {
|
|
29
|
+
maybeCaptureParent(this);
|
|
30
|
+
return this.current;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
__unsafe__getWithoutCapture(): T {
|
|
34
|
+
return this.current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
__unsafe__setWithoutNotify(value: T): void {
|
|
38
|
+
this.current = value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set(value: T): T {
|
|
42
|
+
const previousValue = this.current;
|
|
43
|
+
if (
|
|
44
|
+
this.isEqual
|
|
45
|
+
? this.isEqual(previousValue, value)
|
|
46
|
+
: Object.is(previousValue, value)
|
|
47
|
+
) {
|
|
48
|
+
return previousValue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
noteInitialValue(this, previousValue);
|
|
52
|
+
advanceGlobalEpoch();
|
|
53
|
+
this.lastChangedEpoch = getGlobalEpoch();
|
|
54
|
+
this.current = value;
|
|
55
|
+
atomDidChange(this as any);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
update(updater: (value: T) => T): T {
|
|
60
|
+
return this.set(updater(this.current));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function atom<T>(
|
|
65
|
+
name: string,
|
|
66
|
+
initialValue: T,
|
|
67
|
+
opts?: { isEqual?: (a: T, b: T) => boolean },
|
|
68
|
+
): Atom<T> {
|
|
69
|
+
return new _Atom(name, initialValue, opts?.isEqual);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isAtom(value: any): value is Atom<any> {
|
|
73
|
+
return value instanceof _Atom;
|
|
74
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Child, Signal } from "./types";
|
|
2
|
+
import {
|
|
3
|
+
getGlobalEpoch,
|
|
4
|
+
advanceGlobalEpoch,
|
|
5
|
+
maybeCaptureParent,
|
|
6
|
+
startCapture,
|
|
7
|
+
stopCapture,
|
|
8
|
+
} from "./core";
|
|
9
|
+
|
|
10
|
+
const UNINITIALIZED: unique symbol = Symbol("UNINITIALIZED");
|
|
11
|
+
type UNINITIALIZED = typeof UNINITIALIZED;
|
|
12
|
+
|
|
13
|
+
export { UNINITIALIZED };
|
|
14
|
+
|
|
15
|
+
export interface Computed<T> extends Signal<T> {
|
|
16
|
+
readonly isComputed: true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class _Computed<T> implements Computed<T> {
|
|
20
|
+
readonly isComputed = true;
|
|
21
|
+
lastChangedEpoch: number;
|
|
22
|
+
lastCheckedEpoch: number;
|
|
23
|
+
children = new Set<Child>();
|
|
24
|
+
|
|
25
|
+
private state: T | UNINITIALIZED = UNINITIALIZED;
|
|
26
|
+
private parents: Signal<any>[] = [];
|
|
27
|
+
private parentEpochs: number[] = [];
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
public readonly name: string,
|
|
31
|
+
private derive: (prev: T | UNINITIALIZED) => T,
|
|
32
|
+
private isEqual?: (a: T, b: T) => boolean,
|
|
33
|
+
) {
|
|
34
|
+
this.lastChangedEpoch = getGlobalEpoch();
|
|
35
|
+
this.lastCheckedEpoch = -1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get(): T {
|
|
39
|
+
maybeCaptureParent(this);
|
|
40
|
+
return this.__unsafe__getWithoutCapture();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
__unsafe__getWithoutCapture(): T {
|
|
44
|
+
// Fast path: nothing changed globally since last check
|
|
45
|
+
if (this.lastCheckedEpoch === getGlobalEpoch()) {
|
|
46
|
+
return this.state as T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if parents actually changed
|
|
50
|
+
if (this.state !== UNINITIALIZED && !this.haveParentsChanged()) {
|
|
51
|
+
this.lastCheckedEpoch = getGlobalEpoch();
|
|
52
|
+
return this.state as T;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Recompute
|
|
56
|
+
startCapture(this.selfAsChild);
|
|
57
|
+
let newValue: T;
|
|
58
|
+
let newParentSet: Set<Signal<any>>;
|
|
59
|
+
try {
|
|
60
|
+
newValue = this.derive(
|
|
61
|
+
this.state === UNINITIALIZED ? UNINITIALIZED : this.state,
|
|
62
|
+
);
|
|
63
|
+
} finally {
|
|
64
|
+
newParentSet = stopCapture();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update parent tracking
|
|
68
|
+
// Remove this from old parents' children that are no longer dependencies
|
|
69
|
+
for (const oldParent of this.parents) {
|
|
70
|
+
if (!newParentSet.has(oldParent)) {
|
|
71
|
+
oldParent.children.delete(this.selfAsChild);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
this.parents = [...newParentSet];
|
|
75
|
+
this.parentEpochs = this.parents.map((p) => p.lastChangedEpoch);
|
|
76
|
+
// Add this to new parents' children
|
|
77
|
+
for (const parent of this.parents) {
|
|
78
|
+
parent.children.add(this.selfAsChild);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
this.state === UNINITIALIZED ||
|
|
83
|
+
!(this.isEqual
|
|
84
|
+
? this.isEqual(this.state, newValue)
|
|
85
|
+
: Object.is(this.state, newValue))
|
|
86
|
+
) {
|
|
87
|
+
this.state = newValue;
|
|
88
|
+
advanceGlobalEpoch();
|
|
89
|
+
this.lastChangedEpoch = getGlobalEpoch();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.lastCheckedEpoch = getGlobalEpoch();
|
|
93
|
+
return this.state as T;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private get selfAsChild(): Child {
|
|
97
|
+
// Cache the child identity so we can properly remove from parent sets
|
|
98
|
+
if (!this._selfAsChild) {
|
|
99
|
+
this._selfAsChild = {
|
|
100
|
+
type: "computed",
|
|
101
|
+
notify: () => {},
|
|
102
|
+
source: this,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return this._selfAsChild;
|
|
106
|
+
}
|
|
107
|
+
private _selfAsChild: Child | null = null;
|
|
108
|
+
|
|
109
|
+
private haveParentsChanged(): boolean {
|
|
110
|
+
for (let i = 0; i < this.parents.length; i++) {
|
|
111
|
+
const parent = this.parents[i];
|
|
112
|
+
// Force parent to re-evaluate if it's a computed
|
|
113
|
+
parent.__unsafe__getWithoutCapture();
|
|
114
|
+
if (parent.lastChangedEpoch !== this.parentEpochs[i]) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function computed<T>(
|
|
123
|
+
name: string,
|
|
124
|
+
derive: (prev: T | UNINITIALIZED) => T,
|
|
125
|
+
opts?: { isEqual?: (a: T, b: T) => boolean },
|
|
126
|
+
): Computed<T> {
|
|
127
|
+
return new _Computed(name, derive, opts?.isEqual);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isComputed(value: any): value is Computed<any> {
|
|
131
|
+
return value instanceof _Computed;
|
|
132
|
+
}
|