canvu-react 0.3.33 → 0.3.34

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/dist/react.cjs CHANGED
@@ -291,153 +291,197 @@ function svgNumber(value) {
291
291
  const rounded = Math.round(value * 100) / 100;
292
292
  return Number.isInteger(rounded) ? String(rounded) : String(rounded);
293
293
  }
294
- function architecturalCloudScallopCount(perimeter, amplitude) {
295
- const targetScallopLength = Math.max(18, amplitude * 2.45);
296
- let count = Math.max(12, Math.round(perimeter / targetScallopLength));
297
- if (count % 2 === 1) count += 1;
298
- return count;
299
- }
300
- function roundedRectMetrics(width, height, inset, radius) {
301
- const left = inset;
302
- const top = inset;
303
- const right = width - inset;
304
- const bottom = height - inset;
305
- const rectWidth = Math.max(0, right - left);
306
- const rectHeight = Math.max(0, bottom - top);
307
- const normalizedRadius = Math.max(
308
- 0,
309
- Math.min(radius, rectWidth / 2, rectHeight / 2)
294
+ function architecturalCloudCenterCount(edgeLength, radius) {
295
+ const targetSpacing = Math.min(
296
+ ARCHITECTURAL_CLOUD_TARGET_SPACING,
297
+ Math.max(1, radius * 1.3)
310
298
  );
311
- const centerX = width / 2;
312
- const topHalfLength = Math.max(0, right - normalizedRadius - centerX);
313
- const horizontalLength = Math.max(0, rectWidth - normalizedRadius * 2);
314
- const verticalLength = Math.max(0, rectHeight - normalizedRadius * 2);
315
- const arcLength = normalizedRadius * (Math.PI / 2);
299
+ return Math.max(2, Math.round(edgeLength / targetSpacing) + 1);
300
+ }
301
+ function distributeRange(start, end, count) {
302
+ if (count <= 1) return [start];
303
+ const step = (end - start) / (count - 1);
304
+ return Array.from({ length: count }, (_, index) => start + step * index);
305
+ }
306
+ function lineCloudPathSegment(start, end) {
307
+ const dx = end[0] - start[0];
308
+ const dy = end[1] - start[1];
309
+ const length = Math.hypot(dx, dy);
316
310
  return {
317
- left,
318
- top,
319
- right,
320
- bottom,
321
- radius: normalizedRadius,
322
- centerX,
323
- topHalfLength,
324
- horizontalLength,
325
- verticalLength,
326
- arcLength,
327
- perimeter: horizontalLength * 2 + verticalLength * 2 + Math.PI * 2 * normalizedRadius
311
+ length,
312
+ pointAt: (t) => [start[0] + dx * t, start[1] + dy * t]
328
313
  };
329
314
  }
330
- function pointOnLine(startX, startY, endX, endY, t) {
331
- return [startX + (endX - startX) * t, startY + (endY - startY) * t];
315
+ function ellipsePoint(centerX, centerY, radiusX, radiusY, angle) {
316
+ return [centerX + Math.cos(angle) * radiusX, centerY + Math.sin(angle) * radiusY];
332
317
  }
333
- function pointOnArc(centerX, centerY, radius, startAngle, endAngle, t) {
334
- const theta = startAngle + (endAngle - startAngle) * t;
335
- return [centerX + Math.cos(theta) * radius, centerY + Math.sin(theta) * radius];
318
+ function approximateEllipseArcLength(radiusX, radiusY, startAngle, endAngle) {
319
+ const steps = Math.max(
320
+ 4,
321
+ Math.ceil(Math.abs(endAngle - startAngle) / (Math.PI / 16))
322
+ );
323
+ let length = 0;
324
+ let previous = ellipsePoint(0, 0, radiusX, radiusY, startAngle);
325
+ for (let index = 1; index <= steps; index += 1) {
326
+ const angle = startAngle + (endAngle - startAngle) * index / steps;
327
+ const next = ellipsePoint(0, 0, radiusX, radiusY, angle);
328
+ length += Math.hypot(next[0] - previous[0], next[1] - previous[1]);
329
+ previous = next;
330
+ }
331
+ return length;
336
332
  }
337
- function pointOnRoundedRectPath(metrics, distance) {
338
- if (metrics.perimeter <= 0) return [metrics.centerX, metrics.top];
339
- let remaining = (distance % metrics.perimeter + metrics.perimeter) % metrics.perimeter;
340
- const consume = (length) => {
341
- if (length <= 1e-9) return null;
342
- if (remaining <= length) return remaining / length;
343
- remaining -= length;
344
- return null;
333
+ function ellipseCloudPathSegment(centerX, centerY, radiusX, radiusY, startAngle, endAngle) {
334
+ return {
335
+ length: approximateEllipseArcLength(radiusX, radiusY, startAngle, endAngle),
336
+ pointAt: (t) => ellipsePoint(
337
+ centerX,
338
+ centerY,
339
+ radiusX,
340
+ radiusY,
341
+ startAngle + (endAngle - startAngle) * t
342
+ )
345
343
  };
346
- let t = consume(metrics.topHalfLength);
347
- if (t != null) {
348
- return pointOnLine(
349
- metrics.centerX,
350
- metrics.top,
351
- metrics.right - metrics.radius,
352
- metrics.top,
353
- t
354
- );
344
+ }
345
+ function cloudPathPerimeter(segments) {
346
+ const usableSegments = segments.filter((segment) => segment.length > 1e-9);
347
+ return usableSegments.reduce((sum, segment) => sum + segment.length, 0);
348
+ }
349
+ function pointOnCloudPath(segments, distance) {
350
+ const perimeter = cloudPathPerimeter(segments);
351
+ if (perimeter <= 0) return [0, 0];
352
+ let remaining = (distance % perimeter + perimeter) % perimeter;
353
+ for (const segment of segments) {
354
+ if (segment.length <= 1e-9) continue;
355
+ if (remaining <= segment.length) {
356
+ const t = remaining / segment.length;
357
+ return segment.pointAt(t);
358
+ }
359
+ remaining -= segment.length;
355
360
  }
356
- t = consume(metrics.arcLength);
357
- if (t != null) {
358
- return pointOnArc(
359
- metrics.right - metrics.radius,
360
- metrics.top + metrics.radius,
361
- metrics.radius,
361
+ const fallback = segments.find((segment) => segment.length > 1e-9);
362
+ return fallback?.pointAt(0) ?? [0, 0];
363
+ }
364
+ function buildRoundedCapsulePathSegments(width, height, inset) {
365
+ const left = inset;
366
+ const top = inset;
367
+ const right = width - inset;
368
+ const bottom = height - inset;
369
+ const capsuleWidth = Math.max(0, right - left);
370
+ const capsuleHeight = Math.max(0, bottom - top);
371
+ const radius = Math.min(capsuleWidth, capsuleHeight) / 2;
372
+ if (radius <= 0) return [];
373
+ const leftCenterX = left + radius;
374
+ const rightCenterX = right - radius;
375
+ const topCenterY = top + radius;
376
+ const bottomCenterY = bottom - radius;
377
+ return [
378
+ lineCloudPathSegment([leftCenterX, top], [rightCenterX, top]),
379
+ ellipseCloudPathSegment(
380
+ rightCenterX,
381
+ topCenterY,
382
+ radius,
383
+ radius,
362
384
  -Math.PI / 2,
385
+ 0
386
+ ),
387
+ lineCloudPathSegment([right, topCenterY], [right, bottomCenterY]),
388
+ ellipseCloudPathSegment(
389
+ rightCenterX,
390
+ bottomCenterY,
391
+ radius,
392
+ radius,
363
393
  0,
364
- t
365
- );
366
- }
367
- t = consume(metrics.verticalLength);
368
- if (t != null) {
369
- return pointOnLine(
370
- metrics.right,
371
- metrics.top + metrics.radius,
372
- metrics.right,
373
- metrics.bottom - metrics.radius,
374
- t
375
- );
376
- }
377
- t = consume(metrics.arcLength);
378
- if (t != null) {
379
- return pointOnArc(
380
- metrics.right - metrics.radius,
381
- metrics.bottom - metrics.radius,
382
- metrics.radius,
383
- 0,
384
- Math.PI / 2,
385
- t
386
- );
387
- }
388
- t = consume(metrics.horizontalLength);
389
- if (t != null) {
390
- return pointOnLine(
391
- metrics.right - metrics.radius,
392
- metrics.bottom,
393
- metrics.left + metrics.radius,
394
- metrics.bottom,
395
- t
396
- );
397
- }
398
- t = consume(metrics.arcLength);
399
- if (t != null) {
400
- return pointOnArc(
401
- metrics.left + metrics.radius,
402
- metrics.bottom - metrics.radius,
403
- metrics.radius,
394
+ Math.PI / 2
395
+ ),
396
+ lineCloudPathSegment([rightCenterX, bottom], [leftCenterX, bottom]),
397
+ ellipseCloudPathSegment(
398
+ leftCenterX,
399
+ bottomCenterY,
400
+ radius,
401
+ radius,
404
402
  Math.PI / 2,
403
+ Math.PI
404
+ ),
405
+ lineCloudPathSegment([left, bottomCenterY], [left, topCenterY]),
406
+ ellipseCloudPathSegment(
407
+ leftCenterX,
408
+ topCenterY,
409
+ radius,
410
+ radius,
405
411
  Math.PI,
406
- t
407
- );
408
- }
409
- t = consume(metrics.verticalLength);
410
- if (t != null) {
411
- return pointOnLine(
412
- metrics.left,
413
- metrics.bottom - metrics.radius,
414
- metrics.left,
415
- metrics.top + metrics.radius,
416
- t
417
- );
418
- }
419
- t = consume(metrics.arcLength);
420
- if (t != null) {
421
- return pointOnArc(
422
- metrics.left + metrics.radius,
423
- metrics.top + metrics.radius,
424
- metrics.radius,
425
- Math.PI,
426
- Math.PI * 1.5,
427
- t
428
- );
429
- }
430
- t = consume(metrics.topHalfLength);
431
- if (t != null) {
432
- return pointOnLine(
433
- metrics.left + metrics.radius,
434
- metrics.top,
435
- metrics.centerX,
436
- metrics.top,
437
- t
412
+ Math.PI * 1.5
413
+ )
414
+ ];
415
+ }
416
+ function buildRoundedArcCloudPathD(cloudWidth, cloudHeight, center) {
417
+ const minDimension = Math.min(cloudWidth, cloudHeight);
418
+ const radius = Math.min(
419
+ ARCHITECTURAL_CLOUD_ROUNDED_RADIUS,
420
+ Math.max(ARCHITECTURAL_CLOUD_ROUNDED_MIN_RADIUS, minDimension * 0.16)
421
+ );
422
+ const centerPath = buildRoundedCapsulePathSegments(
423
+ cloudWidth,
424
+ cloudHeight,
425
+ radius
426
+ );
427
+ const centerPerimeter = cloudPathPerimeter(centerPath);
428
+ if (centerPerimeter <= 0) return "";
429
+ const lobeCount = Math.max(
430
+ 8,
431
+ Math.round(centerPerimeter / ARCHITECTURAL_CLOUD_ROUNDED_TARGET_SPACING)
432
+ );
433
+ const centers = Array.from(
434
+ { length: lobeCount },
435
+ (_, index) => pointOnCloudPath(centerPath, centerPerimeter * index / lobeCount)
436
+ );
437
+ const points = centers.map((point, index) => {
438
+ const previous = centers[(index - 1 + centers.length) % centers.length] ?? point;
439
+ return cloudCircleIntersection(previous, point, radius, center);
440
+ });
441
+ const [startX, startY] = points[0] ?? [0, 0];
442
+ const segments = [`M${svgNumber(startX)} ${svgNumber(startY)}`];
443
+ for (const [endX, endY] of points.slice(1)) {
444
+ segments.push(
445
+ `A ${svgNumber(radius)} ${svgNumber(radius)} 0 0 1 ${svgNumber(endX)} ${svgNumber(endY)}`
438
446
  );
439
447
  }
440
- return [metrics.centerX, metrics.top];
448
+ segments.push(
449
+ `A ${svgNumber(radius)} ${svgNumber(radius)} 0 0 1 ${svgNumber(startX)} ${svgNumber(startY)}`
450
+ );
451
+ segments.push("Z");
452
+ return segments.join(" ");
453
+ }
454
+ function cloudCircleIntersection(a, b, radius, center) {
455
+ const midX = (a[0] + b[0]) / 2;
456
+ const midY = (a[1] + b[1]) / 2;
457
+ const dx = b[0] - a[0];
458
+ const dy = b[1] - a[1];
459
+ const distance = Math.hypot(dx, dy);
460
+ if (distance <= 1e-9) return [midX, midY];
461
+ const halfDistance = distance / 2;
462
+ const offset = Math.sqrt(
463
+ Math.max(0, radius * radius - halfDistance * halfDistance)
464
+ );
465
+ const normalX = -dy / distance;
466
+ const normalY = dx / distance;
467
+ const first = [midX + normalX * offset, midY + normalY * offset];
468
+ const second = [
469
+ midX - normalX * offset,
470
+ midY - normalY * offset
471
+ ];
472
+ const firstDistance = (first[0] - center[0]) * (first[0] - center[0]) + (first[1] - center[1]) * (first[1] - center[1]);
473
+ const secondDistance = (second[0] - center[0]) * (second[0] - center[0]) + (second[1] - center[1]) * (second[1] - center[1]);
474
+ return firstDistance >= secondDistance ? first : second;
475
+ }
476
+ function cloudEllipseIntersection(a, b, radiusX, radiusY, center) {
477
+ const scaleY = radiusX / radiusY;
478
+ const [x, y] = cloudCircleIntersection(
479
+ [a[0], a[1] * scaleY],
480
+ [b[0], b[1] * scaleY],
481
+ radiusX,
482
+ [center[0], center[1] * scaleY]
483
+ );
484
+ return [x, y / scaleY];
441
485
  }
442
486
  function buildRectSvg(width, height, style = DEFAULT_STROKE_STYLE) {
443
487
  return `<rect width="${width}" height="${height}" fill="none" stroke="${style.stroke}" stroke-width="${style.strokeWidth}" rx="4"${strokeOpacityAttr(style)} />`;
@@ -451,39 +495,63 @@ function buildArchitecturalCloudPathD(width, height, strokeWidth = DEFAULT_STROK
451
495
  const w = Math.max(0, width);
452
496
  const h = Math.max(0, height);
453
497
  if (w <= 0 || h <= 0) return "";
454
- const inset = Math.max(0.5, strokeWidth / 2);
455
- const outerWidth = Math.max(0, w - inset * 2);
456
- const outerHeight = Math.max(0, h - inset * 2);
457
- if (outerWidth <= 0 || outerHeight <= 0) return "";
458
- const amplitude = Math.min(
459
- outerWidth * 0.12,
460
- outerHeight * 0.12,
461
- Math.max(5, Math.min(14, Math.min(w, h) * 0.07))
498
+ const padding = Math.max(0, strokeWidth * 2);
499
+ const cloudWidth = Math.max(0, w - padding);
500
+ const cloudHeight = Math.max(0, h - padding);
501
+ if (cloudWidth <= 0 || cloudHeight <= 0) return "";
502
+ const radiusX = Math.min(
503
+ ARCHITECTURAL_CLOUD_RADIUS,
504
+ cloudWidth * ARCHITECTURAL_CLOUD_FLAT_RADIUS_RATIO
462
505
  );
463
- const radius = Math.min(
464
- outerWidth / 2,
465
- outerHeight / 2,
466
- Math.max(amplitude * 2.5, outerHeight * 0.43)
506
+ const radiusY = Math.min(
507
+ ARCHITECTURAL_CLOUD_RADIUS,
508
+ cloudHeight * ARCHITECTURAL_CLOUD_FLAT_RADIUS_RATIO
467
509
  );
468
- const outer = roundedRectMetrics(w, h, inset, radius);
469
- const inner = roundedRectMetrics(
470
- w,
471
- h,
472
- inset + amplitude,
473
- Math.max(0, radius - amplitude)
510
+ if (radiusX <= 0 || radiusY <= 0) return "";
511
+ const center = [cloudWidth / 2, cloudHeight / 2];
512
+ const leftCenterX = radiusX;
513
+ const rightCenterX = cloudWidth - radiusX;
514
+ const topCenterY = radiusY;
515
+ const bottomCenterY = cloudHeight - radiusY;
516
+ const horizontalCenters = distributeRange(
517
+ leftCenterX,
518
+ rightCenterX,
519
+ architecturalCloudCenterCount(Math.max(0, rightCenterX - leftCenterX), radiusX)
474
520
  );
475
- const scallopCount = architecturalCloudScallopCount(outer.perimeter, amplitude);
476
- const [startX, startY] = pointOnRoundedRectPath(inner, 0);
521
+ const verticalCenters = distributeRange(
522
+ topCenterY,
523
+ bottomCenterY,
524
+ architecturalCloudCenterCount(Math.max(0, bottomCenterY - topCenterY), radiusY)
525
+ );
526
+ if (horizontalCenters.length > 3 && verticalCenters.length > 3) {
527
+ const roundedArcCloudPath = buildRoundedArcCloudPathD(
528
+ cloudWidth,
529
+ cloudHeight,
530
+ center
531
+ );
532
+ if (roundedArcCloudPath !== "") return roundedArcCloudPath;
533
+ }
534
+ const rectangularCenters = [
535
+ ...horizontalCenters.map((x) => [x, topCenterY]),
536
+ ...verticalCenters.slice(1).map((y) => [rightCenterX, y]),
537
+ ...horizontalCenters.slice(0, -1).reverse().map((x) => [x, bottomCenterY]),
538
+ ...verticalCenters.slice(1, -1).reverse().map((y) => [leftCenterX, y])
539
+ ];
540
+ const centers = rectangularCenters;
541
+ const points = centers.map((point, index) => {
542
+ const previous = centers[(index - 1 + centers.length) % centers.length] ?? point;
543
+ return cloudEllipseIntersection(previous, point, radiusX, radiusY, center);
544
+ });
545
+ const [startX, startY] = points[0] ?? [0, 0];
477
546
  const segments = [`M${svgNumber(startX)} ${svgNumber(startY)}`];
478
- for (let index = 0; index < scallopCount; index += 1) {
479
- const controlDistance = (index + 0.5) / scallopCount * outer.perimeter;
480
- const endDistance = (index + 1) / scallopCount * inner.perimeter;
481
- const [controlX, controlY] = pointOnRoundedRectPath(outer, controlDistance);
482
- const [endX, endY] = pointOnRoundedRectPath(inner, endDistance);
547
+ for (const [endX, endY] of points.slice(1)) {
483
548
  segments.push(
484
- `Q${svgNumber(controlX)} ${svgNumber(controlY)} ${svgNumber(endX)} ${svgNumber(endY)}`
549
+ `A ${svgNumber(radiusX)} ${svgNumber(radiusY)} 0 0 1 ${svgNumber(endX)} ${svgNumber(endY)}`
485
550
  );
486
551
  }
552
+ segments.push(
553
+ `A ${svgNumber(radiusX)} ${svgNumber(radiusY)} 0 0 1 ${svgNumber(startX)} ${svgNumber(startY)}`
554
+ );
487
555
  segments.push("Z");
488
556
  return segments.join(" ");
489
557
  }
@@ -921,7 +989,7 @@ function createImageFromVectorTrace(id, bounds, imageVectorInnerSvg, imageVector
921
989
  childrenSvg
922
990
  };
923
991
  }
924
- var DEFAULT_STROKE_STYLE, TOOL_FREEHAND_DEFAULTS;
992
+ var DEFAULT_STROKE_STYLE, TOOL_FREEHAND_DEFAULTS, ARCHITECTURAL_CLOUD_RADIUS, ARCHITECTURAL_CLOUD_TARGET_SPACING, ARCHITECTURAL_CLOUD_FLAT_RADIUS_RATIO, ARCHITECTURAL_CLOUD_ROUNDED_RADIUS, ARCHITECTURAL_CLOUD_ROUNDED_MIN_RADIUS, ARCHITECTURAL_CLOUD_ROUNDED_TARGET_SPACING;
925
993
  var init_shape_builders = __esm({
926
994
  "src/scene/shape-builders.ts"() {
927
995
  init_rect();
@@ -937,6 +1005,12 @@ var init_shape_builders = __esm({
937
1005
  brush: { strokeWidth: 10 },
938
1006
  marker: { stroke: "#fde047", strokeWidth: 16, strokeOpacity: 0.5 }
939
1007
  };
1008
+ ARCHITECTURAL_CLOUD_RADIUS = 38;
1009
+ ARCHITECTURAL_CLOUD_TARGET_SPACING = 50;
1010
+ ARCHITECTURAL_CLOUD_FLAT_RADIUS_RATIO = 0.38;
1011
+ ARCHITECTURAL_CLOUD_ROUNDED_RADIUS = 72;
1012
+ ARCHITECTURAL_CLOUD_ROUNDED_MIN_RADIUS = 44;
1013
+ ARCHITECTURAL_CLOUD_ROUNDED_TARGET_SPACING = 98;
940
1014
  }
941
1015
  });
942
1016
 
@@ -3241,7 +3315,7 @@ function ShapeContextMenu({
3241
3315
  }
3242
3316
  return reactDom.createPortal(menu, document.body);
3243
3317
  }
3244
- var architecturalCloudIconPath = "M11 2.44 Q13.3 1 14.84 2.44 Q17.84 1.4 18.41 3.47 Q20.8 4.72 19.56 7 Q20.8 9.28 18.41 10.53 Q17.84 12.6 14.84 11.56 Q13.3 13 11 11.56 Q8.7 13 7.16 11.56 Q4.16 12.6 3.59 10.53 Q1.2 9.28 2.44 7 Q1.2 4.72 3.59 3.47 Q4.16 1.4 7.16 2.44 Q8.7 1 11 2.44 Z";
3318
+ var architecturalCloudIconPath = "M1.5 12 A 4.94 5.23 0 0 1 9.07 6 A 4.94 5.23 0 0 1 14.93 6 A 4.94 5.23 0 0 1 22.5 12 A 4.94 5.23 0 0 1 14.93 18 A 4.94 5.23 0 0 1 9.07 18 A 4.94 5.23 0 0 1 1.5 12 Z";
3245
3319
  var base = {
3246
3320
  width: 20,
3247
3321
  height: 20,
@@ -3273,7 +3347,7 @@ function IconEllipse(props) {
3273
3347
  return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "12", rx: "9", ry: "6" }) });
3274
3348
  }
3275
3349
  function IconArchitecturalCloud(props) {
3276
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: architecturalCloudIconPath, transform: "translate(1 5)" }) });
3350
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: architecturalCloudIconPath }) });
3277
3351
  }
3278
3352
  function IconLine(props) {
3279
3353
  return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }) });