canvu-react 0.3.25 → 0.3.27

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.
@@ -1,5 +1,5 @@
1
- import { P as PlacementPreview, R as RemotePresenceMarkupStroke, A as RemotePresencePeer, D as RemotePresenceCamera, E as RealtimeConnectionState, q as VectorViewportHandle, V as VectorToolDefinition, r as VectorViewportProps, C as CanvasPlugin, e as CanvasPluginRenderContext } from './types-7kfWcm0L.cjs';
2
- export { F as PresenceOverlayRenderContext } from './types-7kfWcm0L.cjs';
1
+ import { P as PlacementPreview, R as RemotePresenceMarkupStroke, A as RemotePresencePeer, D as RemotePresenceCamera, E as RealtimeConnectionState, q as VectorViewportHandle, V as VectorToolDefinition, r as VectorViewportProps, C as CanvasPlugin, e as CanvasPluginRenderContext } from './types-BLXR7g_L.cjs';
2
+ export { F as PresenceOverlayRenderContext } from './types-BLXR7g_L.cjs';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { C as Camera2D } from './camera-BwQjm5oh.cjs';
5
5
  import { V as VectorSceneItem, R as Rect } from './types-CB0TZZuk.cjs';
@@ -1,5 +1,5 @@
1
- import { P as PlacementPreview, R as RemotePresenceMarkupStroke, A as RemotePresencePeer, D as RemotePresenceCamera, E as RealtimeConnectionState, q as VectorViewportHandle, V as VectorToolDefinition, r as VectorViewportProps, C as CanvasPlugin, e as CanvasPluginRenderContext } from './types-C4k_AMvi.js';
2
- export { F as PresenceOverlayRenderContext } from './types-C4k_AMvi.js';
1
+ import { P as PlacementPreview, R as RemotePresenceMarkupStroke, A as RemotePresencePeer, D as RemotePresenceCamera, E as RealtimeConnectionState, q as VectorViewportHandle, V as VectorToolDefinition, r as VectorViewportProps, C as CanvasPlugin, e as CanvasPluginRenderContext } from './types-Cm7IsgL4.js';
2
+ export { F as PresenceOverlayRenderContext } from './types-Cm7IsgL4.js';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { C as Camera2D } from './camera-KwCYYPhm.js';
5
5
  import { V as VectorSceneItem, R as Rect } from './types-CB0TZZuk.js';
package/dist/realtime.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { MousePointer2, MessageSquare, Sparkles, Hand, Square, Circle, Minus, ArrowUpRight, PenLine, Highlighter, Eraser, Type, Image } from 'lucide-react';
2
+ import getStroke from 'perfect-freehand';
2
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
4
  import { createContext, useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect, useContext } from 'react';
4
5
 
@@ -9,9 +10,13 @@ function remoteMarkupStrokeFromPlacementPreview(preview) {
9
10
  }
10
11
  const tool = preview.tool;
11
12
  const mapped = tool === "laser" || tool === "marker" || tool === "draw" ? tool : "draw";
13
+ const style = preview.style;
12
14
  return {
13
15
  points: preview.points,
14
- tool: mapped
16
+ tool: mapped,
17
+ ...style?.strokeWidth != null ? { strokeWidth: style.strokeWidth } : {},
18
+ ...style?.stroke ? { stroke: style.stroke } : {},
19
+ ...style?.strokeOpacity != null ? { strokeOpacity: style.strokeOpacity } : {}
15
20
  };
16
21
  }
17
22
 
@@ -67,6 +72,126 @@ function smoothFreehandPointsToPathD(points) {
67
72
  return d;
68
73
  }
69
74
 
75
+ // src/scene/custom-shape.ts
76
+ function expandCustomShapeTemplate(template, width, height) {
77
+ return template.replace(/\{\{w\}\}/g, String(width)).replace(/\{\{h\}\}/g, String(height)).replace(/\{\{width\}\}/g, String(width)).replace(/\{\{height\}\}/g, String(height));
78
+ }
79
+ function resolveCustomInner(content, size) {
80
+ if ("render" in content) {
81
+ return content.render(size);
82
+ }
83
+ return expandCustomShapeTemplate(content.svg, size.width, size.height);
84
+ }
85
+ function buildCustomShapeChildrenSvg(inner, intrinsic, bounds) {
86
+ const b = normalizeRect(bounds);
87
+ const sx = b.width / intrinsic.width;
88
+ const sy = b.height / intrinsic.height;
89
+ return `<g transform="scale(${sx},${sy})">${inner}</g>`;
90
+ }
91
+ function createCustomShapeItem(id, bounds, content) {
92
+ const r = normalizeRect(bounds);
93
+ const intrinsic = { width: r.width, height: r.height };
94
+ const inner = resolveCustomInner(content, intrinsic);
95
+ return {
96
+ id,
97
+ x: r.x,
98
+ y: r.y,
99
+ bounds: { ...r },
100
+ toolKind: "custom",
101
+ customIntrinsicSize: intrinsic,
102
+ customInnerSvg: inner,
103
+ childrenSvg: buildCustomShapeChildrenSvg(inner, intrinsic, r)
104
+ };
105
+ }
106
+
107
+ // src/scene/shape-builders.ts
108
+ function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware = false) {
109
+ const sw = style.strokeWidth;
110
+ const base2 = {
111
+ last: strokeComplete,
112
+ simulatePressure: true
113
+ };
114
+ if (toolKind === "draw" || toolKind === "pencil") {
115
+ if (pressureAware && toolKind === "draw") {
116
+ return {
117
+ ...base2,
118
+ size: Math.max(2, sw * 1.05),
119
+ thinning: 0.42,
120
+ smoothing: 0.78,
121
+ streamline: 0.62,
122
+ simulatePressure: true
123
+ };
124
+ }
125
+ return {
126
+ ...base2,
127
+ size: Math.max(2, sw * 1.18),
128
+ thinning: 0.12,
129
+ smoothing: 0.85,
130
+ streamline: 0.78,
131
+ simulatePressure: true
132
+ };
133
+ }
134
+ if (toolKind === "brush") {
135
+ return {
136
+ ...base2,
137
+ size: Math.max(4, sw * 1.22),
138
+ thinning: 0.52,
139
+ smoothing: 0.64,
140
+ streamline: 0.68
141
+ };
142
+ }
143
+ return {
144
+ ...base2,
145
+ size: Math.max(6, sw * 1.08),
146
+ thinning: 0.08,
147
+ smoothing: 0.88,
148
+ streamline: 0.84,
149
+ simulatePressure: true
150
+ };
151
+ }
152
+ function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeComplete = true) {
153
+ if (pathPointsLocal.length === 0) return null;
154
+ if (pathPointsLocal.length === 1) {
155
+ const p = pathPointsLocal[0];
156
+ if (!p) return null;
157
+ return {
158
+ kind: "circle",
159
+ cx: p.x,
160
+ cy: p.y,
161
+ r: Math.max(0.5, style.strokeWidth / 2),
162
+ fill: style.stroke,
163
+ fillOpacity: style.strokeOpacity
164
+ };
165
+ }
166
+ const hasPressure = pathPointsLocal.some(
167
+ (p) => p.pressure != null && Number.isFinite(p.pressure)
168
+ );
169
+ const input = hasPressure ? pathPointsLocal.map(
170
+ (p) => [p.x, p.y, Math.min(1, Math.max(0, p.pressure ?? 0.5))]
171
+ ) : pathPointsLocal.map((p) => [p.x, p.y]);
172
+ const stroke = getStroke(
173
+ input,
174
+ perfectFreehandOptions(toolKind, style, strokeComplete, hasPressure)
175
+ );
176
+ if (stroke.length < 3) return null;
177
+ const first = stroke[0];
178
+ if (!first) return null;
179
+ let d = `M ${first[0]} ${first[1]} Q`;
180
+ for (let i = 0; i < stroke.length; i++) {
181
+ const a = stroke[i];
182
+ const b = stroke[(i + 1) % stroke.length];
183
+ if (!a || !b) continue;
184
+ d += ` ${a[0]} ${a[1]} ${(a[0] + b[0]) / 2} ${(a[1] + b[1]) / 2}`;
185
+ }
186
+ d += " Z";
187
+ return {
188
+ kind: "fillPath",
189
+ d,
190
+ fill: style.stroke,
191
+ fillOpacity: style.strokeOpacity
192
+ };
193
+ }
194
+
70
195
  // src/react/presence/peer-color.ts
71
196
  function defaultPresenceColorForId(id) {
72
197
  let h = 2166136261;
@@ -91,6 +216,9 @@ function strokePaint(tool, fallback) {
91
216
  return { stroke: fallback, strokeOpacity: 0.95, widthWorld: 3.5 };
92
217
  }
93
218
  }
219
+ function isFreehandTool(tool) {
220
+ return tool === "draw" || tool === "marker" || tool === "pencil" || tool === "brush";
221
+ }
94
222
  function PresenceRemoteLayer({
95
223
  camera,
96
224
  cameraVersion: _cameraVersion,
@@ -126,35 +254,78 @@ function PresenceRemoteLayer({
126
254
  const markup = peer.markupStroke;
127
255
  let strokeNode = null;
128
256
  if (markup && markup.points.length > 0) {
129
- const paint = strokePaint(markup.tool, color);
130
- const d = markup.points.length >= 2 ? smoothFreehandPointsToPathD([...markup.points]) : null;
131
- if (d) {
132
- strokeNode = /* @__PURE__ */ jsx(
133
- "path",
134
- {
135
- d,
136
- fill: "none",
137
- stroke: paint.stroke,
138
- strokeOpacity: paint.strokeOpacity,
139
- strokeWidth: Math.max(paint.widthWorld / z, overlayStrokePx),
140
- strokeLinecap: "round",
141
- strokeLinejoin: "round",
142
- shapeRendering: "geometricPrecision",
143
- vectorEffect: "non-scaling-stroke"
144
- }
257
+ const fallbackPaint = strokePaint(markup.tool, color);
258
+ const paint = {
259
+ stroke: markup.stroke ?? fallbackPaint.stroke,
260
+ strokeOpacity: markup.strokeOpacity ?? fallbackPaint.strokeOpacity,
261
+ widthWorld: markup.strokeWidth ?? fallbackPaint.widthWorld
262
+ };
263
+ if (markup.tool === "laser") {
264
+ const d = markup.points.length >= 2 ? smoothFreehandPointsToPathD([...markup.points]) : null;
265
+ if (d) {
266
+ strokeNode = /* @__PURE__ */ jsx(
267
+ "path",
268
+ {
269
+ d,
270
+ fill: "none",
271
+ stroke: paint.stroke,
272
+ strokeOpacity: paint.strokeOpacity,
273
+ strokeWidth: Math.max(paint.widthWorld / z, overlayStrokePx),
274
+ strokeLinecap: "round",
275
+ strokeLinejoin: "round",
276
+ shapeRendering: "geometricPrecision",
277
+ vectorEffect: "non-scaling-stroke"
278
+ }
279
+ );
280
+ }
281
+ } else if (isFreehandTool(markup.tool)) {
282
+ const style = {
283
+ stroke: paint.stroke,
284
+ strokeWidth: paint.widthWorld,
285
+ ...paint.strokeOpacity != null ? { strokeOpacity: paint.strokeOpacity } : {}
286
+ };
287
+ const payload = computeFreehandSvgPayload(
288
+ markup.points.map((p) => ({ x: p.x, y: p.y })),
289
+ style,
290
+ markup.tool,
291
+ false
145
292
  );
146
- } else {
147
- const p0 = markup.points[0];
148
- if (p0) {
293
+ if (payload?.kind === "circle") {
149
294
  strokeNode = /* @__PURE__ */ jsx(
150
295
  "circle",
151
296
  {
152
- cx: p0.x,
153
- cy: p0.y,
154
- r: Math.max(3 / z, 2),
155
- fill: paint.stroke,
156
- fillOpacity: paint.strokeOpacity,
157
- vectorEffect: "non-scaling-stroke"
297
+ cx: payload.cx,
298
+ cy: payload.cy,
299
+ r: payload.r,
300
+ fill: payload.fill,
301
+ ...payload.fillOpacity != null ? { fillOpacity: payload.fillOpacity } : {},
302
+ shapeRendering: "geometricPrecision"
303
+ }
304
+ );
305
+ } else if (payload?.kind === "fillPath") {
306
+ strokeNode = /* @__PURE__ */ jsx(
307
+ "path",
308
+ {
309
+ d: payload.d,
310
+ fill: payload.fill,
311
+ fillRule: "nonzero",
312
+ stroke: "none",
313
+ ...payload.fillOpacity != null ? { fillOpacity: payload.fillOpacity } : {},
314
+ shapeRendering: "geometricPrecision"
315
+ }
316
+ );
317
+ } else if (payload?.kind === "strokePath") {
318
+ strokeNode = /* @__PURE__ */ jsx(
319
+ "path",
320
+ {
321
+ d: payload.d,
322
+ fill: "none",
323
+ stroke: payload.stroke,
324
+ strokeWidth: payload.strokeWidth,
325
+ ...payload.strokeOpacity != null ? { strokeOpacity: payload.strokeOpacity } : {},
326
+ strokeLinecap: "round",
327
+ strokeLinejoin: "round",
328
+ shapeRendering: "geometricPrecision"
158
329
  }
159
330
  );
160
331
  }
@@ -255,7 +426,16 @@ function parseMarkupStroke(value) {
255
426
  if (x == null || y == null) return null;
256
427
  return { x, y };
257
428
  }).filter((point) => point != null);
258
- return { points, tool };
429
+ const strokeWidth = getNumber(value.strokeWidth);
430
+ const stroke = getString(value.stroke);
431
+ const strokeOpacity = getNumber(value.strokeOpacity);
432
+ return {
433
+ points,
434
+ tool,
435
+ ...strokeWidth != null ? { strokeWidth } : {},
436
+ ...stroke ? { stroke } : {},
437
+ ...strokeOpacity != null ? { strokeOpacity } : {}
438
+ };
259
439
  }
260
440
  function parsePresencePayload(value) {
261
441
  if (!isRecord(value)) return void 0;
@@ -777,38 +957,6 @@ var DEFAULT_VECTOR_TOOLS = [
777
957
  shortcutHint: "I"
778
958
  }
779
959
  ];
780
-
781
- // src/scene/custom-shape.ts
782
- function expandCustomShapeTemplate(template, width, height) {
783
- return template.replace(/\{\{w\}\}/g, String(width)).replace(/\{\{h\}\}/g, String(height)).replace(/\{\{width\}\}/g, String(width)).replace(/\{\{height\}\}/g, String(height));
784
- }
785
- function resolveCustomInner(content, size) {
786
- if ("render" in content) {
787
- return content.render(size);
788
- }
789
- return expandCustomShapeTemplate(content.svg, size.width, size.height);
790
- }
791
- function buildCustomShapeChildrenSvg(inner, intrinsic, bounds) {
792
- const b = normalizeRect(bounds);
793
- const sx = b.width / intrinsic.width;
794
- const sy = b.height / intrinsic.height;
795
- return `<g transform="scale(${sx},${sy})">${inner}</g>`;
796
- }
797
- function createCustomShapeItem(id, bounds, content) {
798
- const r = normalizeRect(bounds);
799
- const intrinsic = { width: r.width, height: r.height };
800
- const inner = resolveCustomInner(content, intrinsic);
801
- return {
802
- id,
803
- x: r.x,
804
- y: r.y,
805
- bounds: { ...r },
806
- toolKind: "custom",
807
- customIntrinsicSize: intrinsic,
808
- customInnerSvg: inner,
809
- childrenSvg: buildCustomShapeChildrenSvg(inner, intrinsic, r)
810
- };
811
- }
812
960
  var iconProps = { size: 20, strokeWidth: 2 };
813
961
  var COMMENT_PLUGIN_DATA_KEY = "realtimeComment";
814
962
  var REALTIME_COMMENT_TOOL = {