canvu-react 0.3.33 → 0.3.35

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
509
+ );
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)
467
520
  );
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)
521
+ const verticalCenters = distributeRange(
522
+ topCenterY,
523
+ bottomCenterY,
524
+ architecturalCloudCenterCount(Math.max(0, bottomCenterY - topCenterY), radiusY)
474
525
  );
475
- const scallopCount = architecturalCloudScallopCount(outer.perimeter, amplitude);
476
- const [startX, startY] = pointOnRoundedRectPath(inner, 0);
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
 
@@ -1751,6 +1825,67 @@ function useCanvuChromeContext() {
1751
1825
  return react.useContext(CanvuChromeContext);
1752
1826
  }
1753
1827
 
1828
+ // src/math/item-transform.ts
1829
+ init_rect();
1830
+ function getItemRotationRad(item) {
1831
+ return item.rotation ?? 0;
1832
+ }
1833
+ function itemLocalToWorld(lx, ly, itemX, itemY, w, h, rotationRad) {
1834
+ const c = { x: w / 2, y: h / 2 };
1835
+ const dlx = lx - c.x;
1836
+ const dly = ly - c.y;
1837
+ const cos = Math.cos(rotationRad);
1838
+ const sin = Math.sin(rotationRad);
1839
+ return {
1840
+ x: itemX + c.x + cos * dlx - sin * dly,
1841
+ y: itemY + c.y + sin * dlx + cos * dly
1842
+ };
1843
+ }
1844
+ function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
1845
+ const c = { x: w / 2, y: h / 2 };
1846
+ const vx = wx - itemX;
1847
+ const vy = wy - itemY;
1848
+ const dx = vx - c.x;
1849
+ const dy = vy - c.y;
1850
+ const cos = Math.cos(-rotationRad);
1851
+ const sin = Math.sin(-rotationRad);
1852
+ const lx = cos * dx - sin * dy;
1853
+ const ly = sin * dx + cos * dy;
1854
+ return { x: c.x + lx, y: c.y + ly };
1855
+ }
1856
+ function itemPivotWorld(item) {
1857
+ const r = normalizeRect(item.bounds);
1858
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
1859
+ }
1860
+ function boundsAabbForRotatedItem(item) {
1861
+ const rot = getItemRotationRad(item);
1862
+ if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
1863
+ return item.bounds;
1864
+ }
1865
+ const r = normalizeRect(item.bounds);
1866
+ if (Math.abs(rot) < 1e-12) {
1867
+ return r;
1868
+ }
1869
+ const corners = [
1870
+ [0, 0],
1871
+ [r.width, 0],
1872
+ [r.width, r.height],
1873
+ [0, r.height]
1874
+ ];
1875
+ let minX = Infinity;
1876
+ let minY = Infinity;
1877
+ let maxX = -Infinity;
1878
+ let maxY = -Infinity;
1879
+ for (const [lx, ly] of corners) {
1880
+ const p = itemLocalToWorld(lx, ly, item.x, item.y, r.width, r.height, rot);
1881
+ minX = Math.min(minX, p.x);
1882
+ minY = Math.min(minY, p.y);
1883
+ maxX = Math.max(maxX, p.x);
1884
+ maxY = Math.max(maxY, p.y);
1885
+ }
1886
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1887
+ }
1888
+
1754
1889
  // src/scene/clone-item.ts
1755
1890
  init_shape_builders();
1756
1891
  function cloneVectorSceneItemWithNewId(item) {
@@ -1790,20 +1925,25 @@ function markImageAsManaged(item) {
1790
1925
  };
1791
1926
  }
1792
1927
  function restackManagedImages(items) {
1793
- let anchor;
1928
+ let anchorAabbY = Infinity;
1929
+ let anchorCenterX = 0;
1794
1930
  for (const item of items) {
1795
1931
  if (!isManagedImage(item)) continue;
1796
- if (!anchor || item.bounds.y < anchor.bounds.y) anchor = item;
1932
+ const aabb = boundsAabbForRotatedItem(item);
1933
+ if (aabb.y < anchorAabbY) {
1934
+ anchorAabbY = aabb.y;
1935
+ anchorCenterX = aabb.x + aabb.width / 2;
1936
+ }
1797
1937
  }
1798
- if (!anchor) return [...items];
1799
- const anchorCenterX = anchor.bounds.x + anchor.bounds.width / 2;
1800
- const anchorTopY = anchor.bounds.y;
1801
- let cursorY = anchorTopY;
1938
+ if (!Number.isFinite(anchorAabbY)) return [...items];
1939
+ let cursorY = anchorAabbY;
1802
1940
  return items.map((item) => {
1803
1941
  if (!isManagedImage(item)) return item;
1942
+ const aabb = boundsAabbForRotatedItem(item);
1943
+ const centerY = cursorY + aabb.height / 2;
1804
1944
  const newX = anchorCenterX - item.bounds.width / 2;
1805
- const newY = cursorY;
1806
- cursorY = newY + item.bounds.height + STACK_GAP_WORLD;
1945
+ const newY = centerY - item.bounds.height / 2;
1946
+ cursorY += aabb.height + STACK_GAP_WORLD;
1807
1947
  if (item.bounds.x === newX && item.bounds.y === newY) return item;
1808
1948
  return {
1809
1949
  ...item,
@@ -1823,8 +1963,10 @@ function copyManagedImage(items, id) {
1823
1963
  return restackManagedImages(inserted);
1824
1964
  }
1825
1965
  function rotateManagedImage(items, id) {
1826
- return items.map(
1827
- (i) => i.id === id ? { ...i, rotation: ((i.rotation ?? 0) + Math.PI / 2) % (Math.PI * 2) } : i
1966
+ return restackManagedImages(
1967
+ items.map(
1968
+ (i) => i.id === id ? { ...i, rotation: ((i.rotation ?? 0) + Math.PI / 2) % (Math.PI * 2) } : i
1969
+ )
1828
1970
  );
1829
1971
  }
1830
1972
  function deleteManagedImage(items, id) {
@@ -2271,69 +2413,6 @@ function getBoardPositionStyle(position, inset = 12, zIndex = 40) {
2271
2413
  return base2;
2272
2414
  }
2273
2415
  }
2274
-
2275
- // src/math/item-transform.ts
2276
- init_rect();
2277
- function getItemRotationRad(item) {
2278
- return item.rotation ?? 0;
2279
- }
2280
- function itemLocalToWorld(lx, ly, itemX, itemY, w, h, rotationRad) {
2281
- const c = { x: w / 2, y: h / 2 };
2282
- const dlx = lx - c.x;
2283
- const dly = ly - c.y;
2284
- const cos = Math.cos(rotationRad);
2285
- const sin = Math.sin(rotationRad);
2286
- return {
2287
- x: itemX + c.x + cos * dlx - sin * dly,
2288
- y: itemY + c.y + sin * dlx + cos * dly
2289
- };
2290
- }
2291
- function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
2292
- const c = { x: w / 2, y: h / 2 };
2293
- const vx = wx - itemX;
2294
- const vy = wy - itemY;
2295
- const dx = vx - c.x;
2296
- const dy = vy - c.y;
2297
- const cos = Math.cos(-rotationRad);
2298
- const sin = Math.sin(-rotationRad);
2299
- const lx = cos * dx - sin * dy;
2300
- const ly = sin * dx + cos * dy;
2301
- return { x: c.x + lx, y: c.y + ly };
2302
- }
2303
- function itemPivotWorld(item) {
2304
- const r = normalizeRect(item.bounds);
2305
- return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
2306
- }
2307
- function boundsAabbForRotatedItem(item) {
2308
- const rot = getItemRotationRad(item);
2309
- if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
2310
- return item.bounds;
2311
- }
2312
- const r = normalizeRect(item.bounds);
2313
- if (Math.abs(rot) < 1e-12) {
2314
- return r;
2315
- }
2316
- const corners = [
2317
- [0, 0],
2318
- [r.width, 0],
2319
- [r.width, r.height],
2320
- [0, r.height]
2321
- ];
2322
- let minX = Infinity;
2323
- let minY = Infinity;
2324
- let maxX = -Infinity;
2325
- let maxY = -Infinity;
2326
- for (const [lx, ly] of corners) {
2327
- const p = itemLocalToWorld(lx, ly, item.x, item.y, r.width, r.height, rot);
2328
- minX = Math.min(minX, p.x);
2329
- minY = Math.min(minY, p.y);
2330
- maxX = Math.max(maxX, p.x);
2331
- maxY = Math.max(maxY, p.y);
2332
- }
2333
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
2334
- }
2335
-
2336
- // src/react/navmenu/minimap.tsx
2337
2416
  init_rect();
2338
2417
  var NavMenuMinimapSlotContext = react.createContext(null);
2339
2418
  function noop() {
@@ -3241,7 +3320,7 @@ function ShapeContextMenu({
3241
3320
  }
3242
3321
  return reactDom.createPortal(menu, document.body);
3243
3322
  }
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";
3323
+ 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
3324
  var base = {
3246
3325
  width: 20,
3247
3326
  height: 20,
@@ -3273,7 +3352,7 @@ function IconEllipse(props) {
3273
3352
  return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "12", rx: "9", ry: "6" }) });
3274
3353
  }
3275
3354
  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)" }) });
3355
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: architecturalCloudIconPath }) });
3277
3356
  }
3278
3357
  function IconLine(props) {
3279
3358
  return /* @__PURE__ */ jsxRuntime.jsx("svg", { ...base, ...props, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "19", x2: "19", y2: "5" }) });