@woven-canvas/core 1.0.6 → 1.0.10

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/build/index.js CHANGED
@@ -8,21 +8,21 @@ var __export = (target, all) => {
8
8
  import {
9
9
  CanvasComponentDef as CanvasComponentDef7,
10
10
  CanvasSingletonDef as CanvasSingletonDef12,
11
- defineCanvasComponent as defineCanvasComponent22,
11
+ defineCanvasComponent as defineCanvasComponent23,
12
12
  defineCanvasSingleton as defineCanvasSingleton3,
13
- Synced as Synced9
13
+ Synced as Synced10
14
14
  } from "@woven-ecs/canvas-store";
15
15
  import {
16
- addComponent as addComponent12,
16
+ addComponent as addComponent13,
17
17
  ComponentDef,
18
- createEntity as createEntity9,
18
+ createEntity as createEntity10,
19
19
  defineQuery as defineQuery22,
20
20
  defineSystem,
21
21
  EventType,
22
- field as field36,
22
+ field as field37,
23
23
  getBackrefs as getBackrefs5,
24
24
  getResources as getResources15,
25
- hasComponent as hasComponent20,
25
+ hasComponent as hasComponent21,
26
26
  isAlive as isAlive3,
27
27
  MainThreadSystem as MainThreadSystem2,
28
28
  removeComponent as removeComponent7,
@@ -122,6 +122,7 @@ var SetCursor = defineCommand("set-cursor");
122
122
  var AddFrameHighlight = defineCommand("frame-add-highlight");
123
123
  var RemoveFrameHighlight = defineCommand("frame-remove-highlight");
124
124
  var AssignFrameChildren = defineCommand("frame-assign-children");
125
+ var PlaceBlockEvent = defineCommand("place-block");
125
126
 
126
127
  // src/commands/selection.ts
127
128
  var SelectBlock = defineCommand("select-block");
@@ -163,8 +164,10 @@ __export(components_exports, {
163
164
  HitGeometry: () => HitGeometry,
164
165
  Hovered: () => Hovered,
165
166
  Image: () => Image,
167
+ Layer: () => Layer,
166
168
  MAX_HIT_ARCS: () => MAX_HIT_ARCS,
167
169
  MAX_HIT_CAPSULES: () => MAX_HIT_CAPSULES,
170
+ MAX_HIT_POLYGON_POINTS: () => MAX_HIT_POLYGON_POINTS,
168
171
  Opacity: () => Opacity,
169
172
  Pointer: () => Pointer,
170
173
  PointerButton: () => PointerButton,
@@ -472,6 +475,7 @@ var BlockDef = z.object({
472
475
  resizeMode: z.enum([ResizeMode.Scale, ResizeMode.Text, ResizeMode.Free, ResizeMode.GroupOnly]).default(ResizeMode.Scale),
473
476
  canRotate: z.boolean().default(true),
474
477
  canScale: z.boolean().default(true),
478
+ snappable: z.boolean().default(true),
475
479
  interactable: z.boolean().default(true),
476
480
  excludeFromRankBounds: z.boolean().default(false),
477
481
  connectors: BlockDefConnectors.default(BlockDefConnectors.parse({}))
@@ -539,7 +543,10 @@ var BlockSchema = {
539
543
  /** How the block can be resized */
540
544
  resizeMode: field2.enum(ResizeMode).default(ResizeMode.Default),
541
545
  /** Parent block ID — position is relative to parent if set */
542
- parentId: field2.ref()
546
+ parentId: field2.ref(),
547
+ /** Layer membership — carried by every block directly (a frame and its
548
+ * children each get their own `layerId`; no inheritance). Null = unlayered. */
549
+ layerId: field2.ref()
543
550
  };
544
551
  var _worldPos = [0, 0];
545
552
  var BlockDef2 = class extends CanvasComponentDef {
@@ -1028,19 +1035,25 @@ var Held = defineCanvasComponent9(
1028
1035
  );
1029
1036
 
1030
1037
  // src/components/HitGeometry.ts
1031
- import { Arc, Capsule, Mat2 } from "@woven-canvas/math";
1038
+ import { Arc, Capsule, Mat2, Polygon } from "@woven-canvas/math";
1032
1039
  import { CanvasComponentDef as CanvasComponentDef4 } from "@woven-ecs/canvas-store";
1033
1040
  import { field as field11 } from "@woven-ecs/core";
1034
1041
  var _uvToWorldMatrix = [1, 0, 0, 1, 0, 0];
1042
+ var _polyPt = [0, 0];
1035
1043
  var MAX_HIT_CAPSULES = 64;
1036
1044
  var FLOATS_PER_CAPSULE = 5;
1037
1045
  var MAX_HIT_ARCS = 2;
1038
1046
  var FLOATS_PER_ARC = 7;
1047
+ var MAX_HIT_POLYGON_POINTS = 128;
1048
+ var FLOATS_PER_POLYGON_POINT = 2;
1049
+ var _worldPolygon = new Float32Array(MAX_HIT_POLYGON_POINTS * FLOATS_PER_POLYGON_POINT);
1039
1050
  var HitGeometrySchema = {
1040
1051
  hitCapsules: field11.buffer(field11.float32()).size(MAX_HIT_CAPSULES * FLOATS_PER_CAPSULE),
1041
1052
  capsuleCount: field11.uint16().default(0),
1042
1053
  hitArcs: field11.buffer(field11.float32()).size(MAX_HIT_ARCS * FLOATS_PER_ARC),
1043
- arcCount: field11.uint16().default(0)
1054
+ arcCount: field11.uint16().default(0),
1055
+ hitPolygon: field11.buffer(field11.float32()).size(MAX_HIT_POLYGON_POINTS * FLOATS_PER_POLYGON_POINT),
1056
+ polygonPointCount: field11.uint16().default(0)
1044
1057
  };
1045
1058
  var HitGeometryDef = class extends CanvasComponentDef4 {
1046
1059
  constructor() {
@@ -1151,8 +1164,27 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1151
1164
  return true;
1152
1165
  }
1153
1166
  }
1167
+ const polygonPointCount = this.fillWorldPolygon(hitGeometry.hitPolygon, hitGeometry.polygonPointCount);
1168
+ if (polygonPointCount > 0 && Polygon.containsPointEvenOdd(_worldPolygon, polygonPointCount, point)) {
1169
+ return true;
1170
+ }
1154
1171
  return false;
1155
1172
  }
1173
+ /**
1174
+ * Transform the UV polygon into the shared `_worldPolygon` scratch buffer
1175
+ * using the current `_uvToWorldMatrix` (caller must have built it first).
1176
+ * Returns the vertex count. Allocation-free.
1177
+ */
1178
+ fillWorldPolygon(hitPolygon, count) {
1179
+ for (let i = 0; i < count; i++) {
1180
+ _polyPt[0] = hitPolygon[i * 2];
1181
+ _polyPt[1] = hitPolygon[i * 2 + 1];
1182
+ Mat2.transformPoint(_uvToWorldMatrix, _polyPt);
1183
+ _worldPolygon[i * 2] = _polyPt[0];
1184
+ _worldPolygon[i * 2 + 1] = _polyPt[1];
1185
+ }
1186
+ return count;
1187
+ }
1156
1188
  /**
1157
1189
  * Check if an AABB intersects with the entity's hit geometry.
1158
1190
  * Hit geometry is stored in UV coordinates (0-1) relative to the block.
@@ -1192,6 +1224,10 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1192
1224
  return true;
1193
1225
  }
1194
1226
  }
1227
+ const polygonPointCount = this.fillWorldPolygon(hitGeometry.hitPolygon, hitGeometry.polygonPointCount);
1228
+ if (polygonPointCount > 0 && Polygon.intersectsAabb(_worldPolygon, polygonPointCount, aabb)) {
1229
+ return true;
1230
+ }
1195
1231
  return false;
1196
1232
  }
1197
1233
  /**
@@ -1229,6 +1265,11 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1229
1265
  const worldArc = Arc.create(worldA[0], worldA[1], worldB[0], worldB[1], worldC[0], worldC[1], thickness);
1230
1266
  pts.push(...Arc.getExtrema(worldArc));
1231
1267
  }
1268
+ for (let i = 0; i < hitGeometry.polygonPointCount; i++) {
1269
+ const world = [hitGeometry.hitPolygon[i * 2], hitGeometry.hitPolygon[i * 2 + 1]];
1270
+ Mat2.transformPoint(_uvToWorldMatrix, world);
1271
+ pts.push(world);
1272
+ }
1232
1273
  return pts;
1233
1274
  }
1234
1275
  // ============================================
@@ -1331,6 +1372,27 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1331
1372
  const uvC = Block.worldToUv(ctx, entityId, worldC);
1332
1373
  this.addArcUv(ctx, entityId, uvA, uvB, uvC, worldThickness);
1333
1374
  }
1375
+ /**
1376
+ * Set the filled hit polygon from UV vertices, replacing any existing one.
1377
+ * Positions are in UV coordinates (0-1) relative to the block. The polygon is
1378
+ * implicitly closed and tested with the even-odd fill rule.
1379
+ *
1380
+ * @param ctx - ECS context
1381
+ * @param entityId - Entity ID
1382
+ * @param uvPoints - Polygon vertices in UV coordinates [[x, y], ...]
1383
+ */
1384
+ setPolygonUv(ctx, entityId, uvPoints) {
1385
+ const hitGeometry = this.write(ctx, entityId);
1386
+ const count = Math.min(uvPoints.length, MAX_HIT_POLYGON_POINTS);
1387
+ for (let i = 0; i < count; i++) {
1388
+ hitGeometry.hitPolygon[i * 2] = uvPoints[i][0];
1389
+ hitGeometry.hitPolygon[i * 2 + 1] = uvPoints[i][1];
1390
+ }
1391
+ hitGeometry.polygonPointCount = count;
1392
+ if (uvPoints.length > MAX_HIT_POLYGON_POINTS) {
1393
+ console.warn(`HitGeometry: polygon truncated to ${MAX_HIT_POLYGON_POINTS} points`);
1394
+ }
1395
+ }
1334
1396
  /**
1335
1397
  * Clear all hit geometry from an entity.
1336
1398
  *
@@ -1341,6 +1403,7 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1341
1403
  const hitGeometry = this.write(ctx, entityId);
1342
1404
  hitGeometry.capsuleCount = 0;
1343
1405
  hitGeometry.arcCount = 0;
1406
+ hitGeometry.polygonPointCount = 0;
1344
1407
  }
1345
1408
  };
1346
1409
  var HitGeometry = new HitGeometryDef();
@@ -1364,19 +1427,39 @@ var Image = defineCanvasComponent11(
1364
1427
  }
1365
1428
  );
1366
1429
 
1367
- // src/components/Opacity.ts
1430
+ // src/components/Layer.ts
1368
1431
  import { defineCanvasComponent as defineCanvasComponent12 } from "@woven-ecs/canvas-store";
1369
1432
  import { field as field13 } from "@woven-ecs/core";
1370
- var Opacity = defineCanvasComponent12(
1433
+ var Layer = defineCanvasComponent12(
1434
+ { name: "layer", sync: "document" },
1435
+ {
1436
+ /** Parent frame/page entity this layer belongs to — back-reference. Layers
1437
+ * exist only for sorting; they have no geometry of their own. */
1438
+ parentId: field13.ref(),
1439
+ /** Z-order rank among the page's layers (LexoRank string). Higher sorts in front. */
1440
+ rank: field13.string().max(36).default(""),
1441
+ /** Display name shown in the layers panel. */
1442
+ label: field13.string().max(128).default("Layer"),
1443
+ /** When true, blocks in this layer are not selectable (skipped by hit-testing). */
1444
+ locked: field13.boolean().default(false),
1445
+ /** When true, the layer is not rendered and is skipped by hit-testing. */
1446
+ hidden: field13.boolean().default(false)
1447
+ }
1448
+ );
1449
+
1450
+ // src/components/Opacity.ts
1451
+ import { defineCanvasComponent as defineCanvasComponent13 } from "@woven-ecs/canvas-store";
1452
+ import { field as field14 } from "@woven-ecs/core";
1453
+ var Opacity = defineCanvasComponent13(
1371
1454
  { name: "opacity" },
1372
1455
  {
1373
- value: field13.uint8().default(255)
1456
+ value: field14.uint8().default(255)
1374
1457
  }
1375
1458
  );
1376
1459
 
1377
1460
  // src/components/Pointer.ts
1378
1461
  import { CanvasComponentDef as CanvasComponentDef5 } from "@woven-ecs/canvas-store";
1379
- import { field as field14 } from "@woven-ecs/core";
1462
+ import { field as field15 } from "@woven-ecs/core";
1380
1463
  var PointerButton = {
1381
1464
  None: "none",
1382
1465
  Left: "left",
@@ -1393,30 +1476,30 @@ var PointerType = {
1393
1476
  var SAMPLE_COUNT = 6;
1394
1477
  var PointerSchema = {
1395
1478
  /** Unique pointer ID (from PointerEvent.pointerId) */
1396
- pointerId: field14.uint16().default(0),
1479
+ pointerId: field15.uint16().default(0),
1397
1480
  /** Current position relative to the editor element [x, y] */
1398
- position: field14.tuple(field14.float32(), 2).default([0, 0]),
1481
+ position: field15.tuple(field15.float32(), 2).default([0, 0]),
1399
1482
  /** Position where the pointer went down [x, y] */
1400
- downPosition: field14.tuple(field14.float32(), 2).default([0, 0]),
1483
+ downPosition: field15.tuple(field15.float32(), 2).default([0, 0]),
1401
1484
  /** Frame number when the pointer went down (for click detection) */
1402
- downFrame: field14.uint32().default(0),
1485
+ downFrame: field15.uint32().default(0),
1403
1486
  /** Which button is pressed */
1404
- button: field14.enum(PointerButton).default(PointerButton.None),
1487
+ button: field15.enum(PointerButton).default(PointerButton.None),
1405
1488
  /** Type of pointer device */
1406
- pointerType: field14.enum(PointerType).default(PointerType.Mouse),
1489
+ pointerType: field15.enum(PointerType).default(PointerType.Mouse),
1407
1490
  /** Pressure from 0 to 1 (for pen/touch) */
1408
- pressure: field14.float32().default(0),
1491
+ pressure: field15.float32().default(0),
1409
1492
  /** Whether the pointer event target was not the editor element */
1410
- obscured: field14.boolean().default(false),
1493
+ obscured: field15.boolean().default(false),
1411
1494
  // Velocity tracking (ring buffer for position samples)
1412
1495
  /** Ring buffer of previous positions [x0, y0, x1, y1, ...] @internal */
1413
- _prevPositions: field14.array(field14.float32(), SAMPLE_COUNT * 2),
1496
+ _prevPositions: field15.array(field15.float32(), SAMPLE_COUNT * 2),
1414
1497
  /** Ring buffer of timestamps for each position sample @internal */
1415
- _prevTimes: field14.array(field14.float32(), SAMPLE_COUNT),
1498
+ _prevTimes: field15.array(field15.float32(), SAMPLE_COUNT),
1416
1499
  /** Total number of samples added (used for ring buffer indexing) @internal */
1417
- _sampleCount: field14.int32().default(0),
1500
+ _sampleCount: field15.int32().default(0),
1418
1501
  /** Computed velocity [vx, vy] in pixels per second @internal */
1419
- _velocity: field14.tuple(field14.float32(), 2).default([0, 0])
1502
+ _velocity: field15.tuple(field15.float32(), 2).default([0, 0])
1420
1503
  };
1421
1504
  var PointerDef = class extends CanvasComponentDef5 {
1422
1505
  constructor() {
@@ -1527,96 +1610,96 @@ function addPointerSample(pointer, position, time) {
1527
1610
  }
1528
1611
 
1529
1612
  // src/components/ScaleWithZoom.ts
1530
- import { defineCanvasComponent as defineCanvasComponent13 } from "@woven-ecs/canvas-store";
1531
- import { field as field15 } from "@woven-ecs/core";
1532
- var ScaleWithZoom = defineCanvasComponent13(
1613
+ import { defineCanvasComponent as defineCanvasComponent14 } from "@woven-ecs/canvas-store";
1614
+ import { field as field16 } from "@woven-ecs/core";
1615
+ var ScaleWithZoom = defineCanvasComponent14(
1533
1616
  { name: "scaleWithZoom" },
1534
1617
  {
1535
1618
  /** Pivot point for scaling as [x, y] (0-1, default 0.5,0.5 = center) */
1536
- anchor: field15.tuple(field15.float64(), 2).default([0.5, 0.5]),
1619
+ anchor: field16.tuple(field16.float64(), 2).default([0.5, 0.5]),
1537
1620
  /** Initial position as [left, top] at zoom=1 */
1538
- startPosition: field15.tuple(field15.float64(), 2).default([0, 0]),
1621
+ startPosition: field16.tuple(field16.float64(), 2).default([0, 0]),
1539
1622
  /** Initial size as [width, height] at zoom=1 */
1540
- startSize: field15.tuple(field15.float64(), 2).default([0, 0]),
1623
+ startSize: field16.tuple(field16.float64(), 2).default([0, 0]),
1541
1624
  /** Scale multiplier per dimension: [x, y] (0 = no zoom effect, 1 = full zoom effect, 0.5 = half effect) */
1542
- scaleMultiplier: field15.tuple(field15.float64(), 2).default([1, 1])
1625
+ scaleMultiplier: field16.tuple(field16.float64(), 2).default([1, 1])
1543
1626
  }
1544
1627
  );
1545
1628
 
1546
1629
  // src/components/Selected.ts
1547
- import { defineCanvasComponent as defineCanvasComponent14 } from "@woven-ecs/canvas-store";
1548
- var Selected = defineCanvasComponent14({ name: "selected" }, {});
1630
+ import { defineCanvasComponent as defineCanvasComponent15 } from "@woven-ecs/canvas-store";
1631
+ var Selected = defineCanvasComponent15({ name: "selected" }, {});
1549
1632
 
1550
1633
  // src/components/SelectionBox.ts
1551
- import { defineCanvasComponent as defineCanvasComponent15 } from "@woven-ecs/canvas-store";
1552
- var SelectionBox = defineCanvasComponent15({ name: "selectionBox" }, {});
1634
+ import { defineCanvasComponent as defineCanvasComponent16 } from "@woven-ecs/canvas-store";
1635
+ var SelectionBox = defineCanvasComponent16({ name: "selectionBox" }, {});
1553
1636
 
1554
1637
  // src/components/Shape.ts
1555
- import { defineCanvasComponent as defineCanvasComponent16 } from "@woven-ecs/canvas-store";
1556
- import { field as field16 } from "@woven-ecs/core";
1638
+ import { defineCanvasComponent as defineCanvasComponent17 } from "@woven-ecs/canvas-store";
1639
+ import { field as field17 } from "@woven-ecs/core";
1557
1640
  var StrokeKind = {
1558
1641
  Solid: "solid",
1559
1642
  Dashed: "dashed",
1560
1643
  None: "none"
1561
1644
  };
1562
- var Shape = defineCanvasComponent16(
1645
+ var Shape = defineCanvasComponent17(
1563
1646
  { name: "shape", sync: "document" },
1564
1647
  {
1565
1648
  /** The kind of shape to render (e.g. 'rectangle', 'ellipse', or custom shape key) */
1566
- kind: field16.string().default("rectangle"),
1649
+ kind: field17.string().default("rectangle"),
1567
1650
  /** Stroke style */
1568
- strokeKind: field16.enum(StrokeKind).default(StrokeKind.Solid),
1651
+ strokeKind: field17.enum(StrokeKind).default(StrokeKind.Solid),
1569
1652
  /** Stroke width in pixels */
1570
- strokeWidth: field16.uint16().default(2),
1653
+ strokeWidth: field17.uint16().default(2),
1571
1654
  /** Stroke color - red component (0-255) */
1572
- strokeRed: field16.uint8().default(0),
1655
+ strokeRed: field17.uint8().default(0),
1573
1656
  /** Stroke color - green component (0-255) */
1574
- strokeGreen: field16.uint8().default(0),
1657
+ strokeGreen: field17.uint8().default(0),
1575
1658
  /** Stroke color - blue component (0-255) */
1576
- strokeBlue: field16.uint8().default(0),
1659
+ strokeBlue: field17.uint8().default(0),
1577
1660
  /** Stroke color - alpha component (0-255) */
1578
- strokeAlpha: field16.uint8().default(255),
1661
+ strokeAlpha: field17.uint8().default(255),
1579
1662
  /** Fill color - red component (0-255) */
1580
- fillRed: field16.uint8().default(255),
1663
+ fillRed: field17.uint8().default(255),
1581
1664
  /** Fill color - green component (0-255) */
1582
- fillGreen: field16.uint8().default(255),
1665
+ fillGreen: field17.uint8().default(255),
1583
1666
  /** Fill color - blue component (0-255) */
1584
- fillBlue: field16.uint8().default(255),
1667
+ fillBlue: field17.uint8().default(255),
1585
1668
  /** Fill color - alpha component (0-255) */
1586
- fillAlpha: field16.uint8().default(0)
1669
+ fillAlpha: field17.uint8().default(0)
1587
1670
  }
1588
1671
  );
1589
1672
 
1590
1673
  // src/components/Text.ts
1591
- import { defineCanvasComponent as defineCanvasComponent17 } from "@woven-ecs/canvas-store";
1592
- import { field as field17 } from "@woven-ecs/core";
1593
- var Text = defineCanvasComponent17(
1674
+ import { defineCanvasComponent as defineCanvasComponent18 } from "@woven-ecs/canvas-store";
1675
+ import { field as field18 } from "@woven-ecs/core";
1676
+ var Text = defineCanvasComponent18(
1594
1677
  { name: "text", sync: "document" },
1595
1678
  {
1596
1679
  /** HTML content (supports rich text formatting) */
1597
- content: field17.string().max(1e4).default(""),
1680
+ content: field18.string().max(1e4).default(""),
1598
1681
  /** Font size in pixels */
1599
- fontSizePx: field17.float64().default(24),
1682
+ fontSizePx: field18.float64().default(24),
1600
1683
  /** Font family name */
1601
- fontFamily: field17.string().max(64).default(""),
1684
+ fontFamily: field18.string().max(64).default(""),
1602
1685
  /** Line height multiplier */
1603
- lineHeight: field17.float64().default(1.2),
1686
+ lineHeight: field18.float64().default(1.2),
1604
1687
  /** Letter spacing in em units */
1605
- letterSpacingEm: field17.float64().default(0),
1688
+ letterSpacingEm: field18.float64().default(0),
1606
1689
  /** Whether width is constrained (text wraps) */
1607
- constrainWidth: field17.boolean().default(true),
1690
+ constrainWidth: field18.boolean().default(true),
1608
1691
  /** Default text alignment for new paragraphs */
1609
- defaultAlignment: field17.enum(TextAlignment).default(TextAlignment.Left)
1692
+ defaultAlignment: field18.enum(TextAlignment).default(TextAlignment.Left)
1610
1693
  }
1611
1694
  );
1612
1695
 
1613
1696
  // src/components/TransformBox.ts
1614
- import { defineCanvasComponent as defineCanvasComponent18 } from "@woven-ecs/canvas-store";
1615
- var TransformBox = defineCanvasComponent18({ name: "transformBox" }, {});
1697
+ import { defineCanvasComponent as defineCanvasComponent19 } from "@woven-ecs/canvas-store";
1698
+ var TransformBox = defineCanvasComponent19({ name: "transformBox" }, {});
1616
1699
 
1617
1700
  // src/components/TransformHandle.ts
1618
- import { defineCanvasComponent as defineCanvasComponent19 } from "@woven-ecs/canvas-store";
1619
- import { field as field18 } from "@woven-ecs/core";
1701
+ import { defineCanvasComponent as defineCanvasComponent20 } from "@woven-ecs/canvas-store";
1702
+ import { field as field19 } from "@woven-ecs/core";
1620
1703
 
1621
1704
  // src/cursors.ts
1622
1705
  var CursorKind = /* @__PURE__ */ ((CursorKind2) => {
@@ -1717,39 +1800,39 @@ var CURSORS = {
1717
1800
  };
1718
1801
 
1719
1802
  // src/components/TransformHandle.ts
1720
- var TransformHandle = defineCanvasComponent19(
1803
+ var TransformHandle = defineCanvasComponent20(
1721
1804
  { name: "transformHandle" },
1722
1805
  {
1723
- kind: field18.string().max(16).default(TransformHandleKind.Scale),
1724
- vectorX: field18.int8().default(0),
1725
- vectorY: field18.int8().default(0),
1726
- transformBox: field18.ref(),
1727
- cursorKind: field18.string().max(16).default("drag" /* Drag */)
1806
+ kind: field19.string().max(16).default(TransformHandleKind.Scale),
1807
+ vectorX: field19.int8().default(0),
1808
+ vectorY: field19.int8().default(0),
1809
+ transformBox: field19.ref(),
1810
+ cursorKind: field19.string().max(16).default("drag" /* Drag */)
1728
1811
  }
1729
1812
  );
1730
1813
 
1731
1814
  // src/components/User.ts
1732
- import { defineCanvasComponent as defineCanvasComponent20 } from "@woven-ecs/canvas-store";
1733
- import { field as field19 } from "@woven-ecs/core";
1734
- var User = defineCanvasComponent20(
1815
+ import { defineCanvasComponent as defineCanvasComponent21 } from "@woven-ecs/canvas-store";
1816
+ import { field as field20 } from "@woven-ecs/core";
1817
+ var User = defineCanvasComponent21(
1735
1818
  { name: "user", sync: "ephemeral" },
1736
1819
  {
1737
- userId: field19.string().max(36),
1738
- sessionId: field19.string().max(36),
1739
- color: field19.string().max(7),
1740
- name: field19.string().max(100),
1741
- avatar: field19.string().max(500),
1742
- position: field19.tuple(field19.float32(), 2).default([0, 0])
1820
+ userId: field20.string().max(36),
1821
+ sessionId: field20.string().max(36),
1822
+ color: field20.string().max(7),
1823
+ name: field20.string().max(100),
1824
+ avatar: field20.string().max(500),
1825
+ position: field20.tuple(field20.float32(), 2).default([0, 0])
1743
1826
  }
1744
1827
  );
1745
1828
 
1746
1829
  // src/components/VerticalAlign.ts
1747
- import { defineCanvasComponent as defineCanvasComponent21 } from "@woven-ecs/canvas-store";
1748
- import { field as field20 } from "@woven-ecs/core";
1749
- var VerticalAlign = defineCanvasComponent21(
1830
+ import { defineCanvasComponent as defineCanvasComponent22 } from "@woven-ecs/canvas-store";
1831
+ import { field as field21 } from "@woven-ecs/core";
1832
+ var VerticalAlign = defineCanvasComponent22(
1750
1833
  { name: "verticalAlign", sync: "document" },
1751
1834
  {
1752
- value: field20.enum(VerticalAlignment).default(VerticalAlignment.Top)
1835
+ value: field21.enum(VerticalAlignment).default(VerticalAlignment.Top)
1753
1836
  }
1754
1837
  );
1755
1838
 
@@ -1788,20 +1871,20 @@ __export(singletons_exports, {
1788
1871
 
1789
1872
  // src/singletons/Camera.ts
1790
1873
  import { CanvasSingletonDef as CanvasSingletonDef2 } from "@woven-ecs/canvas-store";
1791
- import { field as field22 } from "@woven-ecs/core";
1874
+ import { field as field23 } from "@woven-ecs/core";
1792
1875
 
1793
1876
  // src/singletons/Screen.ts
1794
1877
  import { CanvasSingletonDef } from "@woven-ecs/canvas-store";
1795
- import { field as field21 } from "@woven-ecs/core";
1878
+ import { field as field22 } from "@woven-ecs/core";
1796
1879
  var ScreenSchema = {
1797
1880
  /** Width of the editor element in pixels */
1798
- width: field21.float64().default(0),
1881
+ width: field22.float64().default(0),
1799
1882
  /** Height of the editor element in pixels */
1800
- height: field21.float64().default(0),
1883
+ height: field22.float64().default(0),
1801
1884
  /** Left offset of the editor element relative to the viewport */
1802
- left: field21.float64().default(0),
1885
+ left: field22.float64().default(0),
1803
1886
  /** Top offset of the editor element relative to the viewport */
1804
- top: field21.float64().default(0)
1887
+ top: field22.float64().default(0)
1805
1888
  };
1806
1889
  var ScreenDef = class extends CanvasSingletonDef {
1807
1890
  constructor() {
@@ -1828,15 +1911,15 @@ var Screen = new ScreenDef();
1828
1911
  // src/singletons/Camera.ts
1829
1912
  var CameraSchema = {
1830
1913
  /** Top position of the camera in world coordinates */
1831
- top: field22.float64().default(0),
1914
+ top: field23.float64().default(0),
1832
1915
  /** Left position of the camera in world coordinates */
1833
- left: field22.float64().default(0),
1916
+ left: field23.float64().default(0),
1834
1917
  /** Zoom level (1 = 100%, 2 = 200%, 0.5 = 50%) */
1835
- zoom: field22.float64().default(1),
1918
+ zoom: field23.float64().default(1),
1836
1919
  /** Whether the camera viewport intersects any blocks */
1837
- canSeeBlocks: field22.boolean().default(true),
1920
+ canSeeBlocks: field23.boolean().default(true),
1838
1921
  /** Reference to a block that the camera can currently see (for optimization) */
1839
- lastSeenBlock: field22.ref()
1922
+ lastSeenBlock: field23.ref()
1840
1923
  };
1841
1924
  var CameraDef = class extends CanvasSingletonDef2 {
1842
1925
  constructor() {
@@ -1912,22 +1995,22 @@ var Camera = new CameraDef();
1912
1995
 
1913
1996
  // src/singletons/Controls.ts
1914
1997
  import { CanvasSingletonDef as CanvasSingletonDef3 } from "@woven-ecs/canvas-store";
1915
- import { field as field23 } from "@woven-ecs/core";
1998
+ import { field as field24 } from "@woven-ecs/core";
1916
1999
  var ControlsSchema = {
1917
2000
  /** Tool activated by left mouse button */
1918
- leftMouseTool: field23.string().max(32).default("select"),
2001
+ leftMouseTool: field24.string().max(32).default("select"),
1919
2002
  /** Tool activated by middle mouse button */
1920
- middleMouseTool: field23.string().max(32).default("hand"),
2003
+ middleMouseTool: field24.string().max(32).default("hand"),
1921
2004
  /** Tool activated by right mouse button */
1922
- rightMouseTool: field23.string().max(32).default("hand"),
2005
+ rightMouseTool: field24.string().max(32).default("hand"),
1923
2006
  /** Tool activated by mouse wheel */
1924
- wheelTool: field23.string().max(32).default("scroll"),
2007
+ wheelTool: field24.string().max(32).default("scroll"),
1925
2008
  /** Tool activated by mouse wheel with modifier key held */
1926
- modWheelTool: field23.string().max(32).default("zoom"),
2009
+ modWheelTool: field24.string().max(32).default("zoom"),
1927
2010
  /** JSON snapshot of block to place on next click (empty string = no placement active) */
1928
- heldSnapshot: field23.string().max(65536).default(""),
2011
+ heldSnapshot: field24.string().max(65536).default(""),
1929
2012
  /** User-facing tool name for UI highlighting (may differ from leftMouseTool during draw/drag-out) */
1930
- activeToolName: field23.string().max(32).default("select")
2013
+ activeToolName: field24.string().max(32).default("select")
1931
2014
  };
1932
2015
  var ControlsDef = class extends CanvasSingletonDef3 {
1933
2016
  constructor() {
@@ -1969,16 +2052,16 @@ var Controls = new ControlsDef();
1969
2052
 
1970
2053
  // src/singletons/Cursor.ts
1971
2054
  import { CanvasSingletonDef as CanvasSingletonDef4 } from "@woven-ecs/canvas-store";
1972
- import { field as field24 } from "@woven-ecs/core";
2055
+ import { field as field25 } from "@woven-ecs/core";
1973
2056
  var CursorSchema = {
1974
2057
  /** Base cursor kind (from current tool) */
1975
- cursorKind: field24.string().max(64).default("select"),
2058
+ cursorKind: field25.string().max(64).default("select"),
1976
2059
  /** Base cursor rotation in radians */
1977
- rotation: field24.float64().default(0),
2060
+ rotation: field25.float64().default(0),
1978
2061
  /** Context-specific cursor kind (overrides cursorKind when set, e.g., during drag/hover) */
1979
- contextCursorKind: field24.string().max(64).default(""),
2062
+ contextCursorKind: field25.string().max(64).default(""),
1980
2063
  /** Context cursor rotation in radians */
1981
- contextRotation: field24.float64().default(0)
2064
+ contextRotation: field25.float64().default(0)
1982
2065
  };
1983
2066
  var CursorDef2 = class extends CanvasSingletonDef4 {
1984
2067
  constructor() {
@@ -2025,7 +2108,7 @@ var CursorDef2 = class extends CanvasSingletonDef4 {
2025
2108
  var Cursor = new CursorDef2();
2026
2109
 
2027
2110
  // src/singletons/FrameContainmentState.ts
2028
- import { field as field25 } from "@woven-ecs/core";
2111
+ import { field as field26 } from "@woven-ecs/core";
2029
2112
 
2030
2113
  // src/EditorStateDef.ts
2031
2114
  import { CanvasSingletonDef as CanvasSingletonDef5 } from "@woven-ecs/canvas-store";
@@ -2133,27 +2216,27 @@ function defineEditorState(name, schema) {
2133
2216
  // src/singletons/FrameContainmentState.ts
2134
2217
  var FrameContainmentState = defineEditorState("frameContainmentState", {
2135
2218
  /** Current state machine state */
2136
- state: field25.string().max(16).default(FrameContainmentStateEnum.Idle),
2219
+ state: field26.string().max(16).default(FrameContainmentStateEnum.Idle),
2137
2220
  /** EntityId of the frame currently highlighted as a drop target, or null */
2138
- highlightedFrame: field25.ref()
2221
+ highlightedFrame: field26.ref()
2139
2222
  });
2140
2223
 
2141
2224
  // src/singletons/Grid.ts
2142
2225
  import { CanvasSingletonDef as CanvasSingletonDef6 } from "@woven-ecs/canvas-store";
2143
- import { field as field26 } from "@woven-ecs/core";
2226
+ import { field as field27 } from "@woven-ecs/core";
2144
2227
  var GridSchema = {
2145
2228
  /** Whether grid snapping is enabled */
2146
- enabled: field26.boolean().default(false),
2229
+ enabled: field27.boolean().default(false),
2147
2230
  /** Whether resized/rotated objects must stay aligned to the grid */
2148
- strict: field26.boolean().default(false),
2231
+ strict: field27.boolean().default(false),
2149
2232
  /** Width of each grid column in world units */
2150
- colWidth: field26.float64().default(20),
2233
+ colWidth: field27.float64().default(20),
2151
2234
  /** Height of each grid row in world units */
2152
- rowHeight: field26.float64().default(20),
2235
+ rowHeight: field27.float64().default(20),
2153
2236
  /** Angular snap increment in radians when grid is enabled */
2154
- snapAngleRad: field26.float64().default(Math.PI / 36),
2237
+ snapAngleRad: field27.float64().default(Math.PI / 36),
2155
2238
  /** Angular snap increment in radians when shift key is held */
2156
- shiftSnapAngleRad: field26.float64().default(Math.PI / 12)
2239
+ shiftSnapAngleRad: field27.float64().default(Math.PI / 12)
2157
2240
  };
2158
2241
  var GridDef = class extends CanvasSingletonDef6 {
2159
2242
  constructor() {
@@ -2216,14 +2299,14 @@ var Grid = new GridDef();
2216
2299
 
2217
2300
  // src/singletons/Intersect.ts
2218
2301
  import { CanvasSingletonDef as CanvasSingletonDef7 } from "@woven-ecs/canvas-store";
2219
- import { field as field27 } from "@woven-ecs/core";
2302
+ import { field as field28 } from "@woven-ecs/core";
2220
2303
  var IntersectSchema = {
2221
2304
  // Store up to 5 intersected entity IDs
2222
- entity1: field27.ref(),
2223
- entity2: field27.ref(),
2224
- entity3: field27.ref(),
2225
- entity4: field27.ref(),
2226
- entity5: field27.ref()
2305
+ entity1: field28.ref(),
2306
+ entity2: field28.ref(),
2307
+ entity3: field28.ref(),
2308
+ entity4: field28.ref(),
2309
+ entity5: field28.ref()
2227
2310
  };
2228
2311
  var IntersectDef = class extends CanvasSingletonDef7 {
2229
2312
  constructor() {
@@ -2275,30 +2358,30 @@ var Intersect = new IntersectDef();
2275
2358
 
2276
2359
  // src/singletons/Keyboard.ts
2277
2360
  import { CanvasSingletonDef as CanvasSingletonDef8 } from "@woven-ecs/canvas-store";
2278
- import { field as field28 } from "@woven-ecs/core";
2361
+ import { field as field29 } from "@woven-ecs/core";
2279
2362
  var KEY_BUFFER_SIZE = 32;
2280
2363
  var KeyboardSchema = {
2281
2364
  /**
2282
2365
  * Buffer where each bit represents whether a key is currently pressed.
2283
2366
  * Uses field.buffer for zero-allocation subarray views.
2284
2367
  */
2285
- keysDown: field28.buffer(field28.uint8()).size(KEY_BUFFER_SIZE),
2368
+ keysDown: field29.buffer(field29.uint8()).size(KEY_BUFFER_SIZE),
2286
2369
  /**
2287
2370
  * Buffer for key-down triggers (true for exactly 1 frame when key is pressed).
2288
2371
  * Uses field.buffer for zero-allocation subarray views.
2289
2372
  */
2290
- keysDownTrigger: field28.buffer(field28.uint8()).size(KEY_BUFFER_SIZE),
2373
+ keysDownTrigger: field29.buffer(field29.uint8()).size(KEY_BUFFER_SIZE),
2291
2374
  /**
2292
2375
  * Buffer for key-up triggers (true for exactly 1 frame when key is released).
2293
2376
  * Uses field.buffer for zero-allocation subarray views.
2294
2377
  */
2295
- keysUpTrigger: field28.buffer(field28.uint8()).size(KEY_BUFFER_SIZE),
2378
+ keysUpTrigger: field29.buffer(field29.uint8()).size(KEY_BUFFER_SIZE),
2296
2379
  /** Common modifier - Shift key is down */
2297
- shiftDown: field28.boolean().default(false),
2380
+ shiftDown: field29.boolean().default(false),
2298
2381
  /** Common modifier - Alt/Option key is down */
2299
- altDown: field28.boolean().default(false),
2382
+ altDown: field29.boolean().default(false),
2300
2383
  /** Common modifier - Ctrl (Windows/Linux) or Cmd (Mac) is down */
2301
- modDown: field28.boolean().default(false)
2384
+ modDown: field29.boolean().default(false)
2302
2385
  };
2303
2386
  function getBit(buffer, bitIndex) {
2304
2387
  if (bitIndex < 0 || bitIndex >= buffer.length * 8) return false;
@@ -2581,24 +2664,24 @@ var Key = {
2581
2664
 
2582
2665
  // src/singletons/Mouse.ts
2583
2666
  import { CanvasSingletonDef as CanvasSingletonDef9 } from "@woven-ecs/canvas-store";
2584
- import { field as field29 } from "@woven-ecs/core";
2667
+ import { field as field30 } from "@woven-ecs/core";
2585
2668
  var MouseSchema = {
2586
2669
  /** Current mouse position relative to the editor element [x, y] */
2587
- position: field29.tuple(field29.float32(), 2).default([0, 0]),
2670
+ position: field30.tuple(field30.float32(), 2).default([0, 0]),
2588
2671
  /** Horizontal wheel delta (positive = scroll right) */
2589
- wheelDeltaX: field29.float32().default(0),
2672
+ wheelDeltaX: field30.float32().default(0),
2590
2673
  /** Vertical wheel delta (positive = scroll down), normalized across browsers */
2591
- wheelDeltaY: field29.float32().default(0),
2674
+ wheelDeltaY: field30.float32().default(0),
2592
2675
  /** True for 1 frame when mouse moves */
2593
- moveTrigger: field29.boolean().default(false),
2676
+ moveTrigger: field30.boolean().default(false),
2594
2677
  /** True for 1 frame when wheel is scrolled */
2595
- wheelTrigger: field29.boolean().default(false),
2678
+ wheelTrigger: field30.boolean().default(false),
2596
2679
  /** True for 1 frame when mouse enters the editor element */
2597
- enterTrigger: field29.boolean().default(false),
2680
+ enterTrigger: field30.boolean().default(false),
2598
2681
  /** True for 1 frame when mouse leaves the editor element */
2599
- leaveTrigger: field29.boolean().default(false),
2682
+ leaveTrigger: field30.boolean().default(false),
2600
2683
  /** Whether the mouse is over a UI element (not the canvas) */
2601
- obscured: field29.boolean().default(false)
2684
+ obscured: field30.boolean().default(false)
2602
2685
  };
2603
2686
  var MouseDef = class extends CanvasSingletonDef9 {
2604
2687
  constructor() {
@@ -2639,11 +2722,11 @@ var Mouse = new MouseDef();
2639
2722
 
2640
2723
  // src/singletons/RankBounds.ts
2641
2724
  import { CanvasSingletonDef as CanvasSingletonDef10 } from "@woven-ecs/canvas-store";
2642
- import { field as field30 } from "@woven-ecs/core";
2725
+ import { field as field31 } from "@woven-ecs/core";
2643
2726
  import { generateJitteredKeyBetween } from "fractional-indexing-jittered";
2644
2727
  var RankBoundsSchema = {
2645
- minRank: field30.string().max(36).default(""),
2646
- maxRank: field30.string().max(36).default("")
2728
+ minRank: field31.string().max(36).default(""),
2729
+ maxRank: field31.string().max(36).default("")
2647
2730
  };
2648
2731
  var RankBoundsDef = class extends CanvasSingletonDef10 {
2649
2732
  constructor() {
@@ -2702,64 +2785,64 @@ var RankBounds = new RankBoundsDef();
2702
2785
 
2703
2786
  // src/singletons/ScaleWithZoomState.ts
2704
2787
  import { defineCanvasSingleton } from "@woven-ecs/canvas-store";
2705
- import { field as field31 } from "@woven-ecs/core";
2788
+ import { field as field32 } from "@woven-ecs/core";
2706
2789
  var ScaleWithZoomState = defineCanvasSingleton(
2707
2790
  { name: "scaleWithZoomState" },
2708
2791
  {
2709
2792
  /** Last processed zoom level */
2710
- lastZoom: field31.float64().default(1)
2793
+ lastZoom: field32.float64().default(1)
2711
2794
  }
2712
2795
  );
2713
2796
 
2714
2797
  // src/singletons/ScrollEdgesStateSingleton.ts
2715
- import { field as field32 } from "@woven-ecs/core";
2798
+ import { field as field33 } from "@woven-ecs/core";
2716
2799
  var ScrollEdgesStateSingleton = defineEditorState("scrollEdgesState", {
2717
2800
  /** Current state of the scroll edges state machine */
2718
- state: field32.enum(ScrollEdgesState).default(ScrollEdgesState.Idle),
2801
+ state: field33.enum(ScrollEdgesState).default(ScrollEdgesState.Idle),
2719
2802
  /** Timestamp when pointer first entered the edge zone */
2720
- edgeEnterStartTime: field32.float64().default(0)
2803
+ edgeEnterStartTime: field33.float64().default(0)
2721
2804
  });
2722
2805
 
2723
2806
  // src/singletons/SelectionStateSingleton.ts
2724
- import { field as field33 } from "@woven-ecs/core";
2807
+ import { field as field34 } from "@woven-ecs/core";
2725
2808
  var SelectionStateSingleton = defineEditorState("selectionState", {
2726
- state: field33.enum(SelectionState).default(SelectionState.Idle),
2727
- dragStart: field33.tuple(field33.float64(), 2).default([0, 0]),
2728
- pointingStartClient: field33.tuple(field33.float64(), 2).default([0, 0]),
2729
- pointingStartWorld: field33.tuple(field33.float64(), 2).default([0, 0]),
2730
- draggedEntityStart: field33.tuple(field33.float64(), 2).default([0, 0]),
2731
- draggedEntity: field33.ref(),
2732
- cloneGeneratorSeed: field33.string().max(36).default(""),
2733
- isCloning: field33.boolean()
2809
+ state: field34.enum(SelectionState).default(SelectionState.Idle),
2810
+ dragStart: field34.tuple(field34.float64(), 2).default([0, 0]),
2811
+ pointingStartClient: field34.tuple(field34.float64(), 2).default([0, 0]),
2812
+ pointingStartWorld: field34.tuple(field34.float64(), 2).default([0, 0]),
2813
+ draggedEntityStart: field34.tuple(field34.float64(), 2).default([0, 0]),
2814
+ draggedEntity: field34.ref(),
2815
+ cloneGeneratorSeed: field34.string().max(36).default(""),
2816
+ isCloning: field34.boolean()
2734
2817
  });
2735
2818
 
2736
2819
  // src/singletons/Tick.ts
2737
2820
  import { defineCanvasSingleton as defineCanvasSingleton2 } from "@woven-ecs/canvas-store";
2738
- import { field as field34 } from "@woven-ecs/core";
2821
+ import { field as field35 } from "@woven-ecs/core";
2739
2822
  var Tick = defineCanvasSingleton2(
2740
2823
  { name: "tick" },
2741
2824
  {
2742
2825
  /** Current tick number (increments each tick) */
2743
- number: field34.uint32().default(0),
2826
+ number: field35.uint32().default(0),
2744
2827
  /** Time since last tick in seconds */
2745
- delta: field34.float64().default(0),
2828
+ delta: field35.float64().default(0),
2746
2829
  /** Timestamp of current tick in milliseconds (from performance.now()) */
2747
- time: field34.float64().default(0),
2830
+ time: field35.float64().default(0),
2748
2831
  /** Timestamp of previous tick in milliseconds (0 if first tick) */
2749
- lastTime: field34.float64().default(0)
2832
+ lastTime: field35.float64().default(0)
2750
2833
  }
2751
2834
  );
2752
2835
 
2753
2836
  // src/singletons/TransformBoxStateSingleton.ts
2754
- import { field as field35 } from "@woven-ecs/core";
2837
+ import { field as field36 } from "@woven-ecs/core";
2755
2838
  var TransformBoxStateSingleton = defineEditorState("transformBoxState", {
2756
- state: field35.string().max(32).default(TransformBoxState.None),
2757
- transformBoxId: field35.ref()
2839
+ state: field36.string().max(32).default(TransformBoxState.None),
2840
+ transformBoxId: field36.ref()
2758
2841
  });
2759
2842
 
2760
2843
  // src/systems/capture/captureFrameContainmentSystem.ts
2761
- import { Synced as Synced2 } from "@woven-ecs/canvas-store";
2762
- import { defineQuery as defineQuery7, hasComponent as hasComponent10 } from "@woven-ecs/core";
2844
+ import { Synced as Synced3 } from "@woven-ecs/canvas-store";
2845
+ import { defineQuery as defineQuery7, hasComponent as hasComponent11 } from "@woven-ecs/core";
2763
2846
  import { assign, setup } from "xstate";
2764
2847
 
2765
2848
  // src/EditorSystem.ts
@@ -2848,6 +2931,9 @@ function canBlockEdit(ctx, tag) {
2848
2931
  function canBlockInteract(ctx, tag) {
2849
2932
  return getBlockDef(ctx, tag).interactable;
2850
2933
  }
2934
+ function canBlockSnap(ctx, tag) {
2935
+ return getBlockDef(ctx, tag).snappable;
2936
+ }
2851
2937
  function getBlockResizeMode(ctx, entityId) {
2852
2938
  const block = Block.read(ctx, entityId);
2853
2939
  if (block.resizeMode !== ResizeMode.Default) {
@@ -2870,6 +2956,17 @@ function computeAabb(ctx, entityId, out) {
2870
2956
  }
2871
2957
  }
2872
2958
 
2959
+ // src/helpers/createBlock.ts
2960
+ import { Synced } from "@woven-ecs/canvas-store";
2961
+ import { addComponent as addComponent2, createEntity as createEntity2 } from "@woven-ecs/core";
2962
+ function createBlock(ctx, block) {
2963
+ const entityId = createEntity2(ctx);
2964
+ addComponent2(ctx, entityId, Synced, { id: crypto.randomUUID() });
2965
+ addComponent2(ctx, entityId, Block, { ...block, rank: block.rank ?? RankBounds.genNext(ctx) });
2966
+ PlaceBlockEvent.spawn(ctx, { entityId });
2967
+ return entityId;
2968
+ }
2969
+
2873
2970
  // src/helpers/embed.ts
2874
2971
  function detectEmbedProvider(url) {
2875
2972
  try {
@@ -3083,6 +3180,9 @@ function compareRankChains(a, b) {
3083
3180
  function compareBlockOrder(getBlock, a, b) {
3084
3181
  const stratumDiff = STRATUM_ORDER[a.stratum] - STRATUM_ORDER[b.stratum];
3085
3182
  if (stratumDiff !== 0) return stratumDiff;
3183
+ const aLayer = a.layerRank ?? "";
3184
+ const bLayer = b.layerRank ?? "";
3185
+ if (aLayer !== bLayer) return aLayer < bLayer ? -1 : 1;
3086
3186
  const chainA = getRankChain(getBlock, a);
3087
3187
  const chainB = getRankChain(getBlock, b);
3088
3188
  return compareRankChains(chainA, chainB);
@@ -3127,12 +3227,30 @@ function isHeldByRemote(ctx, entityId) {
3127
3227
 
3128
3228
  // src/helpers/intersect.ts
3129
3229
  import { Aabb as Aabb3 } from "@woven-canvas/math";
3130
- import { defineQuery as defineQuery3, hasComponent as hasComponent6 } from "@woven-ecs/core";
3230
+ import { defineQuery as defineQuery3, hasComponent as hasComponent7 } from "@woven-ecs/core";
3231
+
3232
+ // src/helpers/layerRank.ts
3233
+ import { hasComponent as hasComponent6 } from "@woven-ecs/core";
3234
+ function resolveLayerRank(ctx, entityId) {
3235
+ if (!hasComponent6(ctx, entityId, Block)) return "";
3236
+ const layerId = Block.read(ctx, entityId).layerId;
3237
+ if (layerId !== null && hasComponent6(ctx, layerId, Layer)) {
3238
+ return Layer.read(ctx, layerId).rank;
3239
+ }
3240
+ return "";
3241
+ }
3242
+ function isLayerInteractive(ctx, layerId) {
3243
+ if (layerId === null || !hasComponent6(ctx, layerId, Layer)) return true;
3244
+ const layer = Layer.read(ctx, layerId);
3245
+ return !layer.hidden && !layer.locked;
3246
+ }
3247
+
3248
+ // src/helpers/intersect.ts
3131
3249
  var blocksWithAabb = defineQuery3((q) => q.with(Block, Aabb));
3132
3250
  function isPointInsideAncestorFrames(ctx, entityId, point) {
3133
3251
  let current = Block.read(ctx, entityId).parentId;
3134
- while (current !== null && hasComponent6(ctx, current, Block)) {
3135
- if (hasComponent6(ctx, current, Frame)) {
3252
+ while (current !== null && hasComponent7(ctx, current, Block)) {
3253
+ if (hasComponent7(ctx, current, Frame)) {
3136
3254
  if (!Aabb.containsPoint(ctx, current, point)) {
3137
3255
  return false;
3138
3256
  }
@@ -3143,8 +3261,8 @@ function isPointInsideAncestorFrames(ctx, entityId, point) {
3143
3261
  }
3144
3262
  function isAabbInsideAncestorFrames(ctx, entityId, bounds) {
3145
3263
  let current = Block.read(ctx, entityId).parentId;
3146
- while (current !== null && hasComponent6(ctx, current, Block)) {
3147
- if (hasComponent6(ctx, current, Frame)) {
3264
+ while (current !== null && hasComponent7(ctx, current, Block)) {
3265
+ if (hasComponent7(ctx, current, Frame)) {
3148
3266
  const { value: frameAabb } = Aabb.read(ctx, current);
3149
3267
  if (!Aabb3.intersects(bounds, frameAabb)) {
3150
3268
  return false;
@@ -3160,10 +3278,11 @@ function intersectPoint(ctx, point, entityIds) {
3160
3278
  for (const entityId of entities) {
3161
3279
  const block = Block.read(ctx, entityId);
3162
3280
  if (!canBlockInteract(ctx, block.tag)) continue;
3281
+ if (!isLayerInteractive(ctx, block.layerId)) continue;
3163
3282
  if (!Aabb.containsPoint(ctx, entityId, point)) {
3164
3283
  continue;
3165
3284
  }
3166
- if (hasComponent6(ctx, entityId, HitGeometry)) {
3285
+ if (hasComponent7(ctx, entityId, HitGeometry)) {
3167
3286
  if (!HitGeometry.containsPointWorld(ctx, entityId, point)) {
3168
3287
  continue;
3169
3288
  }
@@ -3185,12 +3304,13 @@ function intersectAabb(ctx, bounds, entityIds) {
3185
3304
  for (const entityId of entities) {
3186
3305
  const block = Block.read(ctx, entityId);
3187
3306
  if (!canBlockInteract(ctx, block.tag)) continue;
3307
+ if (!isLayerInteractive(ctx, block.layerId)) continue;
3188
3308
  const { value: entityAabb } = Aabb.read(ctx, entityId);
3189
3309
  if (!Aabb3.intersects(bounds, entityAabb)) {
3190
3310
  continue;
3191
3311
  }
3192
3312
  if (!Aabb3.contains(bounds, entityAabb)) {
3193
- if (hasComponent6(ctx, entityId, HitGeometry)) {
3313
+ if (hasComponent7(ctx, entityId, HitGeometry)) {
3194
3314
  if (!HitGeometry.intersectsAabbWorld(ctx, entityId, bounds)) {
3195
3315
  continue;
3196
3316
  }
@@ -3209,12 +3329,13 @@ function intersectAabb(ctx, bounds, entityIds) {
3209
3329
  }
3210
3330
  function sortByRankDescending(ctx, entityIds) {
3211
3331
  const getBlock = (id) => {
3212
- if (!hasComponent6(ctx, id, Block)) return null;
3332
+ if (!hasComponent7(ctx, id, Block)) return null;
3213
3333
  const block = Block.read(ctx, id);
3214
3334
  return {
3215
3335
  rank: block.rank,
3216
3336
  stratum: getBlockDef(ctx, block.tag).stratum,
3217
- parentId: block.parentId
3337
+ parentId: block.parentId,
3338
+ layerRank: resolveLayerRank(ctx, id)
3218
3339
  };
3219
3340
  };
3220
3341
  const entries = entityIds.map((id) => ({
@@ -3230,13 +3351,15 @@ function getTopmostBlockAtPoint(ctx, point) {
3230
3351
  }
3231
3352
 
3232
3353
  // src/helpers/intersectCapsule.ts
3233
- import { Aabb as AabbNs, Arc as Arc2, Capsule as Capsule2, Mat2 as Mat22 } from "@woven-canvas/math";
3234
- import { hasComponent as hasComponent7 } from "@woven-ecs/core";
3354
+ import { Aabb as AabbNs, Arc as Arc2, Capsule as Capsule2, Mat2 as Mat22, Polygon as Polygon2 } from "@woven-canvas/math";
3355
+ import { hasComponent as hasComponent8 } from "@woven-ecs/core";
3235
3356
  var _uvToWorldMatrix2 = [1, 0, 0, 1, 0, 0];
3357
+ var _worldPolygon2 = new Float32Array(MAX_HIT_POLYGON_POINTS * 2);
3358
+ var _polyPt2 = [0, 0];
3236
3359
  function isCapsuleInsideAncestorFrames(ctx, entityId, capsule) {
3237
3360
  let current = Block.read(ctx, entityId).parentId;
3238
- while (current !== null && hasComponent7(ctx, current, Block)) {
3239
- if (hasComponent7(ctx, current, Frame)) {
3361
+ while (current !== null && hasComponent8(ctx, current, Block)) {
3362
+ if (hasComponent8(ctx, current, Frame)) {
3240
3363
  const { value: frameAabb } = Aabb.read(ctx, current);
3241
3364
  if (!Capsule2.intersectsAabb(capsule, frameAabb)) {
3242
3365
  return false;
@@ -3254,7 +3377,7 @@ function intersectCapsule(ctx, capsule, entityIds) {
3254
3377
  if (!AabbNs.intersects(capsuleBounds, aabb.value)) {
3255
3378
  continue;
3256
3379
  }
3257
- if (hasComponent7(ctx, entityId, HitGeometry)) {
3380
+ if (hasComponent8(ctx, entityId, HitGeometry)) {
3258
3381
  if (!intersectsCapsuleHitGeometry(ctx, entityId, capsule)) {
3259
3382
  continue;
3260
3383
  }
@@ -3301,12 +3424,25 @@ function intersectsCapsuleHitGeometry(ctx, entityId, capsule) {
3301
3424
  return true;
3302
3425
  }
3303
3426
  }
3427
+ const polygonPointCount = hitGeometry.polygonPointCount;
3428
+ if (polygonPointCount > 0) {
3429
+ for (let i = 0; i < polygonPointCount; i++) {
3430
+ _polyPt2[0] = hitGeometry.hitPolygon[i * 2];
3431
+ _polyPt2[1] = hitGeometry.hitPolygon[i * 2 + 1];
3432
+ Mat22.transformPoint(_uvToWorldMatrix2, _polyPt2);
3433
+ _worldPolygon2[i * 2] = _polyPt2[0];
3434
+ _worldPolygon2[i * 2 + 1] = _polyPt2[1];
3435
+ }
3436
+ if (Polygon2.intersectsCapsule(_worldPolygon2, polygonPointCount, capsule)) {
3437
+ return true;
3438
+ }
3439
+ }
3304
3440
  return false;
3305
3441
  }
3306
3442
 
3307
3443
  // src/helpers/ref.ts
3308
- import { Synced } from "@woven-ecs/canvas-store";
3309
- import { hasComponent as hasComponent8 } from "@woven-ecs/core";
3444
+ import { Synced as Synced2 } from "@woven-ecs/canvas-store";
3445
+ import { hasComponent as hasComponent9 } from "@woven-ecs/core";
3310
3446
  function convertRefsToUuids(ctx, schema, data) {
3311
3447
  const result = { ...data };
3312
3448
  for (const [fieldName, fieldBuilder] of Object.entries(schema)) {
@@ -3319,8 +3455,8 @@ function convertRefsToUuids(ctx, schema, data) {
3319
3455
  }
3320
3456
  function getEntityUuid(ctx, entityId) {
3321
3457
  if (entityId == null) return null;
3322
- if (!hasComponent8(ctx, entityId, Synced)) return null;
3323
- return Synced.read(ctx, entityId).id;
3458
+ if (!hasComponent9(ctx, entityId, Synced2)) return null;
3459
+ return Synced2.read(ctx, entityId).id;
3324
3460
  }
3325
3461
  function getRefFieldNames(schema) {
3326
3462
  const refFields = [];
@@ -3345,29 +3481,29 @@ function resolveRefFields(ctx, entityId, componentDef, componentData, uuidToEnti
3345
3481
 
3346
3482
  // src/helpers/select.ts
3347
3483
  import {
3348
- addComponent as addComponent2,
3484
+ addComponent as addComponent3,
3349
3485
  defineQuery as defineQuery4,
3350
3486
  getResources as getResources4,
3351
- hasComponent as hasComponent9,
3487
+ hasComponent as hasComponent10,
3352
3488
  removeComponent
3353
3489
  } from "@woven-ecs/core";
3354
3490
  var selectedQuery = defineQuery4((q) => q.with(Selected));
3355
3491
  var blockQuery = defineQuery4((q) => q.with(Block));
3356
3492
  function selectBlock(ctx, entityId) {
3357
- if (hasComponent9(ctx, entityId, Selected)) return;
3493
+ if (hasComponent10(ctx, entityId, Selected)) return;
3358
3494
  if (isHeldByRemote(ctx, entityId)) return;
3359
- if (hasComponent9(ctx, entityId, Block)) {
3495
+ if (hasComponent10(ctx, entityId, Block)) {
3360
3496
  const block = Block.read(ctx, entityId);
3361
3497
  if (!canBlockInteract(ctx, block.tag)) return;
3362
3498
  }
3363
3499
  const { sessionId } = getResources4(ctx);
3364
- addComponent2(ctx, entityId, Selected, {});
3365
- if (!hasComponent9(ctx, entityId, Held)) {
3366
- addComponent2(ctx, entityId, Held, { sessionId });
3500
+ addComponent3(ctx, entityId, Selected, {});
3501
+ if (!hasComponent10(ctx, entityId, Held)) {
3502
+ addComponent3(ctx, entityId, Held, { sessionId });
3367
3503
  }
3368
3504
  }
3369
3505
  function deselectBlock(ctx, entityId) {
3370
- if (!hasComponent9(ctx, entityId, Selected)) return;
3506
+ if (!hasComponent10(ctx, entityId, Selected)) return;
3371
3507
  removeComponent(ctx, entityId, Selected);
3372
3508
  removeComponent(ctx, entityId, Held);
3373
3509
  }
@@ -3581,13 +3717,13 @@ function spawnAssignments(ctx, targetFrame) {
3581
3717
  const draggedEntity = getDraggedEntity(ctx);
3582
3718
  if (draggedEntity === null) return;
3583
3719
  const assignments = [];
3584
- if (hasComponent10(ctx, draggedEntity, TransformBox)) {
3720
+ if (hasComponent11(ctx, draggedEntity, TransformBox)) {
3585
3721
  for (const blockId of selectedQuery2.current(ctx)) {
3586
3722
  if (targetFrame !== null && blockId === targetFrame) continue;
3587
3723
  if (targetFrame !== null && isAncestor(ctx, targetFrame, blockId)) continue;
3588
3724
  assignments.push({ entityId: blockId, frameId: targetFrame });
3589
3725
  }
3590
- } else if (hasComponent10(ctx, draggedEntity, Synced2)) {
3726
+ } else if (hasComponent11(ctx, draggedEntity, Synced3)) {
3591
3727
  if (targetFrame !== null && draggedEntity === targetFrame) return;
3592
3728
  if (targetFrame !== null && isAncestor(ctx, targetFrame, draggedEntity)) return;
3593
3729
  assignments.push({ entityId: draggedEntity, frameId: targetFrame });
@@ -3712,7 +3848,7 @@ var hoverCursorSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
3712
3848
  });
3713
3849
 
3714
3850
  // src/systems/capture/keybindSystem.ts
3715
- import { addComponent as addComponent3, createEntity as createEntity2, getResources as getResources6 } from "@woven-ecs/core";
3851
+ import { addComponent as addComponent4, createEntity as createEntity3, getResources as getResources6 } from "@woven-ecs/core";
3716
3852
  var keybindSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
3717
3853
  if (isReadonly(ctx)) return;
3718
3854
  const keyboard = Keyboard.read(ctx);
@@ -3722,8 +3858,8 @@ var keybindSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
3722
3858
  triggered &&= !!keybind.mod === keyboard.modDown;
3723
3859
  triggered &&= !!keybind.shift === keyboard.shiftDown;
3724
3860
  if (triggered) {
3725
- const eid = createEntity2(ctx);
3726
- addComponent3(ctx, eid, CommandMarker, { name: keybind.command });
3861
+ const eid = createEntity3(ctx);
3862
+ addComponent4(ctx, eid, CommandMarker, { name: keybind.command });
3727
3863
  break;
3728
3864
  }
3729
3865
  }
@@ -3847,7 +3983,7 @@ var scrollEdgesSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
3847
3983
  });
3848
3984
 
3849
3985
  // src/systems/capture/transformBoxSystem.ts
3850
- import { createEntity as createEntity3, defineQuery as defineQuery9, hasComponent as hasComponent11, removeComponent as removeComponent2 } from "@woven-ecs/core";
3986
+ import { createEntity as createEntity4, defineQuery as defineQuery9, hasComponent as hasComponent12, removeComponent as removeComponent2 } from "@woven-ecs/core";
3851
3987
  import { and, assign as assign3, not as not2, raise, setup as setup3 } from "xstate";
3852
3988
  var selectedBlocksQuery = defineQuery9((q) => q.with(Block).tracking(Selected));
3853
3989
  var transformBoxMachine = setup3({
@@ -3862,12 +3998,12 @@ var transformBoxMachine = setup3({
3862
3998
  isOverTransformBox: ({ event }) => {
3863
3999
  if (!("intersects" in event)) return false;
3864
4000
  if (event.intersects.length === 0) return false;
3865
- return hasComponent11(event.ctx, event.intersects[0], TransformBox);
4001
+ return hasComponent12(event.ctx, event.intersects[0], TransformBox);
3866
4002
  },
3867
4003
  isOverTransformHandle: ({ event }) => {
3868
4004
  if (!("intersects" in event)) return false;
3869
4005
  if (event.intersects.length === 0) return false;
3870
- return hasComponent11(event.ctx, event.intersects[0], TransformHandle);
4006
+ return hasComponent12(event.ctx, event.intersects[0], TransformHandle);
3871
4007
  },
3872
4008
  selectionIsTransformable: ({ event }) => {
3873
4009
  if (event.type !== "selectionChanged") return false;
@@ -3879,7 +4015,7 @@ var transformBoxMachine = setup3({
3879
4015
  hasEditAfterPlacing: ({ event }) => {
3880
4016
  if (event.type !== "selectionChanged") return false;
3881
4017
  if (event.selectedEntityIds.length !== 1) return false;
3882
- return hasComponent11(event.ctx, event.selectedEntityIds[0], EditAfterPlacing);
4018
+ return hasComponent12(event.ctx, event.selectedEntityIds[0], EditAfterPlacing);
3883
4019
  }
3884
4020
  },
3885
4021
  actions: {
@@ -3889,7 +4025,7 @@ var transformBoxMachine = setup3({
3889
4025
  transformBoxId: context.transformBoxId
3890
4026
  });
3891
4027
  }
3892
- const transformBoxId = createEntity3(event.ctx);
4028
+ const transformBoxId = createEntity4(event.ctx);
3893
4029
  AddTransformBox.spawn(event.ctx, {
3894
4030
  transformBoxId,
3895
4031
  skipHandles: params?.skipHandles
@@ -3932,7 +4068,7 @@ var transformBoxMachine = setup3({
3932
4068
  removeEditAfterPlacing: ({ event }) => {
3933
4069
  if (event.type !== "selectionChanged") return;
3934
4070
  for (const entityId of event.selectedEntityIds) {
3935
- if (hasComponent11(event.ctx, entityId, EditAfterPlacing)) {
4071
+ if (hasComponent12(event.ctx, entityId, EditAfterPlacing)) {
3936
4072
  removeComponent2(event.ctx, entityId, EditAfterPlacing);
3937
4073
  }
3938
4074
  }
@@ -4227,7 +4363,7 @@ function normalizeWheelDelta(deltaY, deltaMode) {
4227
4363
  }
4228
4364
 
4229
4365
  // src/systems/input/pointerSystem.ts
4230
- import { addComponent as addComponent4, createEntity as createEntity4, getResources as getResources9, removeEntity as removeEntity3 } from "@woven-ecs/core";
4366
+ import { addComponent as addComponent5, createEntity as createEntity5, getResources as getResources9, removeEntity as removeEntity3 } from "@woven-ecs/core";
4231
4367
  var instanceState3 = /* @__PURE__ */ new WeakMap();
4232
4368
  function attachPointerListeners(domElement) {
4233
4369
  if (instanceState3.has(domElement)) return;
@@ -4334,8 +4470,8 @@ var pointerSystem = defineEditorSystem({ phase: "input" }, (ctx) => {
4334
4470
  switch (event.type) {
4335
4471
  case "pointerdown": {
4336
4472
  const position = [event.clientX - screen.left, event.clientY - screen.top];
4337
- const entityId = createEntity4(ctx);
4338
- addComponent4(ctx, entityId, Pointer, {
4473
+ const entityId = createEntity5(ctx);
4474
+ addComponent5(ctx, entityId, Pointer, {
4339
4475
  pointerId: event.pointerId,
4340
4476
  position,
4341
4477
  downPosition: position,
@@ -4497,11 +4633,11 @@ function updateUserPosition(ctx, position) {
4497
4633
  // src/systems/postUpdate/transformBoxSystem.ts
4498
4634
  import { Rect as Rect2, Vec2 as Vec23 } from "@woven-canvas/math";
4499
4635
  import {
4500
- addComponent as addComponent5,
4501
- createEntity as createEntity5,
4636
+ addComponent as addComponent6,
4637
+ createEntity as createEntity6,
4502
4638
  defineQuery as defineQuery12,
4503
4639
  getBackrefs as getBackrefs2,
4504
- hasComponent as hasComponent12,
4640
+ hasComponent as hasComponent13,
4505
4641
  isAlive,
4506
4642
  removeComponent as removeComponent3,
4507
4643
  removeEntity as removeEntity4
@@ -4534,7 +4670,7 @@ var transformBoxSystem2 = defineEditorSystem({ phase: "update", priority: -100 }
4534
4670
  on(ctx, EndTransformBoxEdit, endTransformBoxEdit);
4535
4671
  });
4536
4672
  function removeTransformBox(ctx, transformBoxId) {
4537
- if (!hasComponent12(ctx, transformBoxId, TransformBox)) {
4673
+ if (!hasComponent13(ctx, transformBoxId, TransformBox)) {
4538
4674
  endTransformBoxEdit(ctx);
4539
4675
  return;
4540
4676
  }
@@ -4546,15 +4682,15 @@ function removeTransformBox(ctx, transformBoxId) {
4546
4682
  endTransformBoxEdit(ctx);
4547
4683
  }
4548
4684
  function addTransformBox(ctx, transformBoxId, skipHandles) {
4549
- addComponent5(ctx, transformBoxId, Block, {
4685
+ addComponent6(ctx, transformBoxId, Block, {
4550
4686
  tag: "transform-box",
4551
4687
  position: [0, 0],
4552
4688
  size: [0, 0],
4553
4689
  rotateZ: 0,
4554
4690
  rank: TRANSFORM_BOX_RANK
4555
4691
  });
4556
- addComponent5(ctx, transformBoxId, TransformBox, {});
4557
- addComponent5(ctx, transformBoxId, DragStart, {
4692
+ addComponent6(ctx, transformBoxId, TransformBox, {});
4693
+ addComponent6(ctx, transformBoxId, DragStart, {
4558
4694
  position: [0, 0],
4559
4695
  size: [0, 0],
4560
4696
  rotateZ: 0,
@@ -4591,7 +4727,7 @@ function updateTransformBox(ctx, transformBoxId) {
4591
4727
  const selectionState = SelectionStateSingleton.read(ctx);
4592
4728
  const isDragging = selectionState.state === SelectionState.Dragging;
4593
4729
  if (!isDragging) {
4594
- if (hasComponent12(ctx, transformBoxId, DragStart)) {
4730
+ if (hasComponent13(ctx, transformBoxId, DragStart)) {
4595
4731
  const dragStart = DragStart.write(ctx, transformBoxId);
4596
4732
  dragStart.position = boxBlock.position;
4597
4733
  dragStart.size = boxBlock.size;
@@ -4600,9 +4736,9 @@ function updateTransformBox(ctx, transformBoxId) {
4600
4736
  for (const entityId of selectedBlocks) {
4601
4737
  const block = Block.read(ctx, entityId);
4602
4738
  const worldPos = Block.getWorldPosition(ctx, entityId);
4603
- const fontSize = hasComponent12(ctx, entityId, Text) ? Text.read(ctx, entityId).fontSizePx : 16;
4604
- if (!hasComponent12(ctx, entityId, DragStart)) {
4605
- addComponent5(ctx, entityId, DragStart, {
4739
+ const fontSize = hasComponent13(ctx, entityId, Text) ? Text.read(ctx, entityId).fontSizePx : 16;
4740
+ if (!hasComponent13(ctx, entityId, DragStart)) {
4741
+ addComponent6(ctx, entityId, DragStart, {
4606
4742
  position: worldPos,
4607
4743
  size: block.size,
4608
4744
  rotateZ: block.rotateZ,
@@ -4784,11 +4920,11 @@ function addOrUpdateTransformHandles(ctx, transformBoxId) {
4784
4920
  const finalLeft = rotatedCenter[0] - def.width / 2;
4785
4921
  const finalTop = rotatedCenter[1] - def.height / 2;
4786
4922
  if (!handleId) {
4787
- handleId = createEntity5(ctx);
4788
- addComponent5(ctx, handleId, Block);
4789
- addComponent5(ctx, handleId, TransformHandle);
4790
- addComponent5(ctx, handleId, DragStart);
4791
- addComponent5(ctx, handleId, ScaleWithZoom);
4923
+ handleId = createEntity6(ctx);
4924
+ addComponent6(ctx, handleId, Block);
4925
+ addComponent6(ctx, handleId, TransformHandle);
4926
+ addComponent6(ctx, handleId, DragStart);
4927
+ addComponent6(ctx, handleId, ScaleWithZoom);
4792
4928
  }
4793
4929
  const block = Block.write(ctx, handleId);
4794
4930
  block.tag = def.tag;
@@ -4869,11 +5005,11 @@ function addTapeHandles(ctx, transformBoxId, left, top, width, height, rotateZ,
4869
5005
  const finalLeft = rotatedCenter[0] - def.width / 2;
4870
5006
  const finalTop = rotatedCenter[1] - def.height / 2;
4871
5007
  if (!handleId) {
4872
- handleId = createEntity5(ctx);
4873
- addComponent5(ctx, handleId, Block);
4874
- addComponent5(ctx, handleId, TransformHandle);
4875
- addComponent5(ctx, handleId, DragStart);
4876
- addComponent5(ctx, handleId, ScaleWithZoom);
5008
+ handleId = createEntity6(ctx);
5009
+ addComponent6(ctx, handleId, Block);
5010
+ addComponent6(ctx, handleId, TransformHandle);
5011
+ addComponent6(ctx, handleId, DragStart);
5012
+ addComponent6(ctx, handleId, ScaleWithZoom);
4877
5013
  }
4878
5014
  const block = Block.write(ctx, handleId);
4879
5015
  block.tag = def.tag;
@@ -4905,9 +5041,9 @@ function addTapeHandles(ctx, transformBoxId, left, top, width, height, rotateZ,
4905
5041
  }
4906
5042
  }
4907
5043
  function hideTransformBox(ctx, transformBoxId) {
4908
- if (!hasComponent12(ctx, transformBoxId, TransformBox)) return;
4909
- if (!hasComponent12(ctx, transformBoxId, Opacity)) {
4910
- addComponent5(ctx, transformBoxId, Opacity, { value: 0 });
5044
+ if (!hasComponent13(ctx, transformBoxId, TransformBox)) return;
5045
+ if (!hasComponent13(ctx, transformBoxId, Opacity)) {
5046
+ addComponent6(ctx, transformBoxId, Opacity, { value: 0 });
4911
5047
  } else {
4912
5048
  const opacity = Opacity.write(ctx, transformBoxId);
4913
5049
  opacity.value = 0;
@@ -4915,8 +5051,8 @@ function hideTransformBox(ctx, transformBoxId) {
4915
5051
  const handles = getBackrefs2(ctx, transformBoxId, TransformHandle, "transformBox");
4916
5052
  for (const handleId of handles) {
4917
5053
  if (!isAlive(ctx, handleId)) continue;
4918
- if (!hasComponent12(ctx, handleId, Opacity)) {
4919
- addComponent5(ctx, handleId, Opacity, { value: 0 });
5054
+ if (!hasComponent13(ctx, handleId, Opacity)) {
5055
+ addComponent6(ctx, handleId, Opacity, { value: 0 });
4920
5056
  } else {
4921
5057
  const opacity = Opacity.write(ctx, handleId);
4922
5058
  opacity.value = 0;
@@ -4924,40 +5060,40 @@ function hideTransformBox(ctx, transformBoxId) {
4924
5060
  }
4925
5061
  }
4926
5062
  function showTransformBox(ctx, transformBoxId) {
4927
- if (!hasComponent12(ctx, transformBoxId, TransformBox)) return;
5063
+ if (!hasComponent13(ctx, transformBoxId, TransformBox)) return;
4928
5064
  const selectedBlocks = selectedBlocksQuery2.current(ctx);
4929
5065
  if (selectedBlocks.length === 0) return;
4930
5066
  if (selectedBlocks.length === 1) {
4931
5067
  if (getBlockResizeMode(ctx, selectedBlocks[0]) === ResizeMode.GroupOnly) return;
4932
5068
  }
4933
- if (hasComponent12(ctx, transformBoxId, Opacity)) {
5069
+ if (hasComponent13(ctx, transformBoxId, Opacity)) {
4934
5070
  removeComponent3(ctx, transformBoxId, Opacity);
4935
5071
  }
4936
5072
  const handles = getBackrefs2(ctx, transformBoxId, TransformHandle, "transformBox");
4937
5073
  for (const handleId of handles) {
4938
5074
  if (!isAlive(ctx, handleId)) continue;
4939
- if (hasComponent12(ctx, handleId, Opacity)) {
5075
+ if (hasComponent13(ctx, handleId, Opacity)) {
4940
5076
  removeComponent3(ctx, handleId, Opacity);
4941
5077
  }
4942
5078
  }
4943
5079
  updateTransformBox(ctx, transformBoxId);
4944
5080
  }
4945
5081
  function startTransformBoxEdit(ctx, transformBoxId) {
4946
- if (hasComponent12(ctx, transformBoxId, TransformBox)) {
5082
+ if (hasComponent13(ctx, transformBoxId, TransformBox)) {
4947
5083
  const handles = getBackrefs2(ctx, transformBoxId, TransformHandle, "transformBox");
4948
5084
  for (const handleId of handles) {
4949
5085
  removeEntity4(ctx, handleId);
4950
5086
  }
4951
5087
  }
4952
5088
  for (const blockId of selectedBlocksQuery2.current(ctx)) {
4953
- if (!hasComponent12(ctx, blockId, Edited)) {
4954
- addComponent5(ctx, blockId, Edited, {});
5089
+ if (!hasComponent13(ctx, blockId, Edited)) {
5090
+ addComponent6(ctx, blockId, Edited, {});
4955
5091
  }
4956
5092
  }
4957
5093
  }
4958
5094
  function endTransformBoxEdit(ctx) {
4959
5095
  for (const blockId of editedBlocksQuery.current(ctx)) {
4960
- if (hasComponent12(ctx, blockId, Edited)) {
5096
+ if (hasComponent13(ctx, blockId, Edited)) {
4961
5097
  removeComponent3(ctx, blockId, Edited);
4962
5098
  }
4963
5099
  }
@@ -4965,11 +5101,11 @@ function endTransformBoxEdit(ctx) {
4965
5101
 
4966
5102
  // src/systems/preCapture/intersectSystem.ts
4967
5103
  import {
4968
- addComponent as addComponent6,
5104
+ addComponent as addComponent7,
4969
5105
  defineQuery as defineQuery13,
4970
5106
  getBackrefs as getBackrefs3,
4971
5107
  getResources as getResources12,
4972
- hasComponent as hasComponent13,
5108
+ hasComponent as hasComponent14,
4973
5109
  removeComponent as removeComponent4
4974
5110
  } from "@woven-ecs/core";
4975
5111
  var blocksChanged = defineQuery13((q) => q.tracking(Block));
@@ -4979,7 +5115,7 @@ var hoveredQuery = defineQuery13((q) => q.with(Hovered));
4979
5115
  var pointerQuery3 = defineQuery13((q) => q.with(Pointer));
4980
5116
  function clearHovered(ctx) {
4981
5117
  for (const entityId of hoveredQuery.current(ctx)) {
4982
- if (hasComponent13(ctx, entityId, Hovered)) {
5118
+ if (hasComponent14(ctx, entityId, Hovered)) {
4983
5119
  removeComponent4(ctx, entityId, Hovered);
4984
5120
  }
4985
5121
  }
@@ -4987,7 +5123,7 @@ function clearHovered(ctx) {
4987
5123
  function findValidHoverTarget(ctx, intersected) {
4988
5124
  for (const entityId of intersected) {
4989
5125
  if (isHeldByRemote(ctx, entityId)) return void 0;
4990
- if (hasComponent13(ctx, entityId, Block)) {
5126
+ if (hasComponent14(ctx, entityId, Block)) {
4991
5127
  const block = Block.read(ctx, entityId);
4992
5128
  if (!canBlockInteract(ctx, block.tag)) continue;
4993
5129
  }
@@ -5002,7 +5138,7 @@ function updateHovered(ctx, intersected) {
5002
5138
  if (!selectToolActive) return;
5003
5139
  const newHoveredId = findValidHoverTarget(ctx, intersected);
5004
5140
  if (newHoveredId !== void 0) {
5005
- addComponent6(ctx, newHoveredId, Hovered, {});
5141
+ addComponent7(ctx, newHoveredId, Hovered, {});
5006
5142
  }
5007
5143
  }
5008
5144
  function arraysEqual(a, b) {
@@ -5041,14 +5177,14 @@ var intersectSystem = defineEditorSystem({ phase: "capture", priority: 100 }, (c
5041
5177
  addDescendants(ctx, entityId, changed);
5042
5178
  }
5043
5179
  for (const entityId of added) {
5044
- if (!hasComponent13(ctx, entityId, Aabb)) {
5045
- addComponent6(ctx, entityId, Aabb, { value: [0, 0, 0, 0] });
5180
+ if (!hasComponent14(ctx, entityId, Aabb)) {
5181
+ addComponent7(ctx, entityId, Aabb, { value: [0, 0, 0, 0] });
5046
5182
  }
5047
5183
  const aabb = Aabb.write(ctx, entityId);
5048
5184
  computeAabb(ctx, entityId, aabb.value);
5049
5185
  }
5050
5186
  for (const entityId of changed) {
5051
- if (!hasComponent13(ctx, entityId, Aabb)) continue;
5187
+ if (!hasComponent14(ctx, entityId, Aabb)) continue;
5052
5188
  const aabb = Aabb.write(ctx, entityId);
5053
5189
  computeAabb(ctx, entityId, aabb.value);
5054
5190
  }
@@ -5097,12 +5233,21 @@ var intersectSystem = defineEditorSystem({ phase: "capture", priority: 100 }, (c
5097
5233
 
5098
5234
  // src/systems/preCapture/selectSystem.ts
5099
5235
  import { Vec2 as Vec2Ns } from "@woven-canvas/math";
5100
- import { Synced as Synced3 } from "@woven-ecs/canvas-store";
5101
- import { defineQuery as defineQuery14, hasComponent as hasComponent14 } from "@woven-ecs/core";
5236
+ import { Synced as Synced4 } from "@woven-ecs/canvas-store";
5237
+ import { defineQuery as defineQuery14, hasComponent as hasComponent15 } from "@woven-ecs/core";
5102
5238
  import { and as and2, assign as assign4, not as not3, setup as setup4 } from "xstate";
5103
5239
  var POINTING_THRESHOLD = 4;
5104
- var selectedBlocksQuery3 = defineQuery14((q) => q.with(Block, Selected, Synced3));
5240
+ var selectedBlocksQuery3 = defineQuery14((q) => q.with(Block, Selected, Synced4));
5105
5241
  var editedBlocksQuery2 = defineQuery14((q) => q.with(Block, Edited));
5242
+ function isDragSnappable(ctx, draggedEntity) {
5243
+ if (hasComponent15(ctx, draggedEntity, TransformBox)) {
5244
+ for (const blockId of selectedBlocksQuery3.current(ctx)) {
5245
+ if (canBlockSnap(ctx, Block.read(ctx, blockId).tag)) return true;
5246
+ }
5247
+ return false;
5248
+ }
5249
+ return canBlockSnap(ctx, Block.read(ctx, draggedEntity).tag);
5250
+ }
5106
5251
  var selectionMachine = setup4({
5107
5252
  types: {
5108
5253
  context: {},
@@ -5118,13 +5263,13 @@ var selectionMachine = setup4({
5118
5263
  },
5119
5264
  hasDraggedEntity: ({ context, event }) => {
5120
5265
  if (context.draggedEntity === null) return false;
5121
- return hasComponent14(event.ctx, context.draggedEntity, Synced3);
5266
+ return hasComponent15(event.ctx, context.draggedEntity, Synced4);
5122
5267
  },
5123
5268
  isOverSyncedBlock: ({ event }) => {
5124
5269
  const ctx = event.ctx;
5125
5270
  for (const entityId of event.intersects) {
5126
- if (hasComponent14(ctx, entityId, Synced3)) {
5127
- if (hasComponent14(ctx, entityId, Block)) {
5271
+ if (hasComponent15(ctx, entityId, Synced4)) {
5272
+ if (hasComponent15(ctx, entityId, Block)) {
5128
5273
  const block = Block.read(ctx, entityId);
5129
5274
  if (!canBlockInteract(ctx, block.tag)) continue;
5130
5275
  }
@@ -5161,7 +5306,7 @@ var selectionMachine = setup4({
5161
5306
  },
5162
5307
  setDragCursor: ({ event, context }) => {
5163
5308
  if (!context.draggedEntity) return;
5164
- if (hasComponent14(event.ctx, context.draggedEntity, TransformHandle)) {
5309
+ if (hasComponent15(event.ctx, context.draggedEntity, TransformHandle)) {
5165
5310
  const handle = TransformHandle.read(event.ctx, context.draggedEntity);
5166
5311
  const transformBoxBlock = handle.transformBox !== null ? Block.read(event.ctx, handle.transformBox) : void 0;
5167
5312
  SetCursor.spawn(event.ctx, {
@@ -5221,7 +5366,7 @@ var selectionMachine = setup4({
5221
5366
  if (context.draggedEntity === null) return false;
5222
5367
  let left = context.draggedEntityStart[0] + event.worldPosition[0] - context.dragStart[0];
5223
5368
  let top = context.draggedEntityStart[1] + event.worldPosition[1] - context.dragStart[1];
5224
- const draggingSynced = hasComponent14(ctx, context.draggedEntity, Synced3) || hasComponent14(ctx, context.draggedEntity, TransformBox);
5369
+ const draggingSynced = hasComponent15(ctx, context.draggedEntity, Synced4) || hasComponent15(ctx, context.draggedEntity, TransformBox);
5225
5370
  if (event.shiftDown && draggingSynced) {
5226
5371
  const dx = Math.abs(event.worldPosition[0] - context.dragStart[0]);
5227
5372
  const dy = Math.abs(event.worldPosition[1] - context.dragStart[1]);
@@ -5231,7 +5376,7 @@ var selectionMachine = setup4({
5231
5376
  left = context.draggedEntityStart[0];
5232
5377
  }
5233
5378
  }
5234
- if (draggingSynced) {
5379
+ if (draggingSynced && isDragSnappable(ctx, context.draggedEntity)) {
5235
5380
  left = Grid.snapX(ctx, left);
5236
5381
  top = Grid.snapY(ctx, top);
5237
5382
  }
@@ -5244,7 +5389,7 @@ var selectionMachine = setup4({
5244
5389
  const dx = context.draggedEntityStart[0] - left;
5245
5390
  const dy = context.draggedEntityStart[1] - top;
5246
5391
  isCloning = true;
5247
- const isTransformBox = hasComponent14(ctx, context.draggedEntity, TransformBox);
5392
+ const isTransformBox = hasComponent15(ctx, context.draggedEntity, TransformBox);
5248
5393
  const entityIds = isTransformBox ? Array.from(selectedBlocksQuery3.current(ctx)) : [context.draggedEntity];
5249
5394
  CloneEntities.spawn(ctx, {
5250
5395
  entityIds,
@@ -5252,7 +5397,7 @@ var selectionMachine = setup4({
5252
5397
  seed: context.cloneGeneratorSeed
5253
5398
  });
5254
5399
  } else if (!event.altDown && context.isCloning) {
5255
- const isTransformBox = hasComponent14(ctx, context.draggedEntity, TransformBox);
5400
+ const isTransformBox = hasComponent15(ctx, context.draggedEntity, TransformBox);
5256
5401
  const entityIds = isTransformBox ? Array.from(selectedBlocksQuery3.current(ctx)) : [context.draggedEntity];
5257
5402
  UncloneEntities.spawn(ctx, {
5258
5403
  entityIds,
@@ -5277,8 +5422,8 @@ var selectionMachine = setup4({
5277
5422
  const ctx = event.ctx;
5278
5423
  let intersectId;
5279
5424
  for (const entityId of event.intersects) {
5280
- if (hasComponent14(ctx, entityId, Synced3)) {
5281
- if (hasComponent14(ctx, entityId, Block)) {
5425
+ if (hasComponent15(ctx, entityId, Synced4)) {
5426
+ if (hasComponent15(ctx, entityId, Block)) {
5282
5427
  const block = Block.read(ctx, entityId);
5283
5428
  if (!canBlockInteract(ctx, block.tag)) continue;
5284
5429
  }
@@ -5298,7 +5443,7 @@ var selectionMachine = setup4({
5298
5443
  },
5299
5444
  selectDragged: ({ context, event }) => {
5300
5445
  if (context.draggedEntity === null) return;
5301
- if (!hasComponent14(event.ctx, context.draggedEntity, Synced3)) return;
5446
+ if (!hasComponent15(event.ctx, context.draggedEntity, Synced4)) return;
5302
5447
  SelectBlock.spawn(event.ctx, {
5303
5448
  entityId: context.draggedEntity,
5304
5449
  deselectOthers: true
@@ -5310,9 +5455,9 @@ var selectionMachine = setup4({
5310
5455
  },
5311
5456
  deselectAllIfDraggedNotSelected: ({ context, event }) => {
5312
5457
  if (context.draggedEntity === null) return;
5313
- if (hasComponent14(event.ctx, context.draggedEntity, Selected)) return;
5314
- if (hasComponent14(event.ctx, context.draggedEntity, TransformBox)) return;
5315
- if (hasComponent14(event.ctx, context.draggedEntity, TransformHandle)) return;
5458
+ if (hasComponent15(event.ctx, context.draggedEntity, Selected)) return;
5459
+ if (hasComponent15(event.ctx, context.draggedEntity, TransformBox)) return;
5460
+ if (hasComponent15(event.ctx, context.draggedEntity, TransformHandle)) return;
5316
5461
  DeselectAll.spawn(event.ctx, void 0);
5317
5462
  }
5318
5463
  }
@@ -5436,9 +5581,9 @@ var selectSystem = defineEditorSystem({ phase: "capture", priority: 100 }, (ctx)
5436
5581
  });
5437
5582
 
5438
5583
  // src/systems/preInput/rankBoundsSystem.ts
5439
- import { Synced as Synced4 } from "@woven-ecs/canvas-store";
5584
+ import { Synced as Synced5 } from "@woven-ecs/canvas-store";
5440
5585
  import { defineQuery as defineQuery15 } from "@woven-ecs/core";
5441
- var blocksQuery = defineQuery15((q) => q.with(Synced4, Block));
5586
+ var blocksQuery = defineQuery15((q) => q.with(Synced5, Block));
5442
5587
  var rankBoundsSystem = defineEditorSystem({ phase: "input", priority: 100 }, (ctx) => {
5443
5588
  const added2 = blocksQuery.added(ctx);
5444
5589
  for (const entityId of added2) {
@@ -5455,11 +5600,11 @@ var rankBoundsSystem = defineEditorSystem({ phase: "input", priority: 100 }, (ct
5455
5600
 
5456
5601
  // src/systems/preRender/canSeeBlocksSystem.ts
5457
5602
  import { Aabb as AabbMath2 } from "@woven-canvas/math";
5458
- import { Synced as Synced5 } from "@woven-ecs/canvas-store";
5459
- import { defineQuery as defineQuery16, hasComponent as hasComponent15 } from "@woven-ecs/core";
5603
+ import { Synced as Synced6 } from "@woven-ecs/canvas-store";
5604
+ import { defineQuery as defineQuery16, hasComponent as hasComponent16 } from "@woven-ecs/core";
5460
5605
  var camerasQuery = defineQuery16((q) => q.tracking(Camera));
5461
- var blocksQuery2 = defineQuery16((q) => q.with(Synced5, Block, Aabb));
5462
- var syncedBlocksQuery = defineQuery16((q) => q.with(Synced5, Block, Aabb));
5606
+ var blocksQuery2 = defineQuery16((q) => q.with(Synced6, Block, Aabb));
5607
+ var syncedBlocksQuery = defineQuery16((q) => q.with(Synced6, Block, Aabb));
5463
5608
  var _cameraAabb = [0, 0, 0, 0];
5464
5609
  var canSeeBlocksSystem = defineEditorSystem({ phase: "render", priority: 90 }, (ctx) => {
5465
5610
  if (camerasQuery.changed(ctx).length === 0 && syncedBlocksQuery.addedOrRemoved(ctx).length === 0) {
@@ -5468,7 +5613,7 @@ var canSeeBlocksSystem = defineEditorSystem({ phase: "render", priority: 90 }, (
5468
5613
  const camera = Camera.read(ctx);
5469
5614
  const cameraAabb = Camera.getAabb(ctx, _cameraAabb);
5470
5615
  if (camera.canSeeBlocks && camera.lastSeenBlock !== null) {
5471
- if (hasComponent15(ctx, camera.lastSeenBlock, Aabb)) {
5616
+ if (hasComponent16(ctx, camera.lastSeenBlock, Aabb)) {
5472
5617
  const aabb = Aabb.read(ctx, camera.lastSeenBlock);
5473
5618
  if (AabbMath2.intersects(cameraAabb, aabb.value)) {
5474
5619
  return;
@@ -5542,23 +5687,23 @@ function scaleBlock(ctx, entityId, zoom) {
5542
5687
 
5543
5688
  // src/systems/update/blockSystem.ts
5544
5689
  import { Vec2 as Vec25 } from "@woven-canvas/math";
5545
- import { Synced as Synced6 } from "@woven-ecs/canvas-store";
5690
+ import { Synced as Synced7 } from "@woven-ecs/canvas-store";
5546
5691
  import {
5547
- addComponent as addComponent7,
5548
- createEntity as createEntity6,
5692
+ addComponent as addComponent8,
5693
+ createEntity as createEntity7,
5549
5694
  defineQuery as defineQuery18,
5550
5695
  getBackrefs as getBackrefs4,
5551
5696
  getResources as getResources13,
5552
- hasComponent as hasComponent16,
5697
+ hasComponent as hasComponent17,
5553
5698
  isAlive as isAlive2,
5554
5699
  removeComponent as removeComponent5,
5555
5700
  removeEntity as removeEntity5
5556
5701
  } from "@woven-ecs/core";
5557
5702
  var selectedBlocksQuery4 = defineQuery18((q) => q.with(Block, Selected));
5558
- var syncedBlocksQuery2 = defineQuery18((q) => q.with(Block, Synced6));
5703
+ var syncedBlocksQuery2 = defineQuery18((q) => q.with(Block, Synced7));
5559
5704
  var blockSystem = defineEditorSystem({ phase: "update" }, (ctx) => {
5560
5705
  on(ctx, DragBlock, (ctx2, { entityId, position }) => {
5561
- if (!hasComponent16(ctx2, entityId, Block)) return;
5706
+ if (!hasComponent17(ctx2, entityId, Block)) return;
5562
5707
  Block.setWorldPosition(ctx2, entityId, position);
5563
5708
  });
5564
5709
  on(ctx, SelectBlock, (ctx2, { entityId, deselectOthers }) => {
@@ -5571,7 +5716,7 @@ var blockSystem = defineEditorSystem({ phase: "update" }, (ctx) => {
5571
5716
  deselectBlock(ctx2, entityId);
5572
5717
  });
5573
5718
  on(ctx, ToggleSelect, (ctx2, { entityId }) => {
5574
- if (hasComponent16(ctx2, entityId, Selected)) {
5719
+ if (hasComponent17(ctx2, entityId, Selected)) {
5575
5720
  deselectBlock(ctx2, entityId);
5576
5721
  } else {
5577
5722
  selectBlock(ctx2, entityId);
@@ -5579,14 +5724,14 @@ var blockSystem = defineEditorSystem({ phase: "update" }, (ctx) => {
5579
5724
  });
5580
5725
  on(ctx, DeselectAll, deselectAllBlocks);
5581
5726
  on(ctx, AddHeld, (ctx2, { entityId }) => {
5582
- if (!hasComponent16(ctx2, entityId, Synced6)) return;
5583
- if (hasComponent16(ctx2, entityId, Held)) return;
5727
+ if (!hasComponent17(ctx2, entityId, Synced7)) return;
5728
+ if (hasComponent17(ctx2, entityId, Held)) return;
5584
5729
  const { sessionId } = getResources13(ctx2);
5585
- addComponent7(ctx2, entityId, Held, { sessionId });
5730
+ addComponent8(ctx2, entityId, Held, { sessionId });
5586
5731
  });
5587
5732
  on(ctx, RemoveHeld, (ctx2, { entityId }) => {
5588
- if (hasComponent16(ctx2, entityId, Selected)) return;
5589
- if (!hasComponent16(ctx2, entityId, Held)) return;
5733
+ if (hasComponent17(ctx2, entityId, Selected)) return;
5734
+ if (!hasComponent17(ctx2, entityId, Held)) return;
5590
5735
  removeComponent5(ctx2, entityId, Held);
5591
5736
  });
5592
5737
  on(ctx, SelectAll, (ctx2) => {
@@ -5676,10 +5821,10 @@ function cloneEntities(ctx, entityIds, offset, seed) {
5676
5821
  entityIds = [...allEntityIdsSet];
5677
5822
  const { componentsById } = getResources13(ctx);
5678
5823
  const documentComponents = new Map([...componentsById].filter(([, def]) => def.sync === "document"));
5679
- const syncedComponentId = Synced6._getComponentId(ctx);
5824
+ const syncedComponentId = Synced7._getComponentId(ctx);
5680
5825
  const blockComponentId = Block._getComponentId(ctx);
5681
5826
  const entityIdSet = new Set(entityIds);
5682
- const sortedEntities = entityIds.filter((id) => hasComponent16(ctx, id, Block)).sort((a, b) => {
5827
+ const sortedEntities = entityIds.filter((id) => hasComponent17(ctx, id, Block)).sort((a, b) => {
5683
5828
  const rankA = Block.read(ctx, a).rank;
5684
5829
  const rankB = Block.read(ctx, b).rank;
5685
5830
  return rankA < rankB ? -1 : rankA > rankB ? 1 : 0;
@@ -5703,12 +5848,12 @@ function cloneEntities(ctx, entityIds, offset, seed) {
5703
5848
  }
5704
5849
  const originalToClone = /* @__PURE__ */ new Map();
5705
5850
  for (const entityId of entityIds) {
5706
- if (!hasComponent16(ctx, entityId, Synced6)) continue;
5707
- const synced = Synced6.read(ctx, entityId);
5851
+ if (!hasComponent17(ctx, entityId, Synced7)) continue;
5852
+ const synced = Synced7.read(ctx, entityId);
5708
5853
  const cloneId = generateUuidBySeed(synced.id, seed);
5709
- const cloneEntityId = createEntity6(ctx);
5854
+ const cloneEntityId = createEntity7(ctx);
5710
5855
  originalToClone.set(entityId, cloneEntityId);
5711
- addComponent7(ctx, cloneEntityId, Synced6, { id: cloneId });
5856
+ addComponent8(ctx, cloneEntityId, Synced7, { id: cloneId });
5712
5857
  for (const componentId of ctx.entityBuffer.getComponentIds(entityId)) {
5713
5858
  if (componentId === syncedComponentId) continue;
5714
5859
  const componentDef = documentComponents.get(componentId);
@@ -5724,7 +5869,7 @@ function cloneEntities(ctx, entityIds, offset, seed) {
5724
5869
  }
5725
5870
  data.rank = cloneRankMap.get(entityId) ?? RankBounds.genBetween(prevRank, lowestOriginalRank);
5726
5871
  }
5727
- addComponent7(ctx, cloneEntityId, componentDef, data);
5872
+ addComponent8(ctx, cloneEntityId, componentDef, data);
5728
5873
  }
5729
5874
  }
5730
5875
  swapConnectorRefs(ctx, originalToClone);
@@ -5740,23 +5885,23 @@ function uncloneEntities(ctx, entityIds, seed) {
5740
5885
  entityIds = [...allEntityIdsSet];
5741
5886
  const expectedCloneUuids = /* @__PURE__ */ new Set();
5742
5887
  for (const entityId of entityIds) {
5743
- if (!hasComponent16(ctx, entityId, Synced6)) continue;
5744
- const synced = Synced6.read(ctx, entityId);
5888
+ if (!hasComponent17(ctx, entityId, Synced7)) continue;
5889
+ const synced = Synced7.read(ctx, entityId);
5745
5890
  expectedCloneUuids.add(generateUuidBySeed(synced.id, seed));
5746
5891
  }
5747
5892
  const clonesToRemove = [];
5748
5893
  for (const entityId of syncedBlocksQuery2.current(ctx)) {
5749
- const synced = Synced6.read(ctx, entityId);
5894
+ const synced = Synced7.read(ctx, entityId);
5750
5895
  if (expectedCloneUuids.has(synced.id)) {
5751
5896
  clonesToRemove.push(entityId);
5752
5897
  }
5753
5898
  }
5754
5899
  const cloneToOriginal = /* @__PURE__ */ new Map();
5755
5900
  for (const cloneId of clonesToRemove) {
5756
- const cloneUuid = Synced6.read(ctx, cloneId).id;
5901
+ const cloneUuid = Synced7.read(ctx, cloneId).id;
5757
5902
  for (const originalId of entityIds) {
5758
- if (!hasComponent16(ctx, originalId, Synced6)) continue;
5759
- const originalUuid = Synced6.read(ctx, originalId).id;
5903
+ if (!hasComponent17(ctx, originalId, Synced7)) continue;
5904
+ const originalUuid = Synced7.read(ctx, originalId).id;
5760
5905
  if (generateUuidBySeed(originalUuid, seed) === cloneUuid) {
5761
5906
  cloneToOriginal.set(cloneId, originalId);
5762
5907
  break;
@@ -5783,7 +5928,7 @@ function swapConnectorRefs(ctx, fromTo) {
5783
5928
  function swapParentRefs(ctx, fromTo) {
5784
5929
  for (const [, cloneId] of fromTo) {
5785
5930
  if (!isAlive2(ctx, cloneId)) continue;
5786
- if (!hasComponent16(ctx, cloneId, Block)) continue;
5931
+ if (!hasComponent17(ctx, cloneId, Block)) continue;
5787
5932
  const block = Block.read(ctx, cloneId);
5788
5933
  if (block.parentId !== null && fromTo.has(block.parentId)) {
5789
5934
  Block.write(ctx, cloneId).parentId = fromTo.get(block.parentId);
@@ -5793,20 +5938,27 @@ function swapParentRefs(ctx, fromTo) {
5793
5938
 
5794
5939
  // src/systems/update/dragHandlerSystem.ts
5795
5940
  import { Scalar as Scalar2, Vec2 as Vec26 } from "@woven-canvas/math";
5796
- import { defineQuery as defineQuery19, hasComponent as hasComponent17 } from "@woven-ecs/core";
5941
+ import { defineQuery as defineQuery19, hasComponent as hasComponent18 } from "@woven-ecs/core";
5797
5942
  var selectedBlocksQuery5 = defineQuery19((q) => q.with(Block, Selected));
5798
5943
  var transformBoxQuery2 = defineQuery19((q) => q.with(Block, TransformBox, DragStart));
5799
5944
  function isParentSelected(ctx, blockId) {
5800
5945
  const block = Block.read(ctx, blockId);
5801
- return block.parentId !== null && hasComponent17(ctx, block.parentId, Selected);
5946
+ return block.parentId !== null && hasComponent18(ctx, block.parentId, Selected);
5947
+ }
5948
+ function isSelectionSnappable(ctx) {
5949
+ for (const blockId of selectedBlocksQuery5.current(ctx)) {
5950
+ if (isParentSelected(ctx, blockId)) continue;
5951
+ if (canBlockSnap(ctx, Block.read(ctx, blockId).tag)) return true;
5952
+ }
5953
+ return false;
5802
5954
  }
5803
5955
  var dragHandlerSystem = defineEditorSystem({ phase: "update" }, (ctx) => {
5804
5956
  on(ctx, DragBlock, (ctx2, { entityId, position }) => {
5805
- if (hasComponent17(ctx2, entityId, TransformBox)) {
5957
+ if (hasComponent18(ctx2, entityId, TransformBox)) {
5806
5958
  onTransformBoxDrag(ctx2, entityId, position);
5807
5959
  return;
5808
5960
  }
5809
- if (hasComponent17(ctx2, entityId, TransformHandle)) {
5961
+ if (hasComponent18(ctx2, entityId, TransformHandle)) {
5810
5962
  onTransformHandleDrag(ctx2, entityId, position);
5811
5963
  return;
5812
5964
  }
@@ -5817,11 +5969,13 @@ function onTransformBoxDrag(ctx, boxId, position) {
5817
5969
  const dx = position[0] - boxStart.position[0];
5818
5970
  const dy = position[1] - boxStart.position[1];
5819
5971
  for (const blockId of selectedBlocksQuery5.current(ctx)) {
5820
- if (!hasComponent17(ctx, blockId, DragStart)) continue;
5972
+ if (!hasComponent18(ctx, blockId, DragStart)) continue;
5821
5973
  if (isParentSelected(ctx, blockId)) continue;
5822
5974
  const blockStart = DragStart.read(ctx, blockId);
5823
5975
  const worldPos = [blockStart.position[0] + dx, blockStart.position[1] + dy];
5824
- Grid.snapPosition(ctx, worldPos);
5976
+ if (canBlockSnap(ctx, Block.read(ctx, blockId).tag)) {
5977
+ Grid.snapPosition(ctx, worldPos);
5978
+ }
5825
5979
  Block.setWorldPosition(ctx, blockId, worldPos);
5826
5980
  }
5827
5981
  }
@@ -5855,10 +6009,11 @@ function onRotateHandleDrag(ctx, handleId, position) {
5855
6009
  let delta = angleHandle - handleStartAngle;
5856
6010
  const grid = Grid.read(ctx);
5857
6011
  const keyboard = Keyboard.read(ctx);
6012
+ const selectionSnappable = isSelectionSnappable(ctx);
5858
6013
  let snapAngle = 0;
5859
6014
  if (keyboard.shiftDown) {
5860
6015
  snapAngle = grid.shiftSnapAngleRad;
5861
- } else if (grid.enabled) {
6016
+ } else if (grid.enabled && selectionSnappable) {
5862
6017
  snapAngle = grid.snapAngleRad;
5863
6018
  }
5864
6019
  if (snapAngle > 0) {
@@ -5867,7 +6022,7 @@ function onRotateHandleDrag(ctx, handleId, position) {
5867
6022
  }
5868
6023
  boxBlock.rotateZ = Scalar2.normalizeAngle(dragStart.rotateZ + delta);
5869
6024
  for (const blockId of selectedBlocksQuery5.current(ctx)) {
5870
- if (!hasComponent17(ctx, blockId, DragStart)) continue;
6025
+ if (!hasComponent18(ctx, blockId, DragStart)) continue;
5871
6026
  if (isParentSelected(ctx, blockId)) continue;
5872
6027
  const blockStart = DragStart.read(ctx, blockId);
5873
6028
  const block = Block.write(ctx, blockId);
@@ -5880,7 +6035,7 @@ function onRotateHandleDrag(ctx, handleId, position) {
5880
6035
  const angle = Vec26.angleTo(boxCenter, startCenter) + delta;
5881
6036
  const newCenter = Vec26.fromPolar(r, angle, boxCenter);
5882
6037
  const worldPos = [newCenter[0] - block.size[0] / 2, newCenter[1] - block.size[1] / 2];
5883
- if (grid.strict) {
6038
+ if (grid.strict && canBlockSnap(ctx, block.tag)) {
5884
6039
  Grid.snapPosition(ctx, worldPos);
5885
6040
  }
5886
6041
  Block.setWorldPosition(ctx, blockId, worldPos);
@@ -5897,7 +6052,10 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5897
6052
  const handle = TransformHandle.read(ctx, handleId);
5898
6053
  const handleBlock = Block.read(ctx, handleId);
5899
6054
  const handleCenter = [position[0] + handleBlock.size[0] / 2, position[1] + handleBlock.size[1] / 2];
5900
- Grid.snapPosition(ctx, handleCenter);
6055
+ const selectionSnappable = isSelectionSnappable(ctx);
6056
+ if (selectionSnappable) {
6057
+ Grid.snapPosition(ctx, handleCenter);
6058
+ }
5901
6059
  const boxRotateZ = boxStart.rotateZ;
5902
6060
  const boxStartCenter = [
5903
6061
  boxStart.position[0] + boxStart.size[0] / 2,
@@ -5948,7 +6106,7 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5948
6106
  Vec26.add(newBoxCenter, vec);
5949
6107
  const scaleFactor = [scaleX, scaleY];
5950
6108
  for (const blockId of selectedBlocksQuery5.current(ctx)) {
5951
- if (!hasComponent17(ctx, blockId, DragStart)) continue;
6109
+ if (!hasComponent18(ctx, blockId, DragStart)) continue;
5952
6110
  if (isParentSelected(ctx, blockId)) continue;
5953
6111
  const blockStart = DragStart.read(ctx, blockId);
5954
6112
  const block = Block.write(ctx, blockId);
@@ -5966,7 +6124,7 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5966
6124
  const newBlockSize = Vec26.clone(blockStart.size);
5967
6125
  Vec26.multiply(newBlockSize, scaleFactor);
5968
6126
  const isTextResize = getBlockResizeMode(ctx, blockId) === ResizeMode.Text;
5969
- if (grid.strict) {
6127
+ if (grid.strict && canBlockSnap(ctx, block.tag)) {
5970
6128
  if (isTextResize && maintainAspectRatio) {
5971
6129
  newBlockSize[1] = Math.max(grid.rowHeight, Grid.snapY(ctx, newBlockSize[1]));
5972
6130
  const aspectRatio = blockStart.size[0] / blockStart.size[1];
@@ -5977,7 +6135,7 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5977
6135
  }
5978
6136
  const worldPos = Vec26.clone(newBlockCenter);
5979
6137
  Vec26.sub(worldPos, [newBlockSize[0] / 2, newBlockSize[1] / 2]);
5980
- if (grid.strict) {
6138
+ if (grid.strict && canBlockSnap(ctx, block.tag)) {
5981
6139
  Grid.snapPosition(ctx, worldPos);
5982
6140
  }
5983
6141
  Block.setWorldPosition(ctx, blockId, worldPos);
@@ -5988,7 +6146,7 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5988
6146
  } else {
5989
6147
  Vec26.copy(block.size, newBlockSize);
5990
6148
  }
5991
- if (hasComponent17(ctx, blockId, Text)) {
6149
+ if (hasComponent18(ctx, blockId, Text)) {
5992
6150
  const didStretch = handle.kind === TransformHandleKind.Stretch;
5993
6151
  const text = Text.write(ctx, blockId);
5994
6152
  if (didStretch) {
@@ -6026,10 +6184,11 @@ function onRotateScaleHandleDrag(ctx, handleId, position) {
6026
6184
  }
6027
6185
  const grid = Grid.read(ctx);
6028
6186
  const keyboard = Keyboard.read(ctx);
6187
+ const selectionSnappable = isSelectionSnappable(ctx);
6029
6188
  let snapAngle = 0;
6030
6189
  if (keyboard.shiftDown) {
6031
6190
  snapAngle = grid.shiftSnapAngleRad;
6032
- } else if (grid.enabled) {
6191
+ } else if (grid.enabled && selectionSnappable) {
6033
6192
  snapAngle = grid.snapAngleRad;
6034
6193
  }
6035
6194
  if (snapAngle > 0) {
@@ -6049,13 +6208,13 @@ function onRotateScaleHandleDrag(ctx, handleId, position) {
6049
6208
  boxBlock.size = [finalLength, startHeight];
6050
6209
  boxBlock.position = [newCenter[0] - finalLength / 2, newCenter[1] - startHeight / 2];
6051
6210
  for (const blockId of selectedBlocksQuery5.current(ctx)) {
6052
- if (!hasComponent17(ctx, blockId, DragStart)) continue;
6211
+ if (!hasComponent18(ctx, blockId, DragStart)) continue;
6053
6212
  if (isParentSelected(ctx, blockId)) continue;
6054
6213
  const block = Block.write(ctx, blockId);
6055
6214
  block.rotateZ = newRotateZ;
6056
6215
  block.size = [finalLength, startHeight];
6057
6216
  const blockWorldPos = [newCenter[0] - finalLength / 2, newCenter[1] - startHeight / 2];
6058
- if (grid.strict) {
6217
+ if (grid.strict && canBlockSnap(ctx, block.tag)) {
6059
6218
  Grid.snapPosition(ctx, blockWorldPos);
6060
6219
  }
6061
6220
  Block.setWorldPosition(ctx, blockId, blockWorldPos);
@@ -6063,23 +6222,23 @@ function onRotateScaleHandleDrag(ctx, handleId, position) {
6063
6222
  }
6064
6223
 
6065
6224
  // src/systems/update/selectSystem.ts
6066
- import { Synced as Synced7 } from "@woven-ecs/canvas-store";
6067
- import { addComponent as addComponent8, createEntity as createEntity7, defineQuery as defineQuery20, removeEntity as removeEntity6 } from "@woven-ecs/core";
6225
+ import { Synced as Synced8 } from "@woven-ecs/canvas-store";
6226
+ import { addComponent as addComponent9, createEntity as createEntity8, defineQuery as defineQuery20, removeEntity as removeEntity6 } from "@woven-ecs/core";
6068
6227
  var selectedBlocksQuery6 = defineQuery20((q) => q.with(Block, Selected));
6069
6228
  var selectionBoxQuery = defineQuery20((q) => q.with(Block, SelectionBox));
6070
- var syncedBlocksQuery3 = defineQuery20((q) => q.with(Block, Synced7, Aabb));
6229
+ var syncedBlocksQuery3 = defineQuery20((q) => q.with(Block, Synced8, Aabb));
6071
6230
  var SELECTION_BOX_RANK = "z";
6072
6231
  var selectSystem2 = defineEditorSystem({ phase: "update" }, (ctx) => {
6073
6232
  on(ctx, AddSelectionBox, (ctx2) => {
6074
- const entityId = createEntity7(ctx2);
6075
- addComponent8(ctx2, entityId, Block, {
6233
+ const entityId = createEntity8(ctx2);
6234
+ addComponent9(ctx2, entityId, Block, {
6076
6235
  tag: "selection-box",
6077
6236
  position: [0, 0],
6078
6237
  size: [0, 0],
6079
6238
  rotateZ: 0,
6080
6239
  rank: SELECTION_BOX_RANK
6081
6240
  });
6082
- addComponent8(ctx2, entityId, SelectionBox, {});
6241
+ addComponent9(ctx2, entityId, SelectionBox, {});
6083
6242
  });
6084
6243
  on(ctx, UpdateSelectionBox, (ctx2, { bounds, deselectOthers }) => {
6085
6244
  const selectionBoxes = selectionBoxQuery.current(ctx2);
@@ -6120,21 +6279,31 @@ var selectSystem2 = defineEditorSystem({ phase: "update" }, (ctx) => {
6120
6279
  });
6121
6280
 
6122
6281
  // src/systems/update/updateFrameContainmentSystem.ts
6123
- import { addComponent as addComponent9, hasComponent as hasComponent18, removeComponent as removeComponent6 } from "@woven-ecs/core";
6282
+ import { addComponent as addComponent10, hasComponent as hasComponent19, removeComponent as removeComponent6 } from "@woven-ecs/core";
6124
6283
  var updateFrameContainmentSystem = defineEditorSystem({ phase: "update", priority: -10 }, (ctx) => {
6125
6284
  on(ctx, AddFrameHighlight, (ctx2, { frameId }) => {
6126
- if (!hasComponent18(ctx2, frameId, FrameDropTarget)) {
6127
- addComponent9(ctx2, frameId, FrameDropTarget);
6285
+ if (!hasComponent19(ctx2, frameId, FrameDropTarget)) {
6286
+ addComponent10(ctx2, frameId, FrameDropTarget);
6128
6287
  }
6129
6288
  });
6289
+ on(ctx, PlaceBlockEvent, (ctx2, { entityId }) => {
6290
+ if (!hasComponent19(ctx2, entityId, Block)) return;
6291
+ if (Block.read(ctx2, entityId).parentId !== null) return;
6292
+ const center = Block.getCenter(ctx2, entityId);
6293
+ const frameId = findFrameAtPoint(ctx2, center, entityId);
6294
+ if (frameId === null) return;
6295
+ const worldPos = Block.getWorldPosition(ctx2, entityId);
6296
+ Block.write(ctx2, entityId).parentId = frameId;
6297
+ Block.setWorldPosition(ctx2, entityId, worldPos);
6298
+ });
6130
6299
  on(ctx, RemoveFrameHighlight, (ctx2, { frameId }) => {
6131
- if (hasComponent18(ctx2, frameId, FrameDropTarget)) {
6300
+ if (hasComponent19(ctx2, frameId, FrameDropTarget)) {
6132
6301
  removeComponent6(ctx2, frameId, FrameDropTarget);
6133
6302
  }
6134
6303
  });
6135
6304
  on(ctx, AssignFrameChildren, (ctx2, { assignments }) => {
6136
6305
  for (const { entityId, frameId } of assignments) {
6137
- if (!hasComponent18(ctx2, entityId, Block)) continue;
6306
+ if (!hasComponent19(ctx2, entityId, Block)) continue;
6138
6307
  const currentParentId = Block.read(ctx2, entityId).parentId;
6139
6308
  if (frameId !== null) {
6140
6309
  if (currentParentId !== frameId) {
@@ -6156,12 +6325,12 @@ var updateFrameContainmentSystem = defineEditorSystem({ phase: "update", priorit
6156
6325
  });
6157
6326
 
6158
6327
  // src/systems/update/updateFrameSetupSystem.ts
6159
- import { addComponent as addComponent10, defineQuery as defineQuery21, hasComponent as hasComponent19 } from "@woven-ecs/core";
6328
+ import { addComponent as addComponent11, defineQuery as defineQuery21, hasComponent as hasComponent20 } from "@woven-ecs/core";
6160
6329
  var frameQuery2 = defineQuery21((q) => q.tracking(Frame));
6161
6330
  var updateFrameSetupSystem = defineEditorSystem({ phase: "update", priority: 100 }, (ctx) => {
6162
6331
  for (const entityId of frameQuery2.added(ctx)) {
6163
- if (hasComponent19(ctx, entityId, HitGeometry)) continue;
6164
- addComponent10(ctx, entityId, HitGeometry);
6332
+ if (hasComponent20(ctx, entityId, HitGeometry)) continue;
6333
+ addComponent11(ctx, entityId, HitGeometry);
6165
6334
  HitGeometry.addCapsuleUv(ctx, entityId, [0, 0], [1, 0], FRAME_EDGE_THICKNESS);
6166
6335
  HitGeometry.addCapsuleUv(ctx, entityId, [1, 0], [1, 1], FRAME_EDGE_THICKNESS);
6167
6336
  HitGeometry.addCapsuleUv(ctx, entityId, [1, 1], [0, 1], FRAME_EDGE_THICKNESS);
@@ -6339,10 +6508,10 @@ function createCorePlugin(options = {}) {
6339
6508
  }
6340
6509
 
6341
6510
  // src/Editor.ts
6342
- import { Synced as Synced8 } from "@woven-ecs/canvas-store";
6511
+ import { Synced as Synced9 } from "@woven-ecs/canvas-store";
6343
6512
  import {
6344
- addComponent as addComponent11,
6345
- createEntity as createEntity8,
6513
+ addComponent as addComponent12,
6514
+ createEntity as createEntity9,
6346
6515
  World
6347
6516
  } from "@woven-ecs/core";
6348
6517
 
@@ -6374,21 +6543,44 @@ var FontFamily = z2.object({
6374
6543
  });
6375
6544
  var FontLoader = class {
6376
6545
  constructor() {
6377
- this.loadedFonts = /* @__PURE__ */ new Set();
6378
- }
6379
- /**
6380
- * Load multiple font families.
6381
- * Fonts that have already been loaded will be skipped.
6546
+ // Families whose stylesheet has fully loaded (fetched, injected, and the
6547
+ // FontFaces reported ready by the browser).
6548
+ this.loaded = /* @__PURE__ */ new Set();
6549
+ // In-flight loads, keyed by family name, so concurrent callers share one load
6550
+ // and critically can `await` it to *completion*. A plain "already started"
6551
+ // flag (the old behaviour) would let a second caller proceed before the font is
6552
+ // actually ready, which breaks code that measures text right after requesting a
6553
+ // font (the block would be sized against fallback metrics).
6554
+ this.inFlight = /* @__PURE__ */ new Map();
6555
+ }
6556
+ /**
6557
+ * Load multiple font families. Already-loaded families are skipped; in-flight
6558
+ * ones are shared. Resolves once all requested families are ready.
6382
6559
  *
6383
6560
  * @param families - Array of font families to load
6384
6561
  */
6385
6562
  async loadFonts(families) {
6386
- const unloadedFamilies = families.filter((family) => !this.loadedFonts.has(family.name));
6387
- if (unloadedFamilies.length === 0) {
6388
- return;
6389
- }
6390
- const fontPromises = unloadedFamilies.map((family) => this.loadSingleFont(family));
6391
- await Promise.all(fontPromises);
6563
+ await Promise.all(families.map((family) => this.loadFont(family)));
6564
+ }
6565
+ /**
6566
+ * Load a single font family, returning a promise that resolves only once the
6567
+ * font is actually ready to render. Idempotent: an already-loaded family
6568
+ * resolves immediately, and concurrent calls for the same family share the one
6569
+ * in-flight load — so a caller that measures text can `await` real metrics.
6570
+ *
6571
+ * @param family - Font family to load
6572
+ */
6573
+ loadFont(family) {
6574
+ if (this.loaded.has(family.name)) return Promise.resolve();
6575
+ const existing = this.inFlight.get(family.name);
6576
+ if (existing) return existing;
6577
+ const promise = this.loadSingleFont(family).then(() => {
6578
+ this.loaded.add(family.name);
6579
+ }).finally(() => {
6580
+ this.inFlight.delete(family.name);
6581
+ });
6582
+ this.inFlight.set(family.name, promise);
6583
+ return promise;
6392
6584
  }
6393
6585
  /**
6394
6586
  * Build a Google Fonts URL with weight and italic variants.
@@ -6425,34 +6617,36 @@ var FontLoader = class {
6425
6617
  }
6426
6618
  /**
6427
6619
  * Load a single font family.
6428
- * Adds a link element to the document head and waits for the font to be ready.
6620
+ * Fetches the stylesheet as text and injects it via an inline <style>, so
6621
+ * the @font-face rules land in a same-origin sheet. Tools that introspect
6622
+ * CSSOM (e.g. modern-screenshot's font embedding for /preview raster
6623
+ * captures) can't read cross-origin sheets, which a <link> to
6624
+ * fonts.googleapis.com would produce.
6429
6625
  *
6430
6626
  * @param family - Font family to load
6431
6627
  */
6432
- loadSingleFont(family) {
6433
- this.loadedFonts.add(family.name);
6434
- if (typeof document === "undefined") {
6435
- return Promise.resolve();
6436
- }
6437
- return new Promise((resolve, reject) => {
6438
- const link = document.createElement("link");
6439
- link.rel = "stylesheet";
6440
- link.href = this.buildGoogleFontsUrl(family);
6441
- link.onload = async () => {
6442
- try {
6443
- const loadPromises = this.getFontLoadPromises(family);
6444
- await Promise.all(loadPromises);
6445
- await document.fonts.ready;
6446
- resolve();
6447
- } catch (error) {
6448
- reject(new Error(`Failed to load font face for: ${family.name}. ${error}`));
6449
- }
6450
- };
6451
- link.onerror = () => {
6452
- reject(new Error(`Failed to load font stylesheet: ${family.name}`));
6453
- };
6454
- document.head.appendChild(link);
6455
- });
6628
+ async loadSingleFont(family) {
6629
+ if (typeof document === "undefined") return;
6630
+ const url = this.buildGoogleFontsUrl(family);
6631
+ let cssText;
6632
+ try {
6633
+ const res = await fetch(url);
6634
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
6635
+ cssText = await res.text();
6636
+ } catch (err) {
6637
+ throw new Error(`Failed to load font stylesheet: ${family.name}. ${err}`);
6638
+ }
6639
+ const style = document.createElement("style");
6640
+ style.setAttribute("data-wov-font-family", family.name);
6641
+ style.textContent = cssText;
6642
+ document.head.appendChild(style);
6643
+ try {
6644
+ const loadPromises = this.getFontLoadPromises(family);
6645
+ await Promise.all(loadPromises);
6646
+ await document.fonts.ready;
6647
+ } catch (err) {
6648
+ throw new Error(`Failed to load font face for: ${family.name}. ${err}`);
6649
+ }
6456
6650
  }
6457
6651
  /**
6458
6652
  * Get promises for loading all font variants (weights and italics).
@@ -6478,13 +6672,13 @@ var FontLoader = class {
6478
6672
  * @returns True if the font has been loaded
6479
6673
  */
6480
6674
  isLoaded(fontName) {
6481
- return this.loadedFonts.has(fontName);
6675
+ return this.loaded.has(fontName);
6482
6676
  }
6483
6677
  /**
6484
6678
  * Get the set of loaded font names.
6485
6679
  */
6486
6680
  getLoadedFonts() {
6487
- return this.loadedFonts;
6681
+ return this.loaded;
6488
6682
  }
6489
6683
  };
6490
6684
 
@@ -6565,7 +6759,7 @@ var Editor = class {
6565
6759
  const plugins = pluginInputs.map(parsePlugin);
6566
6760
  const corePlugin = createCorePlugin(options.corePlugin);
6567
6761
  const sortedPlugins = sortPluginsByDependencies([corePlugin, ...plugins]);
6568
- const allDefs = [CommandMarker, Synced8];
6762
+ const allDefs = [CommandMarker, Synced9];
6569
6763
  for (const plugin of sortedPlugins) {
6570
6764
  if (plugin.components) {
6571
6765
  allDefs.push(...plugin.components);
@@ -6695,17 +6889,14 @@ var Editor = class {
6695
6889
  * Call this after construction to run async setup.
6696
6890
  */
6697
6891
  async initialize() {
6698
- if (this.fonts.length > 0) {
6699
- await this.fontLoader.loadFonts(this.fonts);
6700
- }
6701
6892
  Grid.copy(this.ctx, this.gridOptions);
6702
6893
  Controls.copy(this.ctx, this.controlsOptions);
6703
6894
  this.nextTick((ctx) => {
6704
- const userEntity = createEntity8(ctx);
6705
- addComponent11(ctx, userEntity, Synced8, {
6895
+ const userEntity = createEntity9(ctx);
6896
+ addComponent12(ctx, userEntity, Synced9, {
6706
6897
  id: crypto.randomUUID()
6707
6898
  });
6708
- addComponent11(ctx, userEntity, User, {
6899
+ addComponent12(ctx, userEntity, User, {
6709
6900
  userId: this.userData.userId,
6710
6901
  sessionId: this.userData.sessionId,
6711
6902
  color: this.userData.color,
@@ -6719,6 +6910,29 @@ var Editor = class {
6719
6910
  }
6720
6911
  }
6721
6912
  }
6913
+ /**
6914
+ * Load a single font family's stylesheet on demand, by its `name` (the CSS
6915
+ * font-family — the same key the picker and `Text.fontFamily` use). Looks the
6916
+ * family up in the configured `fonts` and injects its @font-face rules into
6917
+ * the document. Idempotent — the FontLoader dedups, so repeated calls for an
6918
+ * already-loaded (or in-flight) family are cheap no-ops. Unknown names are
6919
+ * ignored. This is how the editor scales to a large font library: nothing is
6920
+ * fetched until a text block actually displays/edits in that family.
6921
+ */
6922
+ async loadFont(name) {
6923
+ if (!name) return;
6924
+ const family = this.fonts.find((f) => f.name === name);
6925
+ if (!family) return;
6926
+ try {
6927
+ await this.fontLoader.loadFont(family);
6928
+ } catch (err) {
6929
+ console.warn(`Failed to load font: ${name}`, err);
6930
+ }
6931
+ }
6932
+ /** Whether a font family's stylesheet has finished loading. */
6933
+ isFontLoaded(name) {
6934
+ return this.fontLoader.isLoaded(name);
6935
+ }
6722
6936
  /**
6723
6937
  * Run one frame of the editor loop.
6724
6938
  *
@@ -6933,11 +7147,14 @@ export {
6933
7147
  Key,
6934
7148
  Keybind,
6935
7149
  Keyboard,
7150
+ Layer,
6936
7151
  MAX_HIT_ARCS,
6937
7152
  MAX_HIT_CAPSULES,
7153
+ MAX_HIT_POLYGON_POINTS,
6938
7154
  MainThreadSystem2 as MainThreadSystem,
6939
7155
  Mouse,
6940
7156
  Opacity,
7157
+ PlaceBlockEvent,
6941
7158
  Pointer,
6942
7159
  PointerButton,
6943
7160
  PointerType,
@@ -6971,7 +7188,7 @@ export {
6971
7188
  StartTransformBoxEdit,
6972
7189
  Stratum,
6973
7190
  StrokeKind,
6974
- Synced9 as Synced,
7191
+ Synced10 as Synced,
6975
7192
  Text,
6976
7193
  TextAlignment,
6977
7194
  Tick,
@@ -6990,16 +7207,17 @@ export {
6990
7207
  UserData,
6991
7208
  VerticalAlign,
6992
7209
  VerticalAlignment,
6993
- addComponent12 as addComponent,
7210
+ addComponent13 as addComponent,
6994
7211
  canBlockEdit,
6995
7212
  canBlockInteract,
6996
7213
  cascadeDelete,
6997
7214
  clearPointerTrackingState,
6998
7215
  compareBlockOrder,
6999
7216
  convertRefsToUuids,
7217
+ createBlock,
7000
7218
  createCorePlugin,
7001
- createEntity9 as createEntity,
7002
- defineCanvasComponent22 as defineCanvasComponent,
7219
+ createEntity10 as createEntity,
7220
+ defineCanvasComponent23 as defineCanvasComponent,
7003
7221
  defineCanvasSingleton3 as defineCanvasSingleton,
7004
7222
  defineCommand,
7005
7223
  defineEditorState,
@@ -7009,7 +7227,7 @@ export {
7009
7227
  deselectAll,
7010
7228
  deselectBlock,
7011
7229
  detectEmbedProvider,
7012
- field36 as field,
7230
+ field37 as field,
7013
7231
  findFrameAtPoint,
7014
7232
  generateUuidBySeed,
7015
7233
  getBackrefs5 as getBackrefs,
@@ -7027,19 +7245,21 @@ export {
7027
7245
  getResources15 as getResources,
7028
7246
  getTopmostBlockAtPoint,
7029
7247
  getWorldPosition,
7030
- hasComponent20 as hasComponent,
7248
+ hasComponent21 as hasComponent,
7031
7249
  intersectAabb,
7032
7250
  intersectCapsule,
7033
7251
  intersectPoint,
7034
7252
  isAlive3 as isAlive,
7035
7253
  isAncestor,
7036
7254
  isHeldByRemote,
7255
+ isLayerInteractive,
7037
7256
  isReadonly,
7038
7257
  on,
7039
7258
  parsePlugin,
7040
7259
  removeComponent7 as removeComponent,
7041
7260
  removeEntity7 as removeEntity,
7042
7261
  resolveEmbedUrl,
7262
+ resolveLayerRank,
7043
7263
  resolveRefFields,
7044
7264
  runMachine,
7045
7265
  selectAll,