canvu-react 0.3.7 → 0.3.8
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 +192 -0
- package/dist/chatbot.d.cts +1 -1
- package/dist/chatbot.d.ts +1 -1
- package/dist/index.cjs +32 -144
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +32 -144
- package/dist/index.js.map +1 -1
- package/dist/native.cjs +31 -109
- package/dist/native.cjs.map +1 -1
- package/dist/native.js +31 -109
- package/dist/native.js.map +1 -1
- package/dist/react.cjs +1388 -1353
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +147 -4
- package/dist/react.d.ts +147 -4
- package/dist/react.js +1388 -1354
- package/dist/react.js.map +1 -1
- package/dist/realtime.d.cts +2 -2
- package/dist/realtime.d.ts +2 -2
- package/dist/tldraw.cjs +30 -142
- package/dist/tldraw.cjs.map +1 -1
- package/dist/tldraw.js +30 -142
- package/dist/tldraw.js.map +1 -1
- package/dist/{types-B_rv7p8b.d.cts → types-CW146bKP.d.cts} +117 -1
- package/dist/{types-BCtWx3zP.d.ts → types-CpqlbbCP.d.ts} +117 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,198 @@ import { realtimeCollaborationPlugin } from "canvu/plugins/realtime";
|
|
|
112
112
|
|
|
113
113
|
No `CanvasPluginHost` is required. The plugin runtime lives inside `VectorViewport`.
|
|
114
114
|
|
|
115
|
+
## Persisting built-in file uploads
|
|
116
|
+
|
|
117
|
+
If you want to keep the native `VectorViewport` file picker and drag-and-drop
|
|
118
|
+
UX while also saving the original binary to your own backend or object storage,
|
|
119
|
+
pass `assetStore`.
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { useState } from "react";
|
|
123
|
+
import {
|
|
124
|
+
type VectorViewportAssetStore,
|
|
125
|
+
useVectorCanvasDocument,
|
|
126
|
+
VectorCanvas,
|
|
127
|
+
VectorToolbar,
|
|
128
|
+
VectorViewport,
|
|
129
|
+
} from "canvu/react";
|
|
130
|
+
|
|
131
|
+
export function Board() {
|
|
132
|
+
const [toolId, setToolId] = useState("hand");
|
|
133
|
+
const [toolLocked, setToolLocked] = useState(false);
|
|
134
|
+
const doc = useVectorCanvasDocument({ persistenceKey: "board.v1" });
|
|
135
|
+
|
|
136
|
+
const assetStore: VectorViewportAssetStore = {
|
|
137
|
+
async upload({ file, kind }) {
|
|
138
|
+
const form = new FormData();
|
|
139
|
+
form.append("file", file);
|
|
140
|
+
form.append("kind", kind);
|
|
141
|
+
|
|
142
|
+
const response = await fetch("/api/canvu/assets", {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: form,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const asset = await response.json();
|
|
148
|
+
return {
|
|
149
|
+
pluginData: {
|
|
150
|
+
assetId: asset.id,
|
|
151
|
+
assetKey: asset.key,
|
|
152
|
+
mimeType: file.type,
|
|
153
|
+
originalFileName: file.name,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
async resolve({ assetIds }) {
|
|
158
|
+
const response = await fetch("/api/canvu/assets/resolve", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "content-type": "application/json" },
|
|
161
|
+
body: JSON.stringify({ assetIds }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return response.json();
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!doc.isHydrated) return null;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<VectorCanvas.Root style={{ height: "100dvh", width: "100%" }}>
|
|
172
|
+
<VectorCanvas.Body>
|
|
173
|
+
<VectorCanvas.Main>
|
|
174
|
+
<VectorCanvas.ViewportSurface>
|
|
175
|
+
<VectorViewport
|
|
176
|
+
ariaLabel="Board"
|
|
177
|
+
toolId={toolId}
|
|
178
|
+
toolLocked={toolLocked}
|
|
179
|
+
onToolChangeRequest={setToolId}
|
|
180
|
+
items={doc.items}
|
|
181
|
+
onItemsChange={doc.onItemsChange}
|
|
182
|
+
interactive
|
|
183
|
+
assetStore={assetStore}
|
|
184
|
+
toolbar={
|
|
185
|
+
<VectorCanvas.Toolbar>
|
|
186
|
+
<VectorToolbar
|
|
187
|
+
value={toolId}
|
|
188
|
+
onChange={setToolId}
|
|
189
|
+
toolLocked={toolLocked}
|
|
190
|
+
onToolLockedChange={setToolLocked}
|
|
191
|
+
aria-label="Canvas tools"
|
|
192
|
+
/>
|
|
193
|
+
</VectorCanvas.Toolbar>
|
|
194
|
+
}
|
|
195
|
+
/>
|
|
196
|
+
</VectorCanvas.ViewportSurface>
|
|
197
|
+
</VectorCanvas.Main>
|
|
198
|
+
</VectorCanvas.Body>
|
|
199
|
+
</VectorCanvas.Root>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
canvu still owns the conversion from `File` to `VectorSceneItem`. Your
|
|
205
|
+
`assetStore.upload(...)` only intercepts the original browser file and returns
|
|
206
|
+
persistable `pluginData`, which canvu merges into every created item from that
|
|
207
|
+
file. `resolve(...)` is optional and is intended for custom persistence
|
|
208
|
+
adapters that need to rehydrate signed URLs after loading a snapshot.
|
|
209
|
+
|
|
210
|
+
## Reusing the same ingestion pipeline outside the viewport
|
|
211
|
+
|
|
212
|
+
When files originate outside the `VectorViewport` UI, such as a board creation
|
|
213
|
+
wizard, a custom import modal, or a migration routine, use
|
|
214
|
+
`ingestAssetFilesToSceneItems(...)`.
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
import { useRef, useState } from "react";
|
|
218
|
+
import {
|
|
219
|
+
ingestAssetFilesToSceneItems,
|
|
220
|
+
type VectorViewportAssetStore,
|
|
221
|
+
useVectorCanvasDocument,
|
|
222
|
+
VectorViewport,
|
|
223
|
+
} from "canvu/react";
|
|
224
|
+
|
|
225
|
+
export function BoardImport() {
|
|
226
|
+
const [toolId, setToolId] = useState("hand");
|
|
227
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
228
|
+
const doc = useVectorCanvasDocument({ persistenceKey: "board.v1" });
|
|
229
|
+
|
|
230
|
+
const assetStore: VectorViewportAssetStore = {
|
|
231
|
+
async upload({ file, kind }) {
|
|
232
|
+
const form = new FormData();
|
|
233
|
+
form.append("file", file);
|
|
234
|
+
form.append("kind", kind);
|
|
235
|
+
|
|
236
|
+
const response = await fetch("/api/canvu/assets", {
|
|
237
|
+
method: "POST",
|
|
238
|
+
body: form,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const asset = await response.json();
|
|
242
|
+
return {
|
|
243
|
+
pluginData: {
|
|
244
|
+
assetId: asset.id,
|
|
245
|
+
assetKey: asset.key,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
async function handleImport(files: FileList | null) {
|
|
252
|
+
if (!files) return;
|
|
253
|
+
|
|
254
|
+
const result = await ingestAssetFilesToSceneItems({
|
|
255
|
+
files: Array.from(files),
|
|
256
|
+
worldCenter: { x: 0, y: 0 },
|
|
257
|
+
assetStore,
|
|
258
|
+
decorateItem(item) {
|
|
259
|
+
return {
|
|
260
|
+
...item,
|
|
261
|
+
locked: true,
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
doc.onItemsChange([...doc.items, ...result.items]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!doc.isHydrated) return null;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<>
|
|
273
|
+
<button type="button" onClick={() => inputRef.current?.click()}>
|
|
274
|
+
Import files
|
|
275
|
+
</button>
|
|
276
|
+
|
|
277
|
+
<input
|
|
278
|
+
ref={inputRef}
|
|
279
|
+
type="file"
|
|
280
|
+
accept="image/*,application/pdf"
|
|
281
|
+
multiple
|
|
282
|
+
hidden
|
|
283
|
+
onChange={(event) => {
|
|
284
|
+
void handleImport(event.target.files);
|
|
285
|
+
event.target.value = "";
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
|
|
289
|
+
<VectorViewport
|
|
290
|
+
ariaLabel="Board"
|
|
291
|
+
items={doc.items}
|
|
292
|
+
onItemsChange={doc.onItemsChange}
|
|
293
|
+
toolId={toolId}
|
|
294
|
+
onToolChangeRequest={setToolId}
|
|
295
|
+
interactive
|
|
296
|
+
assetStore={assetStore}
|
|
297
|
+
/>
|
|
298
|
+
</>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
This helper is the same ingestion layer used internally by the native file
|
|
304
|
+
tool, so external imports do not need to reimplement PDF rasterization, local
|
|
305
|
+
blob persistence, or `pluginData` attachment.
|
|
306
|
+
|
|
115
307
|
## Custom tools
|
|
116
308
|
|
|
117
309
|
Use `createToolPlugin(...)` for isolated tools.
|
package/dist/chatbot.d.cts
CHANGED
package/dist/chatbot.d.ts
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -371,7 +371,7 @@ function attachViewportInput(options) {
|
|
|
371
371
|
if (e.ctrlKey || e.metaKey) {
|
|
372
372
|
e.preventDefault();
|
|
373
373
|
const dy = wheelDeltaYPixels(e);
|
|
374
|
-
const normDy = dy < 20 ? dy * 12 : dy;
|
|
374
|
+
const normDy = Math.abs(dy) < 20 ? dy * 12 : dy;
|
|
375
375
|
const factor = Math.exp(-normDy * wheelZoomSensitivity);
|
|
376
376
|
const rect = element.getBoundingClientRect();
|
|
377
377
|
camera.setZoom(camera.zoom * factor, {
|
|
@@ -616,89 +616,6 @@ function createCustomShapeItem(id, bounds, content) {
|
|
|
616
616
|
};
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
// src/scene/freehand-path.ts
|
|
620
|
-
function dedupeFreehandPoints(points, minDist) {
|
|
621
|
-
if (points.length <= 2) {
|
|
622
|
-
return points.map((p) => ({ ...p }));
|
|
623
|
-
}
|
|
624
|
-
const minSq = minDist * minDist;
|
|
625
|
-
const first = points[0];
|
|
626
|
-
if (!first) return [];
|
|
627
|
-
const out = [{ ...first }];
|
|
628
|
-
for (let i = 1; i < points.length - 1; i++) {
|
|
629
|
-
const p = points[i];
|
|
630
|
-
const last = out[out.length - 1];
|
|
631
|
-
if (!p || !last) continue;
|
|
632
|
-
const dx = p.x - last.x;
|
|
633
|
-
const dy = p.y - last.y;
|
|
634
|
-
if (dx * dx + dy * dy >= minSq) {
|
|
635
|
-
out.push({ ...p });
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
const end = points[points.length - 1];
|
|
639
|
-
const lastKept = out[out.length - 1];
|
|
640
|
-
if (!end || !lastKept) return out;
|
|
641
|
-
if ((end.x - lastKept.x) ** 2 + (end.y - lastKept.y) ** 2 > 1e-12) {
|
|
642
|
-
out.push({ ...end });
|
|
643
|
-
}
|
|
644
|
-
return out;
|
|
645
|
-
}
|
|
646
|
-
function smoothFreehandPointsToPathD(points) {
|
|
647
|
-
const n = points.length;
|
|
648
|
-
if (n === 0) return "";
|
|
649
|
-
if (n === 1) {
|
|
650
|
-
const p = points[0];
|
|
651
|
-
if (!p) return "";
|
|
652
|
-
return `M ${p.x} ${p.y}`;
|
|
653
|
-
}
|
|
654
|
-
if (n === 2) {
|
|
655
|
-
const a = points[0];
|
|
656
|
-
const b = points[1];
|
|
657
|
-
if (!a || !b) return "";
|
|
658
|
-
return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
|
|
659
|
-
}
|
|
660
|
-
const p0 = points[0];
|
|
661
|
-
if (!p0) return "";
|
|
662
|
-
let d = `M ${p0.x} ${p0.y}`;
|
|
663
|
-
let i = 1;
|
|
664
|
-
for (; i < n - 2; i++) {
|
|
665
|
-
const pi = points[i];
|
|
666
|
-
const pi1 = points[i + 1];
|
|
667
|
-
if (!pi || !pi1) continue;
|
|
668
|
-
const xc = (pi.x + pi1.x) / 2;
|
|
669
|
-
const yc = (pi.y + pi1.y) / 2;
|
|
670
|
-
d += ` Q ${pi.x} ${pi.y} ${xc} ${yc}`;
|
|
671
|
-
}
|
|
672
|
-
const pLast = points[i];
|
|
673
|
-
const pEnd = points[i + 1];
|
|
674
|
-
if (!pLast || !pEnd) return d;
|
|
675
|
-
d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
|
|
676
|
-
return d;
|
|
677
|
-
}
|
|
678
|
-
function outlineStrokeToClosedPathD(outline) {
|
|
679
|
-
const len = outline.length;
|
|
680
|
-
if (len === 0) return "";
|
|
681
|
-
const first = outline[0];
|
|
682
|
-
if (!first) return "";
|
|
683
|
-
if (len < 3) {
|
|
684
|
-
let d2 = `M ${first[0]} ${first[1]}`;
|
|
685
|
-
for (let i = 1; i < len; i++) {
|
|
686
|
-
const pt = outline[i];
|
|
687
|
-
if (!pt) continue;
|
|
688
|
-
d2 += ` L ${pt[0]} ${pt[1]}`;
|
|
689
|
-
}
|
|
690
|
-
return `${d2} Z`;
|
|
691
|
-
}
|
|
692
|
-
let d = `M ${first[0]} ${first[1]} Q`;
|
|
693
|
-
for (let i = 0; i < len; i++) {
|
|
694
|
-
const p0 = outline[i];
|
|
695
|
-
const p1 = outline[(i + 1) % len];
|
|
696
|
-
if (!p0 || !p1) continue;
|
|
697
|
-
d += ` ${p0[0]} ${p0[1]} ${(p0[0] + p1[0]) / 2} ${(p0[1] + p1[1]) / 2}`;
|
|
698
|
-
}
|
|
699
|
-
return `${d} Z`;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
619
|
// src/scene/text-svg.ts
|
|
703
620
|
function escapeSvgTextContent(s) {
|
|
704
621
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -819,7 +736,7 @@ var DEFAULT_STROKE_STYLE = {
|
|
|
819
736
|
strokeWidth: 2
|
|
820
737
|
};
|
|
821
738
|
var TOOL_FREEHAND_DEFAULTS = {
|
|
822
|
-
draw: { strokeWidth:
|
|
739
|
+
draw: { strokeWidth: 10 },
|
|
823
740
|
pencil: { strokeWidth: 3 },
|
|
824
741
|
brush: { strokeWidth: 10 },
|
|
825
742
|
marker: { stroke: "#fde047", strokeWidth: 16, strokeOpacity: 0.5 }
|
|
@@ -836,18 +753,18 @@ function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware =
|
|
|
836
753
|
...base,
|
|
837
754
|
size: Math.max(2, sw * 1.05),
|
|
838
755
|
thinning: 0.42,
|
|
839
|
-
smoothing: 0.
|
|
840
|
-
streamline: 0.
|
|
841
|
-
simulatePressure:
|
|
756
|
+
smoothing: 0.78,
|
|
757
|
+
streamline: 0.62,
|
|
758
|
+
simulatePressure: true
|
|
842
759
|
};
|
|
843
760
|
}
|
|
844
761
|
return {
|
|
845
762
|
...base,
|
|
846
763
|
size: Math.max(2, sw * 1.18),
|
|
847
764
|
thinning: 0.12,
|
|
848
|
-
smoothing: 0.
|
|
849
|
-
streamline: 0.
|
|
850
|
-
simulatePressure:
|
|
765
|
+
smoothing: 0.85,
|
|
766
|
+
streamline: 0.78,
|
|
767
|
+
simulatePressure: true
|
|
851
768
|
};
|
|
852
769
|
}
|
|
853
770
|
if (toolKind === "brush") {
|
|
@@ -865,7 +782,7 @@ function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware =
|
|
|
865
782
|
thinning: 0.08,
|
|
866
783
|
smoothing: 0.88,
|
|
867
784
|
streamline: 0.84,
|
|
868
|
-
simulatePressure:
|
|
785
|
+
simulatePressure: true
|
|
869
786
|
};
|
|
870
787
|
}
|
|
871
788
|
function resolveStrokeStyle(item) {
|
|
@@ -935,70 +852,41 @@ function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeCompl
|
|
|
935
852
|
if (pathPointsLocal.length === 1) {
|
|
936
853
|
const p = pathPointsLocal[0];
|
|
937
854
|
if (!p) return null;
|
|
938
|
-
const r = Math.max(0.5, style.strokeWidth / 2);
|
|
939
|
-
return {
|
|
940
|
-
kind: "circle",
|
|
941
|
-
cx: p.x,
|
|
942
|
-
cy: p.y,
|
|
943
|
-
r,
|
|
944
|
-
fill: style.stroke,
|
|
945
|
-
fillOpacity: style.strokeOpacity
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
const minDist = Math.min(0.25, Math.max(0.02, style.strokeWidth * 0.02));
|
|
949
|
-
const pts = dedupeFreehandPoints(pathPointsLocal, minDist);
|
|
950
|
-
if (pts.length === 0) return null;
|
|
951
|
-
if (pts.length === 1) {
|
|
952
|
-
const p = pts[0];
|
|
953
|
-
if (!p) return null;
|
|
954
|
-
const r = Math.max(0.5, style.strokeWidth / 2);
|
|
955
855
|
return {
|
|
956
856
|
kind: "circle",
|
|
957
857
|
cx: p.x,
|
|
958
858
|
cy: p.y,
|
|
959
|
-
r,
|
|
859
|
+
r: Math.max(0.5, style.strokeWidth / 2),
|
|
960
860
|
fill: style.stroke,
|
|
961
861
|
fillOpacity: style.strokeOpacity
|
|
962
862
|
};
|
|
963
863
|
}
|
|
964
|
-
const hasPressure =
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
kind: "strokePath",
|
|
969
|
-
d: d2,
|
|
970
|
-
stroke: style.stroke,
|
|
971
|
-
strokeWidth: style.strokeWidth,
|
|
972
|
-
strokeOpacity: style.strokeOpacity
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
const input = hasPressure ? pts.map(
|
|
864
|
+
const hasPressure = pathPointsLocal.some(
|
|
865
|
+
(p) => p.pressure != null && Number.isFinite(p.pressure)
|
|
866
|
+
);
|
|
867
|
+
const input = hasPressure ? pathPointsLocal.map(
|
|
976
868
|
(p) => [p.x, p.y, Math.min(1, Math.max(0, p.pressure ?? 0.5))]
|
|
977
|
-
) :
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
fill: style.stroke,
|
|
992
|
-
fillOpacity: style.strokeOpacity
|
|
993
|
-
};
|
|
869
|
+
) : pathPointsLocal.map((p) => [p.x, p.y]);
|
|
870
|
+
const stroke = getStroke__default.default(
|
|
871
|
+
input,
|
|
872
|
+
perfectFreehandOptions(toolKind, style, strokeComplete, hasPressure)
|
|
873
|
+
);
|
|
874
|
+
if (stroke.length < 3) return null;
|
|
875
|
+
const first = stroke[0];
|
|
876
|
+
if (!first) return null;
|
|
877
|
+
let d = `M ${first[0]} ${first[1]} Q`;
|
|
878
|
+
for (let i = 0; i < stroke.length; i++) {
|
|
879
|
+
const a = stroke[i];
|
|
880
|
+
const b = stroke[(i + 1) % stroke.length];
|
|
881
|
+
if (!a || !b) continue;
|
|
882
|
+
d += ` ${a[0]} ${a[1]} ${(a[0] + b[0]) / 2} ${(a[1] + b[1]) / 2}`;
|
|
994
883
|
}
|
|
995
|
-
|
|
884
|
+
d += " Z";
|
|
996
885
|
return {
|
|
997
|
-
kind: "
|
|
886
|
+
kind: "fillPath",
|
|
998
887
|
d,
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
strokeOpacity: style.strokeOpacity
|
|
888
|
+
fill: style.stroke,
|
|
889
|
+
fillOpacity: style.strokeOpacity
|
|
1002
890
|
};
|
|
1003
891
|
}
|
|
1004
892
|
function freehandPayloadToSvgString(payload) {
|