@woven-canvas/core 1.0.5 → 1.0.9

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,
@@ -231,6 +234,9 @@ function getPluginResources(ctx, pluginName) {
231
234
  const resources = ctx.resources;
232
235
  return resources.pluginResources[pluginName];
233
236
  }
237
+ function isReadonly(ctx) {
238
+ return ctx.resources.readonly;
239
+ }
234
240
  var GridOptions = z.object({
235
241
  /**
236
242
  * Whether grid snapping is enabled.
@@ -392,7 +398,16 @@ var EditorOptionsSchema = z.object({
392
398
  * }
393
399
  * ```
394
400
  */
395
- controls: z.custom().optional()
401
+ controls: z.custom().optional(),
402
+ /**
403
+ * When true, user-initiated mutations are blocked — input systems skip
404
+ * their bodies and `editor.command()` is a no-op. Adapter-originated
405
+ * mutations (websocket, persistence) still apply, so remote edits remain
406
+ * visible. Useful for viewer-mode share links.
407
+ *
408
+ * @default false
409
+ */
410
+ readonly: z.boolean().default(false)
396
411
  });
397
412
  var Keybind = z.object({
398
413
  /** The command to execute when this keybind is triggered */
@@ -460,7 +475,9 @@ var BlockDef = z.object({
460
475
  resizeMode: z.enum([ResizeMode.Scale, ResizeMode.Text, ResizeMode.Free, ResizeMode.GroupOnly]).default(ResizeMode.Scale),
461
476
  canRotate: z.boolean().default(true),
462
477
  canScale: z.boolean().default(true),
478
+ snappable: z.boolean().default(true),
463
479
  interactable: z.boolean().default(true),
480
+ excludeFromRankBounds: z.boolean().default(false),
464
481
  connectors: BlockDefConnectors.default(BlockDefConnectors.parse({}))
465
482
  });
466
483
  var SelectionState = {
@@ -526,7 +543,10 @@ var BlockSchema = {
526
543
  /** How the block can be resized */
527
544
  resizeMode: field2.enum(ResizeMode).default(ResizeMode.Default),
528
545
  /** Parent block ID — position is relative to parent if set */
529
- 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()
530
550
  };
531
551
  var _worldPos = [0, 0];
532
552
  var BlockDef2 = class extends CanvasComponentDef {
@@ -1015,19 +1035,25 @@ var Held = defineCanvasComponent9(
1015
1035
  );
1016
1036
 
1017
1037
  // src/components/HitGeometry.ts
1018
- import { Arc, Capsule, Mat2 } from "@woven-canvas/math";
1038
+ import { Arc, Capsule, Mat2, Polygon } from "@woven-canvas/math";
1019
1039
  import { CanvasComponentDef as CanvasComponentDef4 } from "@woven-ecs/canvas-store";
1020
1040
  import { field as field11 } from "@woven-ecs/core";
1021
1041
  var _uvToWorldMatrix = [1, 0, 0, 1, 0, 0];
1042
+ var _polyPt = [0, 0];
1022
1043
  var MAX_HIT_CAPSULES = 64;
1023
1044
  var FLOATS_PER_CAPSULE = 5;
1024
1045
  var MAX_HIT_ARCS = 2;
1025
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);
1026
1050
  var HitGeometrySchema = {
1027
1051
  hitCapsules: field11.buffer(field11.float32()).size(MAX_HIT_CAPSULES * FLOATS_PER_CAPSULE),
1028
1052
  capsuleCount: field11.uint16().default(0),
1029
1053
  hitArcs: field11.buffer(field11.float32()).size(MAX_HIT_ARCS * FLOATS_PER_ARC),
1030
- 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)
1031
1057
  };
1032
1058
  var HitGeometryDef = class extends CanvasComponentDef4 {
1033
1059
  constructor() {
@@ -1138,8 +1164,27 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1138
1164
  return true;
1139
1165
  }
1140
1166
  }
1167
+ const polygonPointCount = this.fillWorldPolygon(hitGeometry.hitPolygon, hitGeometry.polygonPointCount);
1168
+ if (polygonPointCount > 0 && Polygon.containsPointEvenOdd(_worldPolygon, polygonPointCount, point)) {
1169
+ return true;
1170
+ }
1141
1171
  return false;
1142
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
+ }
1143
1188
  /**
1144
1189
  * Check if an AABB intersects with the entity's hit geometry.
1145
1190
  * Hit geometry is stored in UV coordinates (0-1) relative to the block.
@@ -1179,6 +1224,10 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1179
1224
  return true;
1180
1225
  }
1181
1226
  }
1227
+ const polygonPointCount = this.fillWorldPolygon(hitGeometry.hitPolygon, hitGeometry.polygonPointCount);
1228
+ if (polygonPointCount > 0 && Polygon.intersectsAabb(_worldPolygon, polygonPointCount, aabb)) {
1229
+ return true;
1230
+ }
1182
1231
  return false;
1183
1232
  }
1184
1233
  /**
@@ -1216,6 +1265,11 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1216
1265
  const worldArc = Arc.create(worldA[0], worldA[1], worldB[0], worldB[1], worldC[0], worldC[1], thickness);
1217
1266
  pts.push(...Arc.getExtrema(worldArc));
1218
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
+ }
1219
1273
  return pts;
1220
1274
  }
1221
1275
  // ============================================
@@ -1318,6 +1372,27 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1318
1372
  const uvC = Block.worldToUv(ctx, entityId, worldC);
1319
1373
  this.addArcUv(ctx, entityId, uvA, uvB, uvC, worldThickness);
1320
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
+ }
1321
1396
  /**
1322
1397
  * Clear all hit geometry from an entity.
1323
1398
  *
@@ -1328,6 +1403,7 @@ var HitGeometryDef = class extends CanvasComponentDef4 {
1328
1403
  const hitGeometry = this.write(ctx, entityId);
1329
1404
  hitGeometry.capsuleCount = 0;
1330
1405
  hitGeometry.arcCount = 0;
1406
+ hitGeometry.polygonPointCount = 0;
1331
1407
  }
1332
1408
  };
1333
1409
  var HitGeometry = new HitGeometryDef();
@@ -1351,19 +1427,39 @@ var Image = defineCanvasComponent11(
1351
1427
  }
1352
1428
  );
1353
1429
 
1354
- // src/components/Opacity.ts
1430
+ // src/components/Layer.ts
1355
1431
  import { defineCanvasComponent as defineCanvasComponent12 } from "@woven-ecs/canvas-store";
1356
1432
  import { field as field13 } from "@woven-ecs/core";
1357
- 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(
1358
1454
  { name: "opacity" },
1359
1455
  {
1360
- value: field13.uint8().default(255)
1456
+ value: field14.uint8().default(255)
1361
1457
  }
1362
1458
  );
1363
1459
 
1364
1460
  // src/components/Pointer.ts
1365
1461
  import { CanvasComponentDef as CanvasComponentDef5 } from "@woven-ecs/canvas-store";
1366
- import { field as field14 } from "@woven-ecs/core";
1462
+ import { field as field15 } from "@woven-ecs/core";
1367
1463
  var PointerButton = {
1368
1464
  None: "none",
1369
1465
  Left: "left",
@@ -1380,30 +1476,30 @@ var PointerType = {
1380
1476
  var SAMPLE_COUNT = 6;
1381
1477
  var PointerSchema = {
1382
1478
  /** Unique pointer ID (from PointerEvent.pointerId) */
1383
- pointerId: field14.uint16().default(0),
1479
+ pointerId: field15.uint16().default(0),
1384
1480
  /** Current position relative to the editor element [x, y] */
1385
- position: field14.tuple(field14.float32(), 2).default([0, 0]),
1481
+ position: field15.tuple(field15.float32(), 2).default([0, 0]),
1386
1482
  /** Position where the pointer went down [x, y] */
1387
- downPosition: field14.tuple(field14.float32(), 2).default([0, 0]),
1483
+ downPosition: field15.tuple(field15.float32(), 2).default([0, 0]),
1388
1484
  /** Frame number when the pointer went down (for click detection) */
1389
- downFrame: field14.uint32().default(0),
1485
+ downFrame: field15.uint32().default(0),
1390
1486
  /** Which button is pressed */
1391
- button: field14.enum(PointerButton).default(PointerButton.None),
1487
+ button: field15.enum(PointerButton).default(PointerButton.None),
1392
1488
  /** Type of pointer device */
1393
- pointerType: field14.enum(PointerType).default(PointerType.Mouse),
1489
+ pointerType: field15.enum(PointerType).default(PointerType.Mouse),
1394
1490
  /** Pressure from 0 to 1 (for pen/touch) */
1395
- pressure: field14.float32().default(0),
1491
+ pressure: field15.float32().default(0),
1396
1492
  /** Whether the pointer event target was not the editor element */
1397
- obscured: field14.boolean().default(false),
1493
+ obscured: field15.boolean().default(false),
1398
1494
  // Velocity tracking (ring buffer for position samples)
1399
1495
  /** Ring buffer of previous positions [x0, y0, x1, y1, ...] @internal */
1400
- _prevPositions: field14.array(field14.float32(), SAMPLE_COUNT * 2),
1496
+ _prevPositions: field15.array(field15.float32(), SAMPLE_COUNT * 2),
1401
1497
  /** Ring buffer of timestamps for each position sample @internal */
1402
- _prevTimes: field14.array(field14.float32(), SAMPLE_COUNT),
1498
+ _prevTimes: field15.array(field15.float32(), SAMPLE_COUNT),
1403
1499
  /** Total number of samples added (used for ring buffer indexing) @internal */
1404
- _sampleCount: field14.int32().default(0),
1500
+ _sampleCount: field15.int32().default(0),
1405
1501
  /** Computed velocity [vx, vy] in pixels per second @internal */
1406
- _velocity: field14.tuple(field14.float32(), 2).default([0, 0])
1502
+ _velocity: field15.tuple(field15.float32(), 2).default([0, 0])
1407
1503
  };
1408
1504
  var PointerDef = class extends CanvasComponentDef5 {
1409
1505
  constructor() {
@@ -1514,96 +1610,96 @@ function addPointerSample(pointer, position, time) {
1514
1610
  }
1515
1611
 
1516
1612
  // src/components/ScaleWithZoom.ts
1517
- import { defineCanvasComponent as defineCanvasComponent13 } from "@woven-ecs/canvas-store";
1518
- import { field as field15 } from "@woven-ecs/core";
1519
- 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(
1520
1616
  { name: "scaleWithZoom" },
1521
1617
  {
1522
1618
  /** Pivot point for scaling as [x, y] (0-1, default 0.5,0.5 = center) */
1523
- anchor: field15.tuple(field15.float64(), 2).default([0.5, 0.5]),
1619
+ anchor: field16.tuple(field16.float64(), 2).default([0.5, 0.5]),
1524
1620
  /** Initial position as [left, top] at zoom=1 */
1525
- startPosition: field15.tuple(field15.float64(), 2).default([0, 0]),
1621
+ startPosition: field16.tuple(field16.float64(), 2).default([0, 0]),
1526
1622
  /** Initial size as [width, height] at zoom=1 */
1527
- startSize: field15.tuple(field15.float64(), 2).default([0, 0]),
1623
+ startSize: field16.tuple(field16.float64(), 2).default([0, 0]),
1528
1624
  /** Scale multiplier per dimension: [x, y] (0 = no zoom effect, 1 = full zoom effect, 0.5 = half effect) */
1529
- scaleMultiplier: field15.tuple(field15.float64(), 2).default([1, 1])
1625
+ scaleMultiplier: field16.tuple(field16.float64(), 2).default([1, 1])
1530
1626
  }
1531
1627
  );
1532
1628
 
1533
1629
  // src/components/Selected.ts
1534
- import { defineCanvasComponent as defineCanvasComponent14 } from "@woven-ecs/canvas-store";
1535
- var Selected = defineCanvasComponent14({ name: "selected" }, {});
1630
+ import { defineCanvasComponent as defineCanvasComponent15 } from "@woven-ecs/canvas-store";
1631
+ var Selected = defineCanvasComponent15({ name: "selected" }, {});
1536
1632
 
1537
1633
  // src/components/SelectionBox.ts
1538
- import { defineCanvasComponent as defineCanvasComponent15 } from "@woven-ecs/canvas-store";
1539
- var SelectionBox = defineCanvasComponent15({ name: "selectionBox" }, {});
1634
+ import { defineCanvasComponent as defineCanvasComponent16 } from "@woven-ecs/canvas-store";
1635
+ var SelectionBox = defineCanvasComponent16({ name: "selectionBox" }, {});
1540
1636
 
1541
1637
  // src/components/Shape.ts
1542
- import { defineCanvasComponent as defineCanvasComponent16 } from "@woven-ecs/canvas-store";
1543
- 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";
1544
1640
  var StrokeKind = {
1545
1641
  Solid: "solid",
1546
1642
  Dashed: "dashed",
1547
1643
  None: "none"
1548
1644
  };
1549
- var Shape = defineCanvasComponent16(
1645
+ var Shape = defineCanvasComponent17(
1550
1646
  { name: "shape", sync: "document" },
1551
1647
  {
1552
1648
  /** The kind of shape to render (e.g. 'rectangle', 'ellipse', or custom shape key) */
1553
- kind: field16.string().default("rectangle"),
1649
+ kind: field17.string().default("rectangle"),
1554
1650
  /** Stroke style */
1555
- strokeKind: field16.enum(StrokeKind).default(StrokeKind.Solid),
1651
+ strokeKind: field17.enum(StrokeKind).default(StrokeKind.Solid),
1556
1652
  /** Stroke width in pixels */
1557
- strokeWidth: field16.uint16().default(2),
1653
+ strokeWidth: field17.uint16().default(2),
1558
1654
  /** Stroke color - red component (0-255) */
1559
- strokeRed: field16.uint8().default(0),
1655
+ strokeRed: field17.uint8().default(0),
1560
1656
  /** Stroke color - green component (0-255) */
1561
- strokeGreen: field16.uint8().default(0),
1657
+ strokeGreen: field17.uint8().default(0),
1562
1658
  /** Stroke color - blue component (0-255) */
1563
- strokeBlue: field16.uint8().default(0),
1659
+ strokeBlue: field17.uint8().default(0),
1564
1660
  /** Stroke color - alpha component (0-255) */
1565
- strokeAlpha: field16.uint8().default(255),
1661
+ strokeAlpha: field17.uint8().default(255),
1566
1662
  /** Fill color - red component (0-255) */
1567
- fillRed: field16.uint8().default(255),
1663
+ fillRed: field17.uint8().default(255),
1568
1664
  /** Fill color - green component (0-255) */
1569
- fillGreen: field16.uint8().default(255),
1665
+ fillGreen: field17.uint8().default(255),
1570
1666
  /** Fill color - blue component (0-255) */
1571
- fillBlue: field16.uint8().default(255),
1667
+ fillBlue: field17.uint8().default(255),
1572
1668
  /** Fill color - alpha component (0-255) */
1573
- fillAlpha: field16.uint8().default(0)
1669
+ fillAlpha: field17.uint8().default(0)
1574
1670
  }
1575
1671
  );
1576
1672
 
1577
1673
  // src/components/Text.ts
1578
- import { defineCanvasComponent as defineCanvasComponent17 } from "@woven-ecs/canvas-store";
1579
- import { field as field17 } from "@woven-ecs/core";
1580
- 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(
1581
1677
  { name: "text", sync: "document" },
1582
1678
  {
1583
1679
  /** HTML content (supports rich text formatting) */
1584
- content: field17.string().max(1e4).default(""),
1680
+ content: field18.string().max(1e4).default(""),
1585
1681
  /** Font size in pixels */
1586
- fontSizePx: field17.float64().default(24),
1682
+ fontSizePx: field18.float64().default(24),
1587
1683
  /** Font family name */
1588
- fontFamily: field17.string().max(64).default(""),
1684
+ fontFamily: field18.string().max(64).default(""),
1589
1685
  /** Line height multiplier */
1590
- lineHeight: field17.float64().default(1.2),
1686
+ lineHeight: field18.float64().default(1.2),
1591
1687
  /** Letter spacing in em units */
1592
- letterSpacingEm: field17.float64().default(0),
1688
+ letterSpacingEm: field18.float64().default(0),
1593
1689
  /** Whether width is constrained (text wraps) */
1594
- constrainWidth: field17.boolean().default(true),
1690
+ constrainWidth: field18.boolean().default(true),
1595
1691
  /** Default text alignment for new paragraphs */
1596
- defaultAlignment: field17.enum(TextAlignment).default(TextAlignment.Left)
1692
+ defaultAlignment: field18.enum(TextAlignment).default(TextAlignment.Left)
1597
1693
  }
1598
1694
  );
1599
1695
 
1600
1696
  // src/components/TransformBox.ts
1601
- import { defineCanvasComponent as defineCanvasComponent18 } from "@woven-ecs/canvas-store";
1602
- var TransformBox = defineCanvasComponent18({ name: "transformBox" }, {});
1697
+ import { defineCanvasComponent as defineCanvasComponent19 } from "@woven-ecs/canvas-store";
1698
+ var TransformBox = defineCanvasComponent19({ name: "transformBox" }, {});
1603
1699
 
1604
1700
  // src/components/TransformHandle.ts
1605
- import { defineCanvasComponent as defineCanvasComponent19 } from "@woven-ecs/canvas-store";
1606
- 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";
1607
1703
 
1608
1704
  // src/cursors.ts
1609
1705
  var CursorKind = /* @__PURE__ */ ((CursorKind2) => {
@@ -1704,39 +1800,39 @@ var CURSORS = {
1704
1800
  };
1705
1801
 
1706
1802
  // src/components/TransformHandle.ts
1707
- var TransformHandle = defineCanvasComponent19(
1803
+ var TransformHandle = defineCanvasComponent20(
1708
1804
  { name: "transformHandle" },
1709
1805
  {
1710
- kind: field18.string().max(16).default(TransformHandleKind.Scale),
1711
- vectorX: field18.int8().default(0),
1712
- vectorY: field18.int8().default(0),
1713
- transformBox: field18.ref(),
1714
- 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 */)
1715
1811
  }
1716
1812
  );
1717
1813
 
1718
1814
  // src/components/User.ts
1719
- import { defineCanvasComponent as defineCanvasComponent20 } from "@woven-ecs/canvas-store";
1720
- import { field as field19 } from "@woven-ecs/core";
1721
- 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(
1722
1818
  { name: "user", sync: "ephemeral" },
1723
1819
  {
1724
- userId: field19.string().max(36),
1725
- sessionId: field19.string().max(36),
1726
- color: field19.string().max(7),
1727
- name: field19.string().max(100),
1728
- avatar: field19.string().max(500),
1729
- 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])
1730
1826
  }
1731
1827
  );
1732
1828
 
1733
1829
  // src/components/VerticalAlign.ts
1734
- import { defineCanvasComponent as defineCanvasComponent21 } from "@woven-ecs/canvas-store";
1735
- import { field as field20 } from "@woven-ecs/core";
1736
- 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(
1737
1833
  { name: "verticalAlign", sync: "document" },
1738
1834
  {
1739
- value: field20.enum(VerticalAlignment).default(VerticalAlignment.Top)
1835
+ value: field21.enum(VerticalAlignment).default(VerticalAlignment.Top)
1740
1836
  }
1741
1837
  );
1742
1838
 
@@ -1775,20 +1871,20 @@ __export(singletons_exports, {
1775
1871
 
1776
1872
  // src/singletons/Camera.ts
1777
1873
  import { CanvasSingletonDef as CanvasSingletonDef2 } from "@woven-ecs/canvas-store";
1778
- import { field as field22 } from "@woven-ecs/core";
1874
+ import { field as field23 } from "@woven-ecs/core";
1779
1875
 
1780
1876
  // src/singletons/Screen.ts
1781
1877
  import { CanvasSingletonDef } from "@woven-ecs/canvas-store";
1782
- import { field as field21 } from "@woven-ecs/core";
1878
+ import { field as field22 } from "@woven-ecs/core";
1783
1879
  var ScreenSchema = {
1784
1880
  /** Width of the editor element in pixels */
1785
- width: field21.float64().default(0),
1881
+ width: field22.float64().default(0),
1786
1882
  /** Height of the editor element in pixels */
1787
- height: field21.float64().default(0),
1883
+ height: field22.float64().default(0),
1788
1884
  /** Left offset of the editor element relative to the viewport */
1789
- left: field21.float64().default(0),
1885
+ left: field22.float64().default(0),
1790
1886
  /** Top offset of the editor element relative to the viewport */
1791
- top: field21.float64().default(0)
1887
+ top: field22.float64().default(0)
1792
1888
  };
1793
1889
  var ScreenDef = class extends CanvasSingletonDef {
1794
1890
  constructor() {
@@ -1815,15 +1911,15 @@ var Screen = new ScreenDef();
1815
1911
  // src/singletons/Camera.ts
1816
1912
  var CameraSchema = {
1817
1913
  /** Top position of the camera in world coordinates */
1818
- top: field22.float64().default(0),
1914
+ top: field23.float64().default(0),
1819
1915
  /** Left position of the camera in world coordinates */
1820
- left: field22.float64().default(0),
1916
+ left: field23.float64().default(0),
1821
1917
  /** Zoom level (1 = 100%, 2 = 200%, 0.5 = 50%) */
1822
- zoom: field22.float64().default(1),
1918
+ zoom: field23.float64().default(1),
1823
1919
  /** Whether the camera viewport intersects any blocks */
1824
- canSeeBlocks: field22.boolean().default(true),
1920
+ canSeeBlocks: field23.boolean().default(true),
1825
1921
  /** Reference to a block that the camera can currently see (for optimization) */
1826
- lastSeenBlock: field22.ref()
1922
+ lastSeenBlock: field23.ref()
1827
1923
  };
1828
1924
  var CameraDef = class extends CanvasSingletonDef2 {
1829
1925
  constructor() {
@@ -1899,22 +1995,22 @@ var Camera = new CameraDef();
1899
1995
 
1900
1996
  // src/singletons/Controls.ts
1901
1997
  import { CanvasSingletonDef as CanvasSingletonDef3 } from "@woven-ecs/canvas-store";
1902
- import { field as field23 } from "@woven-ecs/core";
1998
+ import { field as field24 } from "@woven-ecs/core";
1903
1999
  var ControlsSchema = {
1904
2000
  /** Tool activated by left mouse button */
1905
- leftMouseTool: field23.string().max(32).default("select"),
2001
+ leftMouseTool: field24.string().max(32).default("select"),
1906
2002
  /** Tool activated by middle mouse button */
1907
- middleMouseTool: field23.string().max(32).default("hand"),
2003
+ middleMouseTool: field24.string().max(32).default("hand"),
1908
2004
  /** Tool activated by right mouse button */
1909
- rightMouseTool: field23.string().max(32).default("hand"),
2005
+ rightMouseTool: field24.string().max(32).default("hand"),
1910
2006
  /** Tool activated by mouse wheel */
1911
- wheelTool: field23.string().max(32).default("scroll"),
2007
+ wheelTool: field24.string().max(32).default("scroll"),
1912
2008
  /** Tool activated by mouse wheel with modifier key held */
1913
- modWheelTool: field23.string().max(32).default("zoom"),
2009
+ modWheelTool: field24.string().max(32).default("zoom"),
1914
2010
  /** JSON snapshot of block to place on next click (empty string = no placement active) */
1915
- heldSnapshot: field23.string().max(65536).default(""),
2011
+ heldSnapshot: field24.string().max(65536).default(""),
1916
2012
  /** User-facing tool name for UI highlighting (may differ from leftMouseTool during draw/drag-out) */
1917
- activeToolName: field23.string().max(32).default("select")
2013
+ activeToolName: field24.string().max(32).default("select")
1918
2014
  };
1919
2015
  var ControlsDef = class extends CanvasSingletonDef3 {
1920
2016
  constructor() {
@@ -1956,16 +2052,16 @@ var Controls = new ControlsDef();
1956
2052
 
1957
2053
  // src/singletons/Cursor.ts
1958
2054
  import { CanvasSingletonDef as CanvasSingletonDef4 } from "@woven-ecs/canvas-store";
1959
- import { field as field24 } from "@woven-ecs/core";
2055
+ import { field as field25 } from "@woven-ecs/core";
1960
2056
  var CursorSchema = {
1961
2057
  /** Base cursor kind (from current tool) */
1962
- cursorKind: field24.string().max(64).default("select"),
2058
+ cursorKind: field25.string().max(64).default("select"),
1963
2059
  /** Base cursor rotation in radians */
1964
- rotation: field24.float64().default(0),
2060
+ rotation: field25.float64().default(0),
1965
2061
  /** Context-specific cursor kind (overrides cursorKind when set, e.g., during drag/hover) */
1966
- contextCursorKind: field24.string().max(64).default(""),
2062
+ contextCursorKind: field25.string().max(64).default(""),
1967
2063
  /** Context cursor rotation in radians */
1968
- contextRotation: field24.float64().default(0)
2064
+ contextRotation: field25.float64().default(0)
1969
2065
  };
1970
2066
  var CursorDef2 = class extends CanvasSingletonDef4 {
1971
2067
  constructor() {
@@ -2012,7 +2108,7 @@ var CursorDef2 = class extends CanvasSingletonDef4 {
2012
2108
  var Cursor = new CursorDef2();
2013
2109
 
2014
2110
  // src/singletons/FrameContainmentState.ts
2015
- import { field as field25 } from "@woven-ecs/core";
2111
+ import { field as field26 } from "@woven-ecs/core";
2016
2112
 
2017
2113
  // src/EditorStateDef.ts
2018
2114
  import { CanvasSingletonDef as CanvasSingletonDef5 } from "@woven-ecs/canvas-store";
@@ -2120,27 +2216,27 @@ function defineEditorState(name, schema) {
2120
2216
  // src/singletons/FrameContainmentState.ts
2121
2217
  var FrameContainmentState = defineEditorState("frameContainmentState", {
2122
2218
  /** Current state machine state */
2123
- state: field25.string().max(16).default(FrameContainmentStateEnum.Idle),
2219
+ state: field26.string().max(16).default(FrameContainmentStateEnum.Idle),
2124
2220
  /** EntityId of the frame currently highlighted as a drop target, or null */
2125
- highlightedFrame: field25.ref()
2221
+ highlightedFrame: field26.ref()
2126
2222
  });
2127
2223
 
2128
2224
  // src/singletons/Grid.ts
2129
2225
  import { CanvasSingletonDef as CanvasSingletonDef6 } from "@woven-ecs/canvas-store";
2130
- import { field as field26 } from "@woven-ecs/core";
2226
+ import { field as field27 } from "@woven-ecs/core";
2131
2227
  var GridSchema = {
2132
2228
  /** Whether grid snapping is enabled */
2133
- enabled: field26.boolean().default(false),
2229
+ enabled: field27.boolean().default(false),
2134
2230
  /** Whether resized/rotated objects must stay aligned to the grid */
2135
- strict: field26.boolean().default(false),
2231
+ strict: field27.boolean().default(false),
2136
2232
  /** Width of each grid column in world units */
2137
- colWidth: field26.float64().default(20),
2233
+ colWidth: field27.float64().default(20),
2138
2234
  /** Height of each grid row in world units */
2139
- rowHeight: field26.float64().default(20),
2235
+ rowHeight: field27.float64().default(20),
2140
2236
  /** Angular snap increment in radians when grid is enabled */
2141
- snapAngleRad: field26.float64().default(Math.PI / 36),
2237
+ snapAngleRad: field27.float64().default(Math.PI / 36),
2142
2238
  /** Angular snap increment in radians when shift key is held */
2143
- shiftSnapAngleRad: field26.float64().default(Math.PI / 12)
2239
+ shiftSnapAngleRad: field27.float64().default(Math.PI / 12)
2144
2240
  };
2145
2241
  var GridDef = class extends CanvasSingletonDef6 {
2146
2242
  constructor() {
@@ -2203,14 +2299,14 @@ var Grid = new GridDef();
2203
2299
 
2204
2300
  // src/singletons/Intersect.ts
2205
2301
  import { CanvasSingletonDef as CanvasSingletonDef7 } from "@woven-ecs/canvas-store";
2206
- import { field as field27 } from "@woven-ecs/core";
2302
+ import { field as field28 } from "@woven-ecs/core";
2207
2303
  var IntersectSchema = {
2208
2304
  // Store up to 5 intersected entity IDs
2209
- entity1: field27.ref(),
2210
- entity2: field27.ref(),
2211
- entity3: field27.ref(),
2212
- entity4: field27.ref(),
2213
- entity5: field27.ref()
2305
+ entity1: field28.ref(),
2306
+ entity2: field28.ref(),
2307
+ entity3: field28.ref(),
2308
+ entity4: field28.ref(),
2309
+ entity5: field28.ref()
2214
2310
  };
2215
2311
  var IntersectDef = class extends CanvasSingletonDef7 {
2216
2312
  constructor() {
@@ -2262,30 +2358,30 @@ var Intersect = new IntersectDef();
2262
2358
 
2263
2359
  // src/singletons/Keyboard.ts
2264
2360
  import { CanvasSingletonDef as CanvasSingletonDef8 } from "@woven-ecs/canvas-store";
2265
- import { field as field28 } from "@woven-ecs/core";
2361
+ import { field as field29 } from "@woven-ecs/core";
2266
2362
  var KEY_BUFFER_SIZE = 32;
2267
2363
  var KeyboardSchema = {
2268
2364
  /**
2269
2365
  * Buffer where each bit represents whether a key is currently pressed.
2270
2366
  * Uses field.buffer for zero-allocation subarray views.
2271
2367
  */
2272
- keysDown: field28.buffer(field28.uint8()).size(KEY_BUFFER_SIZE),
2368
+ keysDown: field29.buffer(field29.uint8()).size(KEY_BUFFER_SIZE),
2273
2369
  /**
2274
2370
  * Buffer for key-down triggers (true for exactly 1 frame when key is pressed).
2275
2371
  * Uses field.buffer for zero-allocation subarray views.
2276
2372
  */
2277
- keysDownTrigger: field28.buffer(field28.uint8()).size(KEY_BUFFER_SIZE),
2373
+ keysDownTrigger: field29.buffer(field29.uint8()).size(KEY_BUFFER_SIZE),
2278
2374
  /**
2279
2375
  * Buffer for key-up triggers (true for exactly 1 frame when key is released).
2280
2376
  * Uses field.buffer for zero-allocation subarray views.
2281
2377
  */
2282
- keysUpTrigger: field28.buffer(field28.uint8()).size(KEY_BUFFER_SIZE),
2378
+ keysUpTrigger: field29.buffer(field29.uint8()).size(KEY_BUFFER_SIZE),
2283
2379
  /** Common modifier - Shift key is down */
2284
- shiftDown: field28.boolean().default(false),
2380
+ shiftDown: field29.boolean().default(false),
2285
2381
  /** Common modifier - Alt/Option key is down */
2286
- altDown: field28.boolean().default(false),
2382
+ altDown: field29.boolean().default(false),
2287
2383
  /** Common modifier - Ctrl (Windows/Linux) or Cmd (Mac) is down */
2288
- modDown: field28.boolean().default(false)
2384
+ modDown: field29.boolean().default(false)
2289
2385
  };
2290
2386
  function getBit(buffer, bitIndex) {
2291
2387
  if (bitIndex < 0 || bitIndex >= buffer.length * 8) return false;
@@ -2568,24 +2664,24 @@ var Key = {
2568
2664
 
2569
2665
  // src/singletons/Mouse.ts
2570
2666
  import { CanvasSingletonDef as CanvasSingletonDef9 } from "@woven-ecs/canvas-store";
2571
- import { field as field29 } from "@woven-ecs/core";
2667
+ import { field as field30 } from "@woven-ecs/core";
2572
2668
  var MouseSchema = {
2573
2669
  /** Current mouse position relative to the editor element [x, y] */
2574
- position: field29.tuple(field29.float32(), 2).default([0, 0]),
2670
+ position: field30.tuple(field30.float32(), 2).default([0, 0]),
2575
2671
  /** Horizontal wheel delta (positive = scroll right) */
2576
- wheelDeltaX: field29.float32().default(0),
2672
+ wheelDeltaX: field30.float32().default(0),
2577
2673
  /** Vertical wheel delta (positive = scroll down), normalized across browsers */
2578
- wheelDeltaY: field29.float32().default(0),
2674
+ wheelDeltaY: field30.float32().default(0),
2579
2675
  /** True for 1 frame when mouse moves */
2580
- moveTrigger: field29.boolean().default(false),
2676
+ moveTrigger: field30.boolean().default(false),
2581
2677
  /** True for 1 frame when wheel is scrolled */
2582
- wheelTrigger: field29.boolean().default(false),
2678
+ wheelTrigger: field30.boolean().default(false),
2583
2679
  /** True for 1 frame when mouse enters the editor element */
2584
- enterTrigger: field29.boolean().default(false),
2680
+ enterTrigger: field30.boolean().default(false),
2585
2681
  /** True for 1 frame when mouse leaves the editor element */
2586
- leaveTrigger: field29.boolean().default(false),
2682
+ leaveTrigger: field30.boolean().default(false),
2587
2683
  /** Whether the mouse is over a UI element (not the canvas) */
2588
- obscured: field29.boolean().default(false)
2684
+ obscured: field30.boolean().default(false)
2589
2685
  };
2590
2686
  var MouseDef = class extends CanvasSingletonDef9 {
2591
2687
  constructor() {
@@ -2626,11 +2722,11 @@ var Mouse = new MouseDef();
2626
2722
 
2627
2723
  // src/singletons/RankBounds.ts
2628
2724
  import { CanvasSingletonDef as CanvasSingletonDef10 } from "@woven-ecs/canvas-store";
2629
- import { field as field30 } from "@woven-ecs/core";
2725
+ import { field as field31 } from "@woven-ecs/core";
2630
2726
  import { generateJitteredKeyBetween } from "fractional-indexing-jittered";
2631
2727
  var RankBoundsSchema = {
2632
- minRank: field30.string().max(36).default(""),
2633
- maxRank: field30.string().max(36).default("")
2728
+ minRank: field31.string().max(36).default(""),
2729
+ maxRank: field31.string().max(36).default("")
2634
2730
  };
2635
2731
  var RankBoundsDef = class extends CanvasSingletonDef10 {
2636
2732
  constructor() {
@@ -2689,64 +2785,64 @@ var RankBounds = new RankBoundsDef();
2689
2785
 
2690
2786
  // src/singletons/ScaleWithZoomState.ts
2691
2787
  import { defineCanvasSingleton } from "@woven-ecs/canvas-store";
2692
- import { field as field31 } from "@woven-ecs/core";
2788
+ import { field as field32 } from "@woven-ecs/core";
2693
2789
  var ScaleWithZoomState = defineCanvasSingleton(
2694
2790
  { name: "scaleWithZoomState" },
2695
2791
  {
2696
2792
  /** Last processed zoom level */
2697
- lastZoom: field31.float64().default(1)
2793
+ lastZoom: field32.float64().default(1)
2698
2794
  }
2699
2795
  );
2700
2796
 
2701
2797
  // src/singletons/ScrollEdgesStateSingleton.ts
2702
- import { field as field32 } from "@woven-ecs/core";
2798
+ import { field as field33 } from "@woven-ecs/core";
2703
2799
  var ScrollEdgesStateSingleton = defineEditorState("scrollEdgesState", {
2704
2800
  /** Current state of the scroll edges state machine */
2705
- state: field32.enum(ScrollEdgesState).default(ScrollEdgesState.Idle),
2801
+ state: field33.enum(ScrollEdgesState).default(ScrollEdgesState.Idle),
2706
2802
  /** Timestamp when pointer first entered the edge zone */
2707
- edgeEnterStartTime: field32.float64().default(0)
2803
+ edgeEnterStartTime: field33.float64().default(0)
2708
2804
  });
2709
2805
 
2710
2806
  // src/singletons/SelectionStateSingleton.ts
2711
- import { field as field33 } from "@woven-ecs/core";
2807
+ import { field as field34 } from "@woven-ecs/core";
2712
2808
  var SelectionStateSingleton = defineEditorState("selectionState", {
2713
- state: field33.enum(SelectionState).default(SelectionState.Idle),
2714
- dragStart: field33.tuple(field33.float64(), 2).default([0, 0]),
2715
- pointingStartClient: field33.tuple(field33.float64(), 2).default([0, 0]),
2716
- pointingStartWorld: field33.tuple(field33.float64(), 2).default([0, 0]),
2717
- draggedEntityStart: field33.tuple(field33.float64(), 2).default([0, 0]),
2718
- draggedEntity: field33.ref(),
2719
- cloneGeneratorSeed: field33.string().max(36).default(""),
2720
- 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()
2721
2817
  });
2722
2818
 
2723
2819
  // src/singletons/Tick.ts
2724
2820
  import { defineCanvasSingleton as defineCanvasSingleton2 } from "@woven-ecs/canvas-store";
2725
- import { field as field34 } from "@woven-ecs/core";
2821
+ import { field as field35 } from "@woven-ecs/core";
2726
2822
  var Tick = defineCanvasSingleton2(
2727
2823
  { name: "tick" },
2728
2824
  {
2729
2825
  /** Current tick number (increments each tick) */
2730
- number: field34.uint32().default(0),
2826
+ number: field35.uint32().default(0),
2731
2827
  /** Time since last tick in seconds */
2732
- delta: field34.float64().default(0),
2828
+ delta: field35.float64().default(0),
2733
2829
  /** Timestamp of current tick in milliseconds (from performance.now()) */
2734
- time: field34.float64().default(0),
2830
+ time: field35.float64().default(0),
2735
2831
  /** Timestamp of previous tick in milliseconds (0 if first tick) */
2736
- lastTime: field34.float64().default(0)
2832
+ lastTime: field35.float64().default(0)
2737
2833
  }
2738
2834
  );
2739
2835
 
2740
2836
  // src/singletons/TransformBoxStateSingleton.ts
2741
- import { field as field35 } from "@woven-ecs/core";
2837
+ import { field as field36 } from "@woven-ecs/core";
2742
2838
  var TransformBoxStateSingleton = defineEditorState("transformBoxState", {
2743
- state: field35.string().max(32).default(TransformBoxState.None),
2744
- transformBoxId: field35.ref()
2839
+ state: field36.string().max(32).default(TransformBoxState.None),
2840
+ transformBoxId: field36.ref()
2745
2841
  });
2746
2842
 
2747
2843
  // src/systems/capture/captureFrameContainmentSystem.ts
2748
- import { Synced as Synced2 } from "@woven-ecs/canvas-store";
2749
- 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";
2750
2846
  import { assign, setup } from "xstate";
2751
2847
 
2752
2848
  // src/EditorSystem.ts
@@ -2835,6 +2931,9 @@ function canBlockEdit(ctx, tag) {
2835
2931
  function canBlockInteract(ctx, tag) {
2836
2932
  return getBlockDef(ctx, tag).interactable;
2837
2933
  }
2934
+ function canBlockSnap(ctx, tag) {
2935
+ return getBlockDef(ctx, tag).snappable;
2936
+ }
2838
2937
  function getBlockResizeMode(ctx, entityId) {
2839
2938
  const block = Block.read(ctx, entityId);
2840
2939
  if (block.resizeMode !== ResizeMode.Default) {
@@ -2857,6 +2956,17 @@ function computeAabb(ctx, entityId, out) {
2857
2956
  }
2858
2957
  }
2859
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
+
2860
2970
  // src/helpers/embed.ts
2861
2971
  function detectEmbedProvider(url) {
2862
2972
  try {
@@ -3070,6 +3180,9 @@ function compareRankChains(a, b) {
3070
3180
  function compareBlockOrder(getBlock, a, b) {
3071
3181
  const stratumDiff = STRATUM_ORDER[a.stratum] - STRATUM_ORDER[b.stratum];
3072
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;
3073
3186
  const chainA = getRankChain(getBlock, a);
3074
3187
  const chainB = getRankChain(getBlock, b);
3075
3188
  return compareRankChains(chainA, chainB);
@@ -3114,12 +3227,30 @@ function isHeldByRemote(ctx, entityId) {
3114
3227
 
3115
3228
  // src/helpers/intersect.ts
3116
3229
  import { Aabb as Aabb3 } from "@woven-canvas/math";
3117
- 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
3118
3249
  var blocksWithAabb = defineQuery3((q) => q.with(Block, Aabb));
3119
3250
  function isPointInsideAncestorFrames(ctx, entityId, point) {
3120
3251
  let current = Block.read(ctx, entityId).parentId;
3121
- while (current !== null && hasComponent6(ctx, current, Block)) {
3122
- if (hasComponent6(ctx, current, Frame)) {
3252
+ while (current !== null && hasComponent7(ctx, current, Block)) {
3253
+ if (hasComponent7(ctx, current, Frame)) {
3123
3254
  if (!Aabb.containsPoint(ctx, current, point)) {
3124
3255
  return false;
3125
3256
  }
@@ -3130,8 +3261,8 @@ function isPointInsideAncestorFrames(ctx, entityId, point) {
3130
3261
  }
3131
3262
  function isAabbInsideAncestorFrames(ctx, entityId, bounds) {
3132
3263
  let current = Block.read(ctx, entityId).parentId;
3133
- while (current !== null && hasComponent6(ctx, current, Block)) {
3134
- if (hasComponent6(ctx, current, Frame)) {
3264
+ while (current !== null && hasComponent7(ctx, current, Block)) {
3265
+ if (hasComponent7(ctx, current, Frame)) {
3135
3266
  const { value: frameAabb } = Aabb.read(ctx, current);
3136
3267
  if (!Aabb3.intersects(bounds, frameAabb)) {
3137
3268
  return false;
@@ -3145,10 +3276,13 @@ function intersectPoint(ctx, point, entityIds) {
3145
3276
  const intersects = [];
3146
3277
  const entities = entityIds ?? blocksWithAabb.current(ctx);
3147
3278
  for (const entityId of entities) {
3279
+ const block = Block.read(ctx, entityId);
3280
+ if (!canBlockInteract(ctx, block.tag)) continue;
3281
+ if (!isLayerInteractive(ctx, block.layerId)) continue;
3148
3282
  if (!Aabb.containsPoint(ctx, entityId, point)) {
3149
3283
  continue;
3150
3284
  }
3151
- if (hasComponent6(ctx, entityId, HitGeometry)) {
3285
+ if (hasComponent7(ctx, entityId, HitGeometry)) {
3152
3286
  if (!HitGeometry.containsPointWorld(ctx, entityId, point)) {
3153
3287
  continue;
3154
3288
  }
@@ -3168,12 +3302,15 @@ function intersectAabb(ctx, bounds, entityIds) {
3168
3302
  const intersecting = [];
3169
3303
  const entities = entityIds ?? blocksWithAabb.current(ctx);
3170
3304
  for (const entityId of entities) {
3305
+ const block = Block.read(ctx, entityId);
3306
+ if (!canBlockInteract(ctx, block.tag)) continue;
3307
+ if (!isLayerInteractive(ctx, block.layerId)) continue;
3171
3308
  const { value: entityAabb } = Aabb.read(ctx, entityId);
3172
3309
  if (!Aabb3.intersects(bounds, entityAabb)) {
3173
3310
  continue;
3174
3311
  }
3175
3312
  if (!Aabb3.contains(bounds, entityAabb)) {
3176
- if (hasComponent6(ctx, entityId, HitGeometry)) {
3313
+ if (hasComponent7(ctx, entityId, HitGeometry)) {
3177
3314
  if (!HitGeometry.intersectsAabbWorld(ctx, entityId, bounds)) {
3178
3315
  continue;
3179
3316
  }
@@ -3192,12 +3329,13 @@ function intersectAabb(ctx, bounds, entityIds) {
3192
3329
  }
3193
3330
  function sortByRankDescending(ctx, entityIds) {
3194
3331
  const getBlock = (id) => {
3195
- if (!hasComponent6(ctx, id, Block)) return null;
3332
+ if (!hasComponent7(ctx, id, Block)) return null;
3196
3333
  const block = Block.read(ctx, id);
3197
3334
  return {
3198
3335
  rank: block.rank,
3199
3336
  stratum: getBlockDef(ctx, block.tag).stratum,
3200
- parentId: block.parentId
3337
+ parentId: block.parentId,
3338
+ layerRank: resolveLayerRank(ctx, id)
3201
3339
  };
3202
3340
  };
3203
3341
  const entries = entityIds.map((id) => ({
@@ -3213,13 +3351,15 @@ function getTopmostBlockAtPoint(ctx, point) {
3213
3351
  }
3214
3352
 
3215
3353
  // src/helpers/intersectCapsule.ts
3216
- import { Aabb as AabbNs, Arc as Arc2, Capsule as Capsule2, Mat2 as Mat22 } from "@woven-canvas/math";
3217
- 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";
3218
3356
  var _uvToWorldMatrix2 = [1, 0, 0, 1, 0, 0];
3357
+ var _worldPolygon2 = new Float32Array(MAX_HIT_POLYGON_POINTS * 2);
3358
+ var _polyPt2 = [0, 0];
3219
3359
  function isCapsuleInsideAncestorFrames(ctx, entityId, capsule) {
3220
3360
  let current = Block.read(ctx, entityId).parentId;
3221
- while (current !== null && hasComponent7(ctx, current, Block)) {
3222
- if (hasComponent7(ctx, current, Frame)) {
3361
+ while (current !== null && hasComponent8(ctx, current, Block)) {
3362
+ if (hasComponent8(ctx, current, Frame)) {
3223
3363
  const { value: frameAabb } = Aabb.read(ctx, current);
3224
3364
  if (!Capsule2.intersectsAabb(capsule, frameAabb)) {
3225
3365
  return false;
@@ -3237,7 +3377,7 @@ function intersectCapsule(ctx, capsule, entityIds) {
3237
3377
  if (!AabbNs.intersects(capsuleBounds, aabb.value)) {
3238
3378
  continue;
3239
3379
  }
3240
- if (hasComponent7(ctx, entityId, HitGeometry)) {
3380
+ if (hasComponent8(ctx, entityId, HitGeometry)) {
3241
3381
  if (!intersectsCapsuleHitGeometry(ctx, entityId, capsule)) {
3242
3382
  continue;
3243
3383
  }
@@ -3284,12 +3424,25 @@ function intersectsCapsuleHitGeometry(ctx, entityId, capsule) {
3284
3424
  return true;
3285
3425
  }
3286
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
+ }
3287
3440
  return false;
3288
3441
  }
3289
3442
 
3290
3443
  // src/helpers/ref.ts
3291
- import { Synced } from "@woven-ecs/canvas-store";
3292
- 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";
3293
3446
  function convertRefsToUuids(ctx, schema, data) {
3294
3447
  const result = { ...data };
3295
3448
  for (const [fieldName, fieldBuilder] of Object.entries(schema)) {
@@ -3302,8 +3455,8 @@ function convertRefsToUuids(ctx, schema, data) {
3302
3455
  }
3303
3456
  function getEntityUuid(ctx, entityId) {
3304
3457
  if (entityId == null) return null;
3305
- if (!hasComponent8(ctx, entityId, Synced)) return null;
3306
- return Synced.read(ctx, entityId).id;
3458
+ if (!hasComponent9(ctx, entityId, Synced2)) return null;
3459
+ return Synced2.read(ctx, entityId).id;
3307
3460
  }
3308
3461
  function getRefFieldNames(schema) {
3309
3462
  const refFields = [];
@@ -3328,29 +3481,29 @@ function resolveRefFields(ctx, entityId, componentDef, componentData, uuidToEnti
3328
3481
 
3329
3482
  // src/helpers/select.ts
3330
3483
  import {
3331
- addComponent as addComponent2,
3484
+ addComponent as addComponent3,
3332
3485
  defineQuery as defineQuery4,
3333
3486
  getResources as getResources4,
3334
- hasComponent as hasComponent9,
3487
+ hasComponent as hasComponent10,
3335
3488
  removeComponent
3336
3489
  } from "@woven-ecs/core";
3337
3490
  var selectedQuery = defineQuery4((q) => q.with(Selected));
3338
3491
  var blockQuery = defineQuery4((q) => q.with(Block));
3339
3492
  function selectBlock(ctx, entityId) {
3340
- if (hasComponent9(ctx, entityId, Selected)) return;
3493
+ if (hasComponent10(ctx, entityId, Selected)) return;
3341
3494
  if (isHeldByRemote(ctx, entityId)) return;
3342
- if (hasComponent9(ctx, entityId, Block)) {
3495
+ if (hasComponent10(ctx, entityId, Block)) {
3343
3496
  const block = Block.read(ctx, entityId);
3344
3497
  if (!canBlockInteract(ctx, block.tag)) return;
3345
3498
  }
3346
3499
  const { sessionId } = getResources4(ctx);
3347
- addComponent2(ctx, entityId, Selected, {});
3348
- if (!hasComponent9(ctx, entityId, Held)) {
3349
- addComponent2(ctx, entityId, Held, { sessionId });
3500
+ addComponent3(ctx, entityId, Selected, {});
3501
+ if (!hasComponent10(ctx, entityId, Held)) {
3502
+ addComponent3(ctx, entityId, Held, { sessionId });
3350
3503
  }
3351
3504
  }
3352
3505
  function deselectBlock(ctx, entityId) {
3353
- if (!hasComponent9(ctx, entityId, Selected)) return;
3506
+ if (!hasComponent10(ctx, entityId, Selected)) return;
3354
3507
  removeComponent(ctx, entityId, Selected);
3355
3508
  removeComponent(ctx, entityId, Held);
3356
3509
  }
@@ -3564,13 +3717,13 @@ function spawnAssignments(ctx, targetFrame) {
3564
3717
  const draggedEntity = getDraggedEntity(ctx);
3565
3718
  if (draggedEntity === null) return;
3566
3719
  const assignments = [];
3567
- if (hasComponent10(ctx, draggedEntity, TransformBox)) {
3720
+ if (hasComponent11(ctx, draggedEntity, TransformBox)) {
3568
3721
  for (const blockId of selectedQuery2.current(ctx)) {
3569
3722
  if (targetFrame !== null && blockId === targetFrame) continue;
3570
3723
  if (targetFrame !== null && isAncestor(ctx, targetFrame, blockId)) continue;
3571
3724
  assignments.push({ entityId: blockId, frameId: targetFrame });
3572
3725
  }
3573
- } else if (hasComponent10(ctx, draggedEntity, Synced2)) {
3726
+ } else if (hasComponent11(ctx, draggedEntity, Synced3)) {
3574
3727
  if (targetFrame !== null && draggedEntity === targetFrame) return;
3575
3728
  if (targetFrame !== null && isAncestor(ctx, targetFrame, draggedEntity)) return;
3576
3729
  assignments.push({ entityId: draggedEntity, frameId: targetFrame });
@@ -3695,8 +3848,9 @@ var hoverCursorSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
3695
3848
  });
3696
3849
 
3697
3850
  // src/systems/capture/keybindSystem.ts
3698
- 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";
3699
3852
  var keybindSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
3853
+ if (isReadonly(ctx)) return;
3700
3854
  const keyboard = Keyboard.read(ctx);
3701
3855
  const { editor } = getResources6(ctx);
3702
3856
  for (const keybind of editor.keybinds) {
@@ -3704,8 +3858,8 @@ var keybindSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
3704
3858
  triggered &&= !!keybind.mod === keyboard.modDown;
3705
3859
  triggered &&= !!keybind.shift === keyboard.shiftDown;
3706
3860
  if (triggered) {
3707
- const eid = createEntity2(ctx);
3708
- addComponent3(ctx, eid, CommandMarker, { name: keybind.command });
3861
+ const eid = createEntity3(ctx);
3862
+ addComponent4(ctx, eid, CommandMarker, { name: keybind.command });
3709
3863
  break;
3710
3864
  }
3711
3865
  }
@@ -3829,7 +3983,7 @@ var scrollEdgesSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
3829
3983
  });
3830
3984
 
3831
3985
  // src/systems/capture/transformBoxSystem.ts
3832
- 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";
3833
3987
  import { and, assign as assign3, not as not2, raise, setup as setup3 } from "xstate";
3834
3988
  var selectedBlocksQuery = defineQuery9((q) => q.with(Block).tracking(Selected));
3835
3989
  var transformBoxMachine = setup3({
@@ -3844,12 +3998,12 @@ var transformBoxMachine = setup3({
3844
3998
  isOverTransformBox: ({ event }) => {
3845
3999
  if (!("intersects" in event)) return false;
3846
4000
  if (event.intersects.length === 0) return false;
3847
- return hasComponent11(event.ctx, event.intersects[0], TransformBox);
4001
+ return hasComponent12(event.ctx, event.intersects[0], TransformBox);
3848
4002
  },
3849
4003
  isOverTransformHandle: ({ event }) => {
3850
4004
  if (!("intersects" in event)) return false;
3851
4005
  if (event.intersects.length === 0) return false;
3852
- return hasComponent11(event.ctx, event.intersects[0], TransformHandle);
4006
+ return hasComponent12(event.ctx, event.intersects[0], TransformHandle);
3853
4007
  },
3854
4008
  selectionIsTransformable: ({ event }) => {
3855
4009
  if (event.type !== "selectionChanged") return false;
@@ -3861,7 +4015,7 @@ var transformBoxMachine = setup3({
3861
4015
  hasEditAfterPlacing: ({ event }) => {
3862
4016
  if (event.type !== "selectionChanged") return false;
3863
4017
  if (event.selectedEntityIds.length !== 1) return false;
3864
- return hasComponent11(event.ctx, event.selectedEntityIds[0], EditAfterPlacing);
4018
+ return hasComponent12(event.ctx, event.selectedEntityIds[0], EditAfterPlacing);
3865
4019
  }
3866
4020
  },
3867
4021
  actions: {
@@ -3871,7 +4025,7 @@ var transformBoxMachine = setup3({
3871
4025
  transformBoxId: context.transformBoxId
3872
4026
  });
3873
4027
  }
3874
- const transformBoxId = createEntity3(event.ctx);
4028
+ const transformBoxId = createEntity4(event.ctx);
3875
4029
  AddTransformBox.spawn(event.ctx, {
3876
4030
  transformBoxId,
3877
4031
  skipHandles: params?.skipHandles
@@ -3914,7 +4068,7 @@ var transformBoxMachine = setup3({
3914
4068
  removeEditAfterPlacing: ({ event }) => {
3915
4069
  if (event.type !== "selectionChanged") return;
3916
4070
  for (const entityId of event.selectedEntityIds) {
3917
- if (hasComponent11(event.ctx, entityId, EditAfterPlacing)) {
4071
+ if (hasComponent12(event.ctx, entityId, EditAfterPlacing)) {
3918
4072
  removeComponent2(event.ctx, entityId, EditAfterPlacing);
3919
4073
  }
3920
4074
  }
@@ -4209,7 +4363,7 @@ function normalizeWheelDelta(deltaY, deltaMode) {
4209
4363
  }
4210
4364
 
4211
4365
  // src/systems/input/pointerSystem.ts
4212
- 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";
4213
4367
  var instanceState3 = /* @__PURE__ */ new WeakMap();
4214
4368
  function attachPointerListeners(domElement) {
4215
4369
  if (instanceState3.has(domElement)) return;
@@ -4316,8 +4470,8 @@ var pointerSystem = defineEditorSystem({ phase: "input" }, (ctx) => {
4316
4470
  switch (event.type) {
4317
4471
  case "pointerdown": {
4318
4472
  const position = [event.clientX - screen.left, event.clientY - screen.top];
4319
- const entityId = createEntity4(ctx);
4320
- addComponent4(ctx, entityId, Pointer, {
4473
+ const entityId = createEntity5(ctx);
4474
+ addComponent5(ctx, entityId, Pointer, {
4321
4475
  pointerId: event.pointerId,
4322
4476
  position,
4323
4477
  downPosition: position,
@@ -4447,8 +4601,9 @@ var cursorSystem = defineEditorSystem({ phase: "render", priority: -100 }, (ctx)
4447
4601
  // src/systems/postRender/presenceSystem.ts
4448
4602
  import { defineQuery as defineQuery11 } from "@woven-ecs/core";
4449
4603
  var pointerQuery2 = defineQuery11((q) => q.tracking(Pointer));
4604
+ var cameraChangedQuery = defineQuery11((q) => q.tracking(Camera));
4450
4605
  var presenceSystem = defineEditorSystem({ phase: "render", priority: -100 }, (ctx) => {
4451
- if (Mouse.didMove(ctx)) {
4606
+ if (Mouse.didMove(ctx) || cameraChangedQuery.changed(ctx).length > 0) {
4452
4607
  const mouse = Mouse.read(ctx);
4453
4608
  updateUserPosition(ctx, mouse.position);
4454
4609
  return;
@@ -4478,11 +4633,11 @@ function updateUserPosition(ctx, position) {
4478
4633
  // src/systems/postUpdate/transformBoxSystem.ts
4479
4634
  import { Rect as Rect2, Vec2 as Vec23 } from "@woven-canvas/math";
4480
4635
  import {
4481
- addComponent as addComponent5,
4482
- createEntity as createEntity5,
4636
+ addComponent as addComponent6,
4637
+ createEntity as createEntity6,
4483
4638
  defineQuery as defineQuery12,
4484
4639
  getBackrefs as getBackrefs2,
4485
- hasComponent as hasComponent12,
4640
+ hasComponent as hasComponent13,
4486
4641
  isAlive,
4487
4642
  removeComponent as removeComponent3,
4488
4643
  removeEntity as removeEntity4
@@ -4515,7 +4670,7 @@ var transformBoxSystem2 = defineEditorSystem({ phase: "update", priority: -100 }
4515
4670
  on(ctx, EndTransformBoxEdit, endTransformBoxEdit);
4516
4671
  });
4517
4672
  function removeTransformBox(ctx, transformBoxId) {
4518
- if (!hasComponent12(ctx, transformBoxId, TransformBox)) {
4673
+ if (!hasComponent13(ctx, transformBoxId, TransformBox)) {
4519
4674
  endTransformBoxEdit(ctx);
4520
4675
  return;
4521
4676
  }
@@ -4527,15 +4682,15 @@ function removeTransformBox(ctx, transformBoxId) {
4527
4682
  endTransformBoxEdit(ctx);
4528
4683
  }
4529
4684
  function addTransformBox(ctx, transformBoxId, skipHandles) {
4530
- addComponent5(ctx, transformBoxId, Block, {
4685
+ addComponent6(ctx, transformBoxId, Block, {
4531
4686
  tag: "transform-box",
4532
4687
  position: [0, 0],
4533
4688
  size: [0, 0],
4534
4689
  rotateZ: 0,
4535
4690
  rank: TRANSFORM_BOX_RANK
4536
4691
  });
4537
- addComponent5(ctx, transformBoxId, TransformBox, {});
4538
- addComponent5(ctx, transformBoxId, DragStart, {
4692
+ addComponent6(ctx, transformBoxId, TransformBox, {});
4693
+ addComponent6(ctx, transformBoxId, DragStart, {
4539
4694
  position: [0, 0],
4540
4695
  size: [0, 0],
4541
4696
  rotateZ: 0,
@@ -4572,7 +4727,7 @@ function updateTransformBox(ctx, transformBoxId) {
4572
4727
  const selectionState = SelectionStateSingleton.read(ctx);
4573
4728
  const isDragging = selectionState.state === SelectionState.Dragging;
4574
4729
  if (!isDragging) {
4575
- if (hasComponent12(ctx, transformBoxId, DragStart)) {
4730
+ if (hasComponent13(ctx, transformBoxId, DragStart)) {
4576
4731
  const dragStart = DragStart.write(ctx, transformBoxId);
4577
4732
  dragStart.position = boxBlock.position;
4578
4733
  dragStart.size = boxBlock.size;
@@ -4581,9 +4736,9 @@ function updateTransformBox(ctx, transformBoxId) {
4581
4736
  for (const entityId of selectedBlocks) {
4582
4737
  const block = Block.read(ctx, entityId);
4583
4738
  const worldPos = Block.getWorldPosition(ctx, entityId);
4584
- const fontSize = hasComponent12(ctx, entityId, Text) ? Text.read(ctx, entityId).fontSizePx : 16;
4585
- if (!hasComponent12(ctx, entityId, DragStart)) {
4586
- 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, {
4587
4742
  position: worldPos,
4588
4743
  size: block.size,
4589
4744
  rotateZ: block.rotateZ,
@@ -4765,11 +4920,11 @@ function addOrUpdateTransformHandles(ctx, transformBoxId) {
4765
4920
  const finalLeft = rotatedCenter[0] - def.width / 2;
4766
4921
  const finalTop = rotatedCenter[1] - def.height / 2;
4767
4922
  if (!handleId) {
4768
- handleId = createEntity5(ctx);
4769
- addComponent5(ctx, handleId, Block);
4770
- addComponent5(ctx, handleId, TransformHandle);
4771
- addComponent5(ctx, handleId, DragStart);
4772
- 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);
4773
4928
  }
4774
4929
  const block = Block.write(ctx, handleId);
4775
4930
  block.tag = def.tag;
@@ -4850,11 +5005,11 @@ function addTapeHandles(ctx, transformBoxId, left, top, width, height, rotateZ,
4850
5005
  const finalLeft = rotatedCenter[0] - def.width / 2;
4851
5006
  const finalTop = rotatedCenter[1] - def.height / 2;
4852
5007
  if (!handleId) {
4853
- handleId = createEntity5(ctx);
4854
- addComponent5(ctx, handleId, Block);
4855
- addComponent5(ctx, handleId, TransformHandle);
4856
- addComponent5(ctx, handleId, DragStart);
4857
- 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);
4858
5013
  }
4859
5014
  const block = Block.write(ctx, handleId);
4860
5015
  block.tag = def.tag;
@@ -4886,9 +5041,9 @@ function addTapeHandles(ctx, transformBoxId, left, top, width, height, rotateZ,
4886
5041
  }
4887
5042
  }
4888
5043
  function hideTransformBox(ctx, transformBoxId) {
4889
- if (!hasComponent12(ctx, transformBoxId, TransformBox)) return;
4890
- if (!hasComponent12(ctx, transformBoxId, Opacity)) {
4891
- 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 });
4892
5047
  } else {
4893
5048
  const opacity = Opacity.write(ctx, transformBoxId);
4894
5049
  opacity.value = 0;
@@ -4896,8 +5051,8 @@ function hideTransformBox(ctx, transformBoxId) {
4896
5051
  const handles = getBackrefs2(ctx, transformBoxId, TransformHandle, "transformBox");
4897
5052
  for (const handleId of handles) {
4898
5053
  if (!isAlive(ctx, handleId)) continue;
4899
- if (!hasComponent12(ctx, handleId, Opacity)) {
4900
- addComponent5(ctx, handleId, Opacity, { value: 0 });
5054
+ if (!hasComponent13(ctx, handleId, Opacity)) {
5055
+ addComponent6(ctx, handleId, Opacity, { value: 0 });
4901
5056
  } else {
4902
5057
  const opacity = Opacity.write(ctx, handleId);
4903
5058
  opacity.value = 0;
@@ -4905,40 +5060,40 @@ function hideTransformBox(ctx, transformBoxId) {
4905
5060
  }
4906
5061
  }
4907
5062
  function showTransformBox(ctx, transformBoxId) {
4908
- if (!hasComponent12(ctx, transformBoxId, TransformBox)) return;
5063
+ if (!hasComponent13(ctx, transformBoxId, TransformBox)) return;
4909
5064
  const selectedBlocks = selectedBlocksQuery2.current(ctx);
4910
5065
  if (selectedBlocks.length === 0) return;
4911
5066
  if (selectedBlocks.length === 1) {
4912
5067
  if (getBlockResizeMode(ctx, selectedBlocks[0]) === ResizeMode.GroupOnly) return;
4913
5068
  }
4914
- if (hasComponent12(ctx, transformBoxId, Opacity)) {
5069
+ if (hasComponent13(ctx, transformBoxId, Opacity)) {
4915
5070
  removeComponent3(ctx, transformBoxId, Opacity);
4916
5071
  }
4917
5072
  const handles = getBackrefs2(ctx, transformBoxId, TransformHandle, "transformBox");
4918
5073
  for (const handleId of handles) {
4919
5074
  if (!isAlive(ctx, handleId)) continue;
4920
- if (hasComponent12(ctx, handleId, Opacity)) {
5075
+ if (hasComponent13(ctx, handleId, Opacity)) {
4921
5076
  removeComponent3(ctx, handleId, Opacity);
4922
5077
  }
4923
5078
  }
4924
5079
  updateTransformBox(ctx, transformBoxId);
4925
5080
  }
4926
5081
  function startTransformBoxEdit(ctx, transformBoxId) {
4927
- if (hasComponent12(ctx, transformBoxId, TransformBox)) {
5082
+ if (hasComponent13(ctx, transformBoxId, TransformBox)) {
4928
5083
  const handles = getBackrefs2(ctx, transformBoxId, TransformHandle, "transformBox");
4929
5084
  for (const handleId of handles) {
4930
5085
  removeEntity4(ctx, handleId);
4931
5086
  }
4932
5087
  }
4933
5088
  for (const blockId of selectedBlocksQuery2.current(ctx)) {
4934
- if (!hasComponent12(ctx, blockId, Edited)) {
4935
- addComponent5(ctx, blockId, Edited, {});
5089
+ if (!hasComponent13(ctx, blockId, Edited)) {
5090
+ addComponent6(ctx, blockId, Edited, {});
4936
5091
  }
4937
5092
  }
4938
5093
  }
4939
5094
  function endTransformBoxEdit(ctx) {
4940
5095
  for (const blockId of editedBlocksQuery.current(ctx)) {
4941
- if (hasComponent12(ctx, blockId, Edited)) {
5096
+ if (hasComponent13(ctx, blockId, Edited)) {
4942
5097
  removeComponent3(ctx, blockId, Edited);
4943
5098
  }
4944
5099
  }
@@ -4946,11 +5101,11 @@ function endTransformBoxEdit(ctx) {
4946
5101
 
4947
5102
  // src/systems/preCapture/intersectSystem.ts
4948
5103
  import {
4949
- addComponent as addComponent6,
5104
+ addComponent as addComponent7,
4950
5105
  defineQuery as defineQuery13,
4951
5106
  getBackrefs as getBackrefs3,
4952
5107
  getResources as getResources12,
4953
- hasComponent as hasComponent13,
5108
+ hasComponent as hasComponent14,
4954
5109
  removeComponent as removeComponent4
4955
5110
  } from "@woven-ecs/core";
4956
5111
  var blocksChanged = defineQuery13((q) => q.tracking(Block));
@@ -4960,7 +5115,7 @@ var hoveredQuery = defineQuery13((q) => q.with(Hovered));
4960
5115
  var pointerQuery3 = defineQuery13((q) => q.with(Pointer));
4961
5116
  function clearHovered(ctx) {
4962
5117
  for (const entityId of hoveredQuery.current(ctx)) {
4963
- if (hasComponent13(ctx, entityId, Hovered)) {
5118
+ if (hasComponent14(ctx, entityId, Hovered)) {
4964
5119
  removeComponent4(ctx, entityId, Hovered);
4965
5120
  }
4966
5121
  }
@@ -4968,6 +5123,10 @@ function clearHovered(ctx) {
4968
5123
  function findValidHoverTarget(ctx, intersected) {
4969
5124
  for (const entityId of intersected) {
4970
5125
  if (isHeldByRemote(ctx, entityId)) return void 0;
5126
+ if (hasComponent14(ctx, entityId, Block)) {
5127
+ const block = Block.read(ctx, entityId);
5128
+ if (!canBlockInteract(ctx, block.tag)) continue;
5129
+ }
4971
5130
  return entityId;
4972
5131
  }
4973
5132
  return void 0;
@@ -4979,7 +5138,7 @@ function updateHovered(ctx, intersected) {
4979
5138
  if (!selectToolActive) return;
4980
5139
  const newHoveredId = findValidHoverTarget(ctx, intersected);
4981
5140
  if (newHoveredId !== void 0) {
4982
- addComponent6(ctx, newHoveredId, Hovered, {});
5141
+ addComponent7(ctx, newHoveredId, Hovered, {});
4983
5142
  }
4984
5143
  }
4985
5144
  function arraysEqual(a, b) {
@@ -5018,18 +5177,14 @@ var intersectSystem = defineEditorSystem({ phase: "capture", priority: 100 }, (c
5018
5177
  addDescendants(ctx, entityId, changed);
5019
5178
  }
5020
5179
  for (const entityId of added) {
5021
- if (hasComponent13(ctx, entityId, Block)) {
5022
- const block = Block.read(ctx, entityId);
5023
- if (!canBlockInteract(ctx, block.tag)) continue;
5024
- }
5025
- if (!hasComponent13(ctx, entityId, Aabb)) {
5026
- 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] });
5027
5182
  }
5028
5183
  const aabb = Aabb.write(ctx, entityId);
5029
5184
  computeAabb(ctx, entityId, aabb.value);
5030
5185
  }
5031
5186
  for (const entityId of changed) {
5032
- if (!hasComponent13(ctx, entityId, Aabb)) continue;
5187
+ if (!hasComponent14(ctx, entityId, Aabb)) continue;
5033
5188
  const aabb = Aabb.write(ctx, entityId);
5034
5189
  computeAabb(ctx, entityId, aabb.value);
5035
5190
  }
@@ -5078,12 +5233,21 @@ var intersectSystem = defineEditorSystem({ phase: "capture", priority: 100 }, (c
5078
5233
 
5079
5234
  // src/systems/preCapture/selectSystem.ts
5080
5235
  import { Vec2 as Vec2Ns } from "@woven-canvas/math";
5081
- import { Synced as Synced3 } from "@woven-ecs/canvas-store";
5082
- 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";
5083
5238
  import { and as and2, assign as assign4, not as not3, setup as setup4 } from "xstate";
5084
5239
  var POINTING_THRESHOLD = 4;
5085
- var selectedBlocksQuery3 = defineQuery14((q) => q.with(Block, Selected, Synced3));
5240
+ var selectedBlocksQuery3 = defineQuery14((q) => q.with(Block, Selected, Synced4));
5086
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
+ }
5087
5251
  var selectionMachine = setup4({
5088
5252
  types: {
5089
5253
  context: {},
@@ -5099,13 +5263,13 @@ var selectionMachine = setup4({
5099
5263
  },
5100
5264
  hasDraggedEntity: ({ context, event }) => {
5101
5265
  if (context.draggedEntity === null) return false;
5102
- return hasComponent14(event.ctx, context.draggedEntity, Synced3);
5266
+ return hasComponent15(event.ctx, context.draggedEntity, Synced4);
5103
5267
  },
5104
5268
  isOverSyncedBlock: ({ event }) => {
5105
5269
  const ctx = event.ctx;
5106
5270
  for (const entityId of event.intersects) {
5107
- if (hasComponent14(ctx, entityId, Synced3)) {
5108
- if (hasComponent14(ctx, entityId, Block)) {
5271
+ if (hasComponent15(ctx, entityId, Synced4)) {
5272
+ if (hasComponent15(ctx, entityId, Block)) {
5109
5273
  const block = Block.read(ctx, entityId);
5110
5274
  if (!canBlockInteract(ctx, block.tag)) continue;
5111
5275
  }
@@ -5142,7 +5306,7 @@ var selectionMachine = setup4({
5142
5306
  },
5143
5307
  setDragCursor: ({ event, context }) => {
5144
5308
  if (!context.draggedEntity) return;
5145
- if (hasComponent14(event.ctx, context.draggedEntity, TransformHandle)) {
5309
+ if (hasComponent15(event.ctx, context.draggedEntity, TransformHandle)) {
5146
5310
  const handle = TransformHandle.read(event.ctx, context.draggedEntity);
5147
5311
  const transformBoxBlock = handle.transformBox !== null ? Block.read(event.ctx, handle.transformBox) : void 0;
5148
5312
  SetCursor.spawn(event.ctx, {
@@ -5202,7 +5366,7 @@ var selectionMachine = setup4({
5202
5366
  if (context.draggedEntity === null) return false;
5203
5367
  let left = context.draggedEntityStart[0] + event.worldPosition[0] - context.dragStart[0];
5204
5368
  let top = context.draggedEntityStart[1] + event.worldPosition[1] - context.dragStart[1];
5205
- 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);
5206
5370
  if (event.shiftDown && draggingSynced) {
5207
5371
  const dx = Math.abs(event.worldPosition[0] - context.dragStart[0]);
5208
5372
  const dy = Math.abs(event.worldPosition[1] - context.dragStart[1]);
@@ -5212,7 +5376,7 @@ var selectionMachine = setup4({
5212
5376
  left = context.draggedEntityStart[0];
5213
5377
  }
5214
5378
  }
5215
- if (draggingSynced) {
5379
+ if (draggingSynced && isDragSnappable(ctx, context.draggedEntity)) {
5216
5380
  left = Grid.snapX(ctx, left);
5217
5381
  top = Grid.snapY(ctx, top);
5218
5382
  }
@@ -5225,7 +5389,7 @@ var selectionMachine = setup4({
5225
5389
  const dx = context.draggedEntityStart[0] - left;
5226
5390
  const dy = context.draggedEntityStart[1] - top;
5227
5391
  isCloning = true;
5228
- const isTransformBox = hasComponent14(ctx, context.draggedEntity, TransformBox);
5392
+ const isTransformBox = hasComponent15(ctx, context.draggedEntity, TransformBox);
5229
5393
  const entityIds = isTransformBox ? Array.from(selectedBlocksQuery3.current(ctx)) : [context.draggedEntity];
5230
5394
  CloneEntities.spawn(ctx, {
5231
5395
  entityIds,
@@ -5233,7 +5397,7 @@ var selectionMachine = setup4({
5233
5397
  seed: context.cloneGeneratorSeed
5234
5398
  });
5235
5399
  } else if (!event.altDown && context.isCloning) {
5236
- const isTransformBox = hasComponent14(ctx, context.draggedEntity, TransformBox);
5400
+ const isTransformBox = hasComponent15(ctx, context.draggedEntity, TransformBox);
5237
5401
  const entityIds = isTransformBox ? Array.from(selectedBlocksQuery3.current(ctx)) : [context.draggedEntity];
5238
5402
  UncloneEntities.spawn(ctx, {
5239
5403
  entityIds,
@@ -5258,8 +5422,8 @@ var selectionMachine = setup4({
5258
5422
  const ctx = event.ctx;
5259
5423
  let intersectId;
5260
5424
  for (const entityId of event.intersects) {
5261
- if (hasComponent14(ctx, entityId, Synced3)) {
5262
- if (hasComponent14(ctx, entityId, Block)) {
5425
+ if (hasComponent15(ctx, entityId, Synced4)) {
5426
+ if (hasComponent15(ctx, entityId, Block)) {
5263
5427
  const block = Block.read(ctx, entityId);
5264
5428
  if (!canBlockInteract(ctx, block.tag)) continue;
5265
5429
  }
@@ -5279,7 +5443,7 @@ var selectionMachine = setup4({
5279
5443
  },
5280
5444
  selectDragged: ({ context, event }) => {
5281
5445
  if (context.draggedEntity === null) return;
5282
- if (!hasComponent14(event.ctx, context.draggedEntity, Synced3)) return;
5446
+ if (!hasComponent15(event.ctx, context.draggedEntity, Synced4)) return;
5283
5447
  SelectBlock.spawn(event.ctx, {
5284
5448
  entityId: context.draggedEntity,
5285
5449
  deselectOthers: true
@@ -5291,9 +5455,9 @@ var selectionMachine = setup4({
5291
5455
  },
5292
5456
  deselectAllIfDraggedNotSelected: ({ context, event }) => {
5293
5457
  if (context.draggedEntity === null) return;
5294
- if (hasComponent14(event.ctx, context.draggedEntity, Selected)) return;
5295
- if (hasComponent14(event.ctx, context.draggedEntity, TransformBox)) return;
5296
- 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;
5297
5461
  DeselectAll.spawn(event.ctx, void 0);
5298
5462
  }
5299
5463
  }
@@ -5405,6 +5569,7 @@ var selectionMachine = setup4({
5405
5569
  }
5406
5570
  });
5407
5571
  var selectSystem = defineEditorSystem({ phase: "capture", priority: 100 }, (ctx) => {
5572
+ if (isReadonly(ctx)) return;
5408
5573
  let buttons = Controls.getButtons(ctx, "select");
5409
5574
  const state = SelectionStateSingleton.read(ctx);
5410
5575
  if (state.state === SelectionState.Dragging && buttons.length === 0) {
@@ -5416,13 +5581,14 @@ var selectSystem = defineEditorSystem({ phase: "capture", priority: 100 }, (ctx)
5416
5581
  });
5417
5582
 
5418
5583
  // src/systems/preInput/rankBoundsSystem.ts
5419
- import { Synced as Synced4 } from "@woven-ecs/canvas-store";
5584
+ import { Synced as Synced5 } from "@woven-ecs/canvas-store";
5420
5585
  import { defineQuery as defineQuery15 } from "@woven-ecs/core";
5421
- var blocksQuery = defineQuery15((q) => q.with(Synced4, Block));
5586
+ var blocksQuery = defineQuery15((q) => q.with(Synced5, Block));
5422
5587
  var rankBoundsSystem = defineEditorSystem({ phase: "input", priority: 100 }, (ctx) => {
5423
5588
  const added2 = blocksQuery.added(ctx);
5424
5589
  for (const entityId of added2) {
5425
5590
  const block = Block.read(ctx, entityId);
5591
+ if (getBlockDef(ctx, block.tag).excludeFromRankBounds) continue;
5426
5592
  if (block.rank === "") {
5427
5593
  const writableBlock = Block.write(ctx, entityId);
5428
5594
  writableBlock.rank = RankBounds.genNext(ctx);
@@ -5434,11 +5600,11 @@ var rankBoundsSystem = defineEditorSystem({ phase: "input", priority: 100 }, (ct
5434
5600
 
5435
5601
  // src/systems/preRender/canSeeBlocksSystem.ts
5436
5602
  import { Aabb as AabbMath2 } from "@woven-canvas/math";
5437
- import { Synced as Synced5 } from "@woven-ecs/canvas-store";
5438
- 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";
5439
5605
  var camerasQuery = defineQuery16((q) => q.tracking(Camera));
5440
- var blocksQuery2 = defineQuery16((q) => q.with(Synced5, Block, Aabb));
5441
- 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));
5442
5608
  var _cameraAabb = [0, 0, 0, 0];
5443
5609
  var canSeeBlocksSystem = defineEditorSystem({ phase: "render", priority: 90 }, (ctx) => {
5444
5610
  if (camerasQuery.changed(ctx).length === 0 && syncedBlocksQuery.addedOrRemoved(ctx).length === 0) {
@@ -5447,7 +5613,7 @@ var canSeeBlocksSystem = defineEditorSystem({ phase: "render", priority: 90 }, (
5447
5613
  const camera = Camera.read(ctx);
5448
5614
  const cameraAabb = Camera.getAabb(ctx, _cameraAabb);
5449
5615
  if (camera.canSeeBlocks && camera.lastSeenBlock !== null) {
5450
- if (hasComponent15(ctx, camera.lastSeenBlock, Aabb)) {
5616
+ if (hasComponent16(ctx, camera.lastSeenBlock, Aabb)) {
5451
5617
  const aabb = Aabb.read(ctx, camera.lastSeenBlock);
5452
5618
  if (AabbMath2.intersects(cameraAabb, aabb.value)) {
5453
5619
  return;
@@ -5521,23 +5687,23 @@ function scaleBlock(ctx, entityId, zoom) {
5521
5687
 
5522
5688
  // src/systems/update/blockSystem.ts
5523
5689
  import { Vec2 as Vec25 } from "@woven-canvas/math";
5524
- import { Synced as Synced6 } from "@woven-ecs/canvas-store";
5690
+ import { Synced as Synced7 } from "@woven-ecs/canvas-store";
5525
5691
  import {
5526
- addComponent as addComponent7,
5527
- createEntity as createEntity6,
5692
+ addComponent as addComponent8,
5693
+ createEntity as createEntity7,
5528
5694
  defineQuery as defineQuery18,
5529
5695
  getBackrefs as getBackrefs4,
5530
5696
  getResources as getResources13,
5531
- hasComponent as hasComponent16,
5697
+ hasComponent as hasComponent17,
5532
5698
  isAlive as isAlive2,
5533
5699
  removeComponent as removeComponent5,
5534
5700
  removeEntity as removeEntity5
5535
5701
  } from "@woven-ecs/core";
5536
5702
  var selectedBlocksQuery4 = defineQuery18((q) => q.with(Block, Selected));
5537
- var syncedBlocksQuery2 = defineQuery18((q) => q.with(Block, Synced6));
5703
+ var syncedBlocksQuery2 = defineQuery18((q) => q.with(Block, Synced7));
5538
5704
  var blockSystem = defineEditorSystem({ phase: "update" }, (ctx) => {
5539
5705
  on(ctx, DragBlock, (ctx2, { entityId, position }) => {
5540
- if (!hasComponent16(ctx2, entityId, Block)) return;
5706
+ if (!hasComponent17(ctx2, entityId, Block)) return;
5541
5707
  Block.setWorldPosition(ctx2, entityId, position);
5542
5708
  });
5543
5709
  on(ctx, SelectBlock, (ctx2, { entityId, deselectOthers }) => {
@@ -5550,7 +5716,7 @@ var blockSystem = defineEditorSystem({ phase: "update" }, (ctx) => {
5550
5716
  deselectBlock(ctx2, entityId);
5551
5717
  });
5552
5718
  on(ctx, ToggleSelect, (ctx2, { entityId }) => {
5553
- if (hasComponent16(ctx2, entityId, Selected)) {
5719
+ if (hasComponent17(ctx2, entityId, Selected)) {
5554
5720
  deselectBlock(ctx2, entityId);
5555
5721
  } else {
5556
5722
  selectBlock(ctx2, entityId);
@@ -5558,14 +5724,14 @@ var blockSystem = defineEditorSystem({ phase: "update" }, (ctx) => {
5558
5724
  });
5559
5725
  on(ctx, DeselectAll, deselectAllBlocks);
5560
5726
  on(ctx, AddHeld, (ctx2, { entityId }) => {
5561
- if (!hasComponent16(ctx2, entityId, Synced6)) return;
5562
- if (hasComponent16(ctx2, entityId, Held)) return;
5727
+ if (!hasComponent17(ctx2, entityId, Synced7)) return;
5728
+ if (hasComponent17(ctx2, entityId, Held)) return;
5563
5729
  const { sessionId } = getResources13(ctx2);
5564
- addComponent7(ctx2, entityId, Held, { sessionId });
5730
+ addComponent8(ctx2, entityId, Held, { sessionId });
5565
5731
  });
5566
5732
  on(ctx, RemoveHeld, (ctx2, { entityId }) => {
5567
- if (hasComponent16(ctx2, entityId, Selected)) return;
5568
- if (!hasComponent16(ctx2, entityId, Held)) return;
5733
+ if (hasComponent17(ctx2, entityId, Selected)) return;
5734
+ if (!hasComponent17(ctx2, entityId, Held)) return;
5569
5735
  removeComponent5(ctx2, entityId, Held);
5570
5736
  });
5571
5737
  on(ctx, SelectAll, (ctx2) => {
@@ -5655,10 +5821,10 @@ function cloneEntities(ctx, entityIds, offset, seed) {
5655
5821
  entityIds = [...allEntityIdsSet];
5656
5822
  const { componentsById } = getResources13(ctx);
5657
5823
  const documentComponents = new Map([...componentsById].filter(([, def]) => def.sync === "document"));
5658
- const syncedComponentId = Synced6._getComponentId(ctx);
5824
+ const syncedComponentId = Synced7._getComponentId(ctx);
5659
5825
  const blockComponentId = Block._getComponentId(ctx);
5660
5826
  const entityIdSet = new Set(entityIds);
5661
- 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) => {
5662
5828
  const rankA = Block.read(ctx, a).rank;
5663
5829
  const rankB = Block.read(ctx, b).rank;
5664
5830
  return rankA < rankB ? -1 : rankA > rankB ? 1 : 0;
@@ -5682,12 +5848,12 @@ function cloneEntities(ctx, entityIds, offset, seed) {
5682
5848
  }
5683
5849
  const originalToClone = /* @__PURE__ */ new Map();
5684
5850
  for (const entityId of entityIds) {
5685
- if (!hasComponent16(ctx, entityId, Synced6)) continue;
5686
- const synced = Synced6.read(ctx, entityId);
5851
+ if (!hasComponent17(ctx, entityId, Synced7)) continue;
5852
+ const synced = Synced7.read(ctx, entityId);
5687
5853
  const cloneId = generateUuidBySeed(synced.id, seed);
5688
- const cloneEntityId = createEntity6(ctx);
5854
+ const cloneEntityId = createEntity7(ctx);
5689
5855
  originalToClone.set(entityId, cloneEntityId);
5690
- addComponent7(ctx, cloneEntityId, Synced6, { id: cloneId });
5856
+ addComponent8(ctx, cloneEntityId, Synced7, { id: cloneId });
5691
5857
  for (const componentId of ctx.entityBuffer.getComponentIds(entityId)) {
5692
5858
  if (componentId === syncedComponentId) continue;
5693
5859
  const componentDef = documentComponents.get(componentId);
@@ -5703,7 +5869,7 @@ function cloneEntities(ctx, entityIds, offset, seed) {
5703
5869
  }
5704
5870
  data.rank = cloneRankMap.get(entityId) ?? RankBounds.genBetween(prevRank, lowestOriginalRank);
5705
5871
  }
5706
- addComponent7(ctx, cloneEntityId, componentDef, data);
5872
+ addComponent8(ctx, cloneEntityId, componentDef, data);
5707
5873
  }
5708
5874
  }
5709
5875
  swapConnectorRefs(ctx, originalToClone);
@@ -5719,23 +5885,23 @@ function uncloneEntities(ctx, entityIds, seed) {
5719
5885
  entityIds = [...allEntityIdsSet];
5720
5886
  const expectedCloneUuids = /* @__PURE__ */ new Set();
5721
5887
  for (const entityId of entityIds) {
5722
- if (!hasComponent16(ctx, entityId, Synced6)) continue;
5723
- const synced = Synced6.read(ctx, entityId);
5888
+ if (!hasComponent17(ctx, entityId, Synced7)) continue;
5889
+ const synced = Synced7.read(ctx, entityId);
5724
5890
  expectedCloneUuids.add(generateUuidBySeed(synced.id, seed));
5725
5891
  }
5726
5892
  const clonesToRemove = [];
5727
5893
  for (const entityId of syncedBlocksQuery2.current(ctx)) {
5728
- const synced = Synced6.read(ctx, entityId);
5894
+ const synced = Synced7.read(ctx, entityId);
5729
5895
  if (expectedCloneUuids.has(synced.id)) {
5730
5896
  clonesToRemove.push(entityId);
5731
5897
  }
5732
5898
  }
5733
5899
  const cloneToOriginal = /* @__PURE__ */ new Map();
5734
5900
  for (const cloneId of clonesToRemove) {
5735
- const cloneUuid = Synced6.read(ctx, cloneId).id;
5901
+ const cloneUuid = Synced7.read(ctx, cloneId).id;
5736
5902
  for (const originalId of entityIds) {
5737
- if (!hasComponent16(ctx, originalId, Synced6)) continue;
5738
- const originalUuid = Synced6.read(ctx, originalId).id;
5903
+ if (!hasComponent17(ctx, originalId, Synced7)) continue;
5904
+ const originalUuid = Synced7.read(ctx, originalId).id;
5739
5905
  if (generateUuidBySeed(originalUuid, seed) === cloneUuid) {
5740
5906
  cloneToOriginal.set(cloneId, originalId);
5741
5907
  break;
@@ -5762,7 +5928,7 @@ function swapConnectorRefs(ctx, fromTo) {
5762
5928
  function swapParentRefs(ctx, fromTo) {
5763
5929
  for (const [, cloneId] of fromTo) {
5764
5930
  if (!isAlive2(ctx, cloneId)) continue;
5765
- if (!hasComponent16(ctx, cloneId, Block)) continue;
5931
+ if (!hasComponent17(ctx, cloneId, Block)) continue;
5766
5932
  const block = Block.read(ctx, cloneId);
5767
5933
  if (block.parentId !== null && fromTo.has(block.parentId)) {
5768
5934
  Block.write(ctx, cloneId).parentId = fromTo.get(block.parentId);
@@ -5772,20 +5938,27 @@ function swapParentRefs(ctx, fromTo) {
5772
5938
 
5773
5939
  // src/systems/update/dragHandlerSystem.ts
5774
5940
  import { Scalar as Scalar2, Vec2 as Vec26 } from "@woven-canvas/math";
5775
- import { defineQuery as defineQuery19, hasComponent as hasComponent17 } from "@woven-ecs/core";
5941
+ import { defineQuery as defineQuery19, hasComponent as hasComponent18 } from "@woven-ecs/core";
5776
5942
  var selectedBlocksQuery5 = defineQuery19((q) => q.with(Block, Selected));
5777
5943
  var transformBoxQuery2 = defineQuery19((q) => q.with(Block, TransformBox, DragStart));
5778
5944
  function isParentSelected(ctx, blockId) {
5779
5945
  const block = Block.read(ctx, blockId);
5780
- 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;
5781
5954
  }
5782
5955
  var dragHandlerSystem = defineEditorSystem({ phase: "update" }, (ctx) => {
5783
5956
  on(ctx, DragBlock, (ctx2, { entityId, position }) => {
5784
- if (hasComponent17(ctx2, entityId, TransformBox)) {
5957
+ if (hasComponent18(ctx2, entityId, TransformBox)) {
5785
5958
  onTransformBoxDrag(ctx2, entityId, position);
5786
5959
  return;
5787
5960
  }
5788
- if (hasComponent17(ctx2, entityId, TransformHandle)) {
5961
+ if (hasComponent18(ctx2, entityId, TransformHandle)) {
5789
5962
  onTransformHandleDrag(ctx2, entityId, position);
5790
5963
  return;
5791
5964
  }
@@ -5796,11 +5969,13 @@ function onTransformBoxDrag(ctx, boxId, position) {
5796
5969
  const dx = position[0] - boxStart.position[0];
5797
5970
  const dy = position[1] - boxStart.position[1];
5798
5971
  for (const blockId of selectedBlocksQuery5.current(ctx)) {
5799
- if (!hasComponent17(ctx, blockId, DragStart)) continue;
5972
+ if (!hasComponent18(ctx, blockId, DragStart)) continue;
5800
5973
  if (isParentSelected(ctx, blockId)) continue;
5801
5974
  const blockStart = DragStart.read(ctx, blockId);
5802
5975
  const worldPos = [blockStart.position[0] + dx, blockStart.position[1] + dy];
5803
- Grid.snapPosition(ctx, worldPos);
5976
+ if (canBlockSnap(ctx, Block.read(ctx, blockId).tag)) {
5977
+ Grid.snapPosition(ctx, worldPos);
5978
+ }
5804
5979
  Block.setWorldPosition(ctx, blockId, worldPos);
5805
5980
  }
5806
5981
  }
@@ -5834,10 +6009,11 @@ function onRotateHandleDrag(ctx, handleId, position) {
5834
6009
  let delta = angleHandle - handleStartAngle;
5835
6010
  const grid = Grid.read(ctx);
5836
6011
  const keyboard = Keyboard.read(ctx);
6012
+ const selectionSnappable = isSelectionSnappable(ctx);
5837
6013
  let snapAngle = 0;
5838
6014
  if (keyboard.shiftDown) {
5839
6015
  snapAngle = grid.shiftSnapAngleRad;
5840
- } else if (grid.enabled) {
6016
+ } else if (grid.enabled && selectionSnappable) {
5841
6017
  snapAngle = grid.snapAngleRad;
5842
6018
  }
5843
6019
  if (snapAngle > 0) {
@@ -5846,7 +6022,7 @@ function onRotateHandleDrag(ctx, handleId, position) {
5846
6022
  }
5847
6023
  boxBlock.rotateZ = Scalar2.normalizeAngle(dragStart.rotateZ + delta);
5848
6024
  for (const blockId of selectedBlocksQuery5.current(ctx)) {
5849
- if (!hasComponent17(ctx, blockId, DragStart)) continue;
6025
+ if (!hasComponent18(ctx, blockId, DragStart)) continue;
5850
6026
  if (isParentSelected(ctx, blockId)) continue;
5851
6027
  const blockStart = DragStart.read(ctx, blockId);
5852
6028
  const block = Block.write(ctx, blockId);
@@ -5859,7 +6035,7 @@ function onRotateHandleDrag(ctx, handleId, position) {
5859
6035
  const angle = Vec26.angleTo(boxCenter, startCenter) + delta;
5860
6036
  const newCenter = Vec26.fromPolar(r, angle, boxCenter);
5861
6037
  const worldPos = [newCenter[0] - block.size[0] / 2, newCenter[1] - block.size[1] / 2];
5862
- if (grid.strict) {
6038
+ if (grid.strict && canBlockSnap(ctx, block.tag)) {
5863
6039
  Grid.snapPosition(ctx, worldPos);
5864
6040
  }
5865
6041
  Block.setWorldPosition(ctx, blockId, worldPos);
@@ -5876,7 +6052,10 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5876
6052
  const handle = TransformHandle.read(ctx, handleId);
5877
6053
  const handleBlock = Block.read(ctx, handleId);
5878
6054
  const handleCenter = [position[0] + handleBlock.size[0] / 2, position[1] + handleBlock.size[1] / 2];
5879
- Grid.snapPosition(ctx, handleCenter);
6055
+ const selectionSnappable = isSelectionSnappable(ctx);
6056
+ if (selectionSnappable) {
6057
+ Grid.snapPosition(ctx, handleCenter);
6058
+ }
5880
6059
  const boxRotateZ = boxStart.rotateZ;
5881
6060
  const boxStartCenter = [
5882
6061
  boxStart.position[0] + boxStart.size[0] / 2,
@@ -5927,7 +6106,7 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5927
6106
  Vec26.add(newBoxCenter, vec);
5928
6107
  const scaleFactor = [scaleX, scaleY];
5929
6108
  for (const blockId of selectedBlocksQuery5.current(ctx)) {
5930
- if (!hasComponent17(ctx, blockId, DragStart)) continue;
6109
+ if (!hasComponent18(ctx, blockId, DragStart)) continue;
5931
6110
  if (isParentSelected(ctx, blockId)) continue;
5932
6111
  const blockStart = DragStart.read(ctx, blockId);
5933
6112
  const block = Block.write(ctx, blockId);
@@ -5945,7 +6124,7 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5945
6124
  const newBlockSize = Vec26.clone(blockStart.size);
5946
6125
  Vec26.multiply(newBlockSize, scaleFactor);
5947
6126
  const isTextResize = getBlockResizeMode(ctx, blockId) === ResizeMode.Text;
5948
- if (grid.strict) {
6127
+ if (grid.strict && canBlockSnap(ctx, block.tag)) {
5949
6128
  if (isTextResize && maintainAspectRatio) {
5950
6129
  newBlockSize[1] = Math.max(grid.rowHeight, Grid.snapY(ctx, newBlockSize[1]));
5951
6130
  const aspectRatio = blockStart.size[0] / blockStart.size[1];
@@ -5956,7 +6135,7 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5956
6135
  }
5957
6136
  const worldPos = Vec26.clone(newBlockCenter);
5958
6137
  Vec26.sub(worldPos, [newBlockSize[0] / 2, newBlockSize[1] / 2]);
5959
- if (grid.strict) {
6138
+ if (grid.strict && canBlockSnap(ctx, block.tag)) {
5960
6139
  Grid.snapPosition(ctx, worldPos);
5961
6140
  }
5962
6141
  Block.setWorldPosition(ctx, blockId, worldPos);
@@ -5967,7 +6146,7 @@ function onScaleHandleDrag(ctx, handleId, position, maintainAspectRatio) {
5967
6146
  } else {
5968
6147
  Vec26.copy(block.size, newBlockSize);
5969
6148
  }
5970
- if (hasComponent17(ctx, blockId, Text)) {
6149
+ if (hasComponent18(ctx, blockId, Text)) {
5971
6150
  const didStretch = handle.kind === TransformHandleKind.Stretch;
5972
6151
  const text = Text.write(ctx, blockId);
5973
6152
  if (didStretch) {
@@ -6005,10 +6184,11 @@ function onRotateScaleHandleDrag(ctx, handleId, position) {
6005
6184
  }
6006
6185
  const grid = Grid.read(ctx);
6007
6186
  const keyboard = Keyboard.read(ctx);
6187
+ const selectionSnappable = isSelectionSnappable(ctx);
6008
6188
  let snapAngle = 0;
6009
6189
  if (keyboard.shiftDown) {
6010
6190
  snapAngle = grid.shiftSnapAngleRad;
6011
- } else if (grid.enabled) {
6191
+ } else if (grid.enabled && selectionSnappable) {
6012
6192
  snapAngle = grid.snapAngleRad;
6013
6193
  }
6014
6194
  if (snapAngle > 0) {
@@ -6028,13 +6208,13 @@ function onRotateScaleHandleDrag(ctx, handleId, position) {
6028
6208
  boxBlock.size = [finalLength, startHeight];
6029
6209
  boxBlock.position = [newCenter[0] - finalLength / 2, newCenter[1] - startHeight / 2];
6030
6210
  for (const blockId of selectedBlocksQuery5.current(ctx)) {
6031
- if (!hasComponent17(ctx, blockId, DragStart)) continue;
6211
+ if (!hasComponent18(ctx, blockId, DragStart)) continue;
6032
6212
  if (isParentSelected(ctx, blockId)) continue;
6033
6213
  const block = Block.write(ctx, blockId);
6034
6214
  block.rotateZ = newRotateZ;
6035
6215
  block.size = [finalLength, startHeight];
6036
6216
  const blockWorldPos = [newCenter[0] - finalLength / 2, newCenter[1] - startHeight / 2];
6037
- if (grid.strict) {
6217
+ if (grid.strict && canBlockSnap(ctx, block.tag)) {
6038
6218
  Grid.snapPosition(ctx, blockWorldPos);
6039
6219
  }
6040
6220
  Block.setWorldPosition(ctx, blockId, blockWorldPos);
@@ -6042,23 +6222,23 @@ function onRotateScaleHandleDrag(ctx, handleId, position) {
6042
6222
  }
6043
6223
 
6044
6224
  // src/systems/update/selectSystem.ts
6045
- import { Synced as Synced7 } from "@woven-ecs/canvas-store";
6046
- 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";
6047
6227
  var selectedBlocksQuery6 = defineQuery20((q) => q.with(Block, Selected));
6048
6228
  var selectionBoxQuery = defineQuery20((q) => q.with(Block, SelectionBox));
6049
- var syncedBlocksQuery3 = defineQuery20((q) => q.with(Block, Synced7, Aabb));
6229
+ var syncedBlocksQuery3 = defineQuery20((q) => q.with(Block, Synced8, Aabb));
6050
6230
  var SELECTION_BOX_RANK = "z";
6051
6231
  var selectSystem2 = defineEditorSystem({ phase: "update" }, (ctx) => {
6052
6232
  on(ctx, AddSelectionBox, (ctx2) => {
6053
- const entityId = createEntity7(ctx2);
6054
- addComponent8(ctx2, entityId, Block, {
6233
+ const entityId = createEntity8(ctx2);
6234
+ addComponent9(ctx2, entityId, Block, {
6055
6235
  tag: "selection-box",
6056
6236
  position: [0, 0],
6057
6237
  size: [0, 0],
6058
6238
  rotateZ: 0,
6059
6239
  rank: SELECTION_BOX_RANK
6060
6240
  });
6061
- addComponent8(ctx2, entityId, SelectionBox, {});
6241
+ addComponent9(ctx2, entityId, SelectionBox, {});
6062
6242
  });
6063
6243
  on(ctx, UpdateSelectionBox, (ctx2, { bounds, deselectOthers }) => {
6064
6244
  const selectionBoxes = selectionBoxQuery.current(ctx2);
@@ -6099,21 +6279,31 @@ var selectSystem2 = defineEditorSystem({ phase: "update" }, (ctx) => {
6099
6279
  });
6100
6280
 
6101
6281
  // src/systems/update/updateFrameContainmentSystem.ts
6102
- 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";
6103
6283
  var updateFrameContainmentSystem = defineEditorSystem({ phase: "update", priority: -10 }, (ctx) => {
6104
6284
  on(ctx, AddFrameHighlight, (ctx2, { frameId }) => {
6105
- if (!hasComponent18(ctx2, frameId, FrameDropTarget)) {
6106
- addComponent9(ctx2, frameId, FrameDropTarget);
6285
+ if (!hasComponent19(ctx2, frameId, FrameDropTarget)) {
6286
+ addComponent10(ctx2, frameId, FrameDropTarget);
6107
6287
  }
6108
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
+ });
6109
6299
  on(ctx, RemoveFrameHighlight, (ctx2, { frameId }) => {
6110
- if (hasComponent18(ctx2, frameId, FrameDropTarget)) {
6300
+ if (hasComponent19(ctx2, frameId, FrameDropTarget)) {
6111
6301
  removeComponent6(ctx2, frameId, FrameDropTarget);
6112
6302
  }
6113
6303
  });
6114
6304
  on(ctx, AssignFrameChildren, (ctx2, { assignments }) => {
6115
6305
  for (const { entityId, frameId } of assignments) {
6116
- if (!hasComponent18(ctx2, entityId, Block)) continue;
6306
+ if (!hasComponent19(ctx2, entityId, Block)) continue;
6117
6307
  const currentParentId = Block.read(ctx2, entityId).parentId;
6118
6308
  if (frameId !== null) {
6119
6309
  if (currentParentId !== frameId) {
@@ -6135,12 +6325,12 @@ var updateFrameContainmentSystem = defineEditorSystem({ phase: "update", priorit
6135
6325
  });
6136
6326
 
6137
6327
  // src/systems/update/updateFrameSetupSystem.ts
6138
- 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";
6139
6329
  var frameQuery2 = defineQuery21((q) => q.tracking(Frame));
6140
6330
  var updateFrameSetupSystem = defineEditorSystem({ phase: "update", priority: 100 }, (ctx) => {
6141
6331
  for (const entityId of frameQuery2.added(ctx)) {
6142
- if (hasComponent19(ctx, entityId, HitGeometry)) continue;
6143
- addComponent10(ctx, entityId, HitGeometry);
6332
+ if (hasComponent20(ctx, entityId, HitGeometry)) continue;
6333
+ addComponent11(ctx, entityId, HitGeometry);
6144
6334
  HitGeometry.addCapsuleUv(ctx, entityId, [0, 0], [1, 0], FRAME_EDGE_THICKNESS);
6145
6335
  HitGeometry.addCapsuleUv(ctx, entityId, [1, 0], [1, 1], FRAME_EDGE_THICKNESS);
6146
6336
  HitGeometry.addCapsuleUv(ctx, entityId, [1, 1], [0, 1], FRAME_EDGE_THICKNESS);
@@ -6318,10 +6508,10 @@ function createCorePlugin(options = {}) {
6318
6508
  }
6319
6509
 
6320
6510
  // src/Editor.ts
6321
- import { Synced as Synced8 } from "@woven-ecs/canvas-store";
6511
+ import { Synced as Synced9 } from "@woven-ecs/canvas-store";
6322
6512
  import {
6323
- addComponent as addComponent11,
6324
- createEntity as createEntity8,
6513
+ addComponent as addComponent12,
6514
+ createEntity as createEntity9,
6325
6515
  World
6326
6516
  } from "@woven-ecs/core";
6327
6517
 
@@ -6353,21 +6543,44 @@ var FontFamily = z2.object({
6353
6543
  });
6354
6544
  var FontLoader = class {
6355
6545
  constructor() {
6356
- this.loadedFonts = /* @__PURE__ */ new Set();
6357
- }
6358
- /**
6359
- * Load multiple font families.
6360
- * 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.
6361
6559
  *
6362
6560
  * @param families - Array of font families to load
6363
6561
  */
6364
6562
  async loadFonts(families) {
6365
- const unloadedFamilies = families.filter((family) => !this.loadedFonts.has(family.name));
6366
- if (unloadedFamilies.length === 0) {
6367
- return;
6368
- }
6369
- const fontPromises = unloadedFamilies.map((family) => this.loadSingleFont(family));
6370
- 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;
6371
6584
  }
6372
6585
  /**
6373
6586
  * Build a Google Fonts URL with weight and italic variants.
@@ -6404,34 +6617,36 @@ var FontLoader = class {
6404
6617
  }
6405
6618
  /**
6406
6619
  * Load a single font family.
6407
- * 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.
6408
6625
  *
6409
6626
  * @param family - Font family to load
6410
6627
  */
6411
- loadSingleFont(family) {
6412
- this.loadedFonts.add(family.name);
6413
- if (typeof document === "undefined") {
6414
- return Promise.resolve();
6415
- }
6416
- return new Promise((resolve, reject) => {
6417
- const link = document.createElement("link");
6418
- link.rel = "stylesheet";
6419
- link.href = this.buildGoogleFontsUrl(family);
6420
- link.onload = async () => {
6421
- try {
6422
- const loadPromises = this.getFontLoadPromises(family);
6423
- await Promise.all(loadPromises);
6424
- await document.fonts.ready;
6425
- resolve();
6426
- } catch (error) {
6427
- reject(new Error(`Failed to load font face for: ${family.name}. ${error}`));
6428
- }
6429
- };
6430
- link.onerror = () => {
6431
- reject(new Error(`Failed to load font stylesheet: ${family.name}`));
6432
- };
6433
- document.head.appendChild(link);
6434
- });
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
+ }
6435
6650
  }
6436
6651
  /**
6437
6652
  * Get promises for loading all font variants (weights and italics).
@@ -6457,13 +6672,13 @@ var FontLoader = class {
6457
6672
  * @returns True if the font has been loaded
6458
6673
  */
6459
6674
  isLoaded(fontName) {
6460
- return this.loadedFonts.has(fontName);
6675
+ return this.loaded.has(fontName);
6461
6676
  }
6462
6677
  /**
6463
6678
  * Get the set of loaded font names.
6464
6679
  */
6465
6680
  getLoadedFonts() {
6466
- return this.loadedFonts;
6681
+ return this.loaded;
6467
6682
  }
6468
6683
  };
6469
6684
 
@@ -6544,7 +6759,7 @@ var Editor = class {
6544
6759
  const plugins = pluginInputs.map(parsePlugin);
6545
6760
  const corePlugin = createCorePlugin(options.corePlugin);
6546
6761
  const sortedPlugins = sortPluginsByDependencies([corePlugin, ...plugins]);
6547
- const allDefs = [CommandMarker, Synced8];
6762
+ const allDefs = [CommandMarker, Synced9];
6548
6763
  for (const plugin of sortedPlugins) {
6549
6764
  if (plugin.components) {
6550
6765
  allDefs.push(...plugin.components);
@@ -6617,7 +6832,8 @@ var Editor = class {
6617
6832
  componentsByName,
6618
6833
  singletonsByName,
6619
6834
  componentsById,
6620
- singletonsById
6835
+ singletonsById,
6836
+ readonly: options.readonly
6621
6837
  };
6622
6838
  this.world = new World(allDefs, {
6623
6839
  maxEntities,
@@ -6673,17 +6889,14 @@ var Editor = class {
6673
6889
  * Call this after construction to run async setup.
6674
6890
  */
6675
6891
  async initialize() {
6676
- if (this.fonts.length > 0) {
6677
- await this.fontLoader.loadFonts(this.fonts);
6678
- }
6679
6892
  Grid.copy(this.ctx, this.gridOptions);
6680
6893
  Controls.copy(this.ctx, this.controlsOptions);
6681
6894
  this.nextTick((ctx) => {
6682
- const userEntity = createEntity8(ctx);
6683
- addComponent11(ctx, userEntity, Synced8, {
6895
+ const userEntity = createEntity9(ctx);
6896
+ addComponent12(ctx, userEntity, Synced9, {
6684
6897
  id: crypto.randomUUID()
6685
6898
  });
6686
- addComponent11(ctx, userEntity, User, {
6899
+ addComponent12(ctx, userEntity, User, {
6687
6900
  userId: this.userData.userId,
6688
6901
  sessionId: this.userData.sessionId,
6689
6902
  color: this.userData.color,
@@ -6697,6 +6910,29 @@ var Editor = class {
6697
6910
  }
6698
6911
  }
6699
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
+ }
6700
6936
  /**
6701
6937
  * Run one frame of the editor loop.
6702
6938
  *
@@ -6762,6 +6998,10 @@ var Editor = class {
6762
6998
  def.spawn(ctx, payload);
6763
6999
  });
6764
7000
  }
7001
+ /** Runtime readonly flag. Input systems and command() check this to gate mutations. */
7002
+ get readonly() {
7003
+ return this.ctx.resources.readonly;
7004
+ }
6765
7005
  /**
6766
7006
  * Subscribe to query changes.
6767
7007
  *
@@ -6907,11 +7147,14 @@ export {
6907
7147
  Key,
6908
7148
  Keybind,
6909
7149
  Keyboard,
7150
+ Layer,
6910
7151
  MAX_HIT_ARCS,
6911
7152
  MAX_HIT_CAPSULES,
7153
+ MAX_HIT_POLYGON_POINTS,
6912
7154
  MainThreadSystem2 as MainThreadSystem,
6913
7155
  Mouse,
6914
7156
  Opacity,
7157
+ PlaceBlockEvent,
6915
7158
  Pointer,
6916
7159
  PointerButton,
6917
7160
  PointerType,
@@ -6945,7 +7188,7 @@ export {
6945
7188
  StartTransformBoxEdit,
6946
7189
  Stratum,
6947
7190
  StrokeKind,
6948
- Synced9 as Synced,
7191
+ Synced10 as Synced,
6949
7192
  Text,
6950
7193
  TextAlignment,
6951
7194
  Tick,
@@ -6964,16 +7207,17 @@ export {
6964
7207
  UserData,
6965
7208
  VerticalAlign,
6966
7209
  VerticalAlignment,
6967
- addComponent12 as addComponent,
7210
+ addComponent13 as addComponent,
6968
7211
  canBlockEdit,
6969
7212
  canBlockInteract,
6970
7213
  cascadeDelete,
6971
7214
  clearPointerTrackingState,
6972
7215
  compareBlockOrder,
6973
7216
  convertRefsToUuids,
7217
+ createBlock,
6974
7218
  createCorePlugin,
6975
- createEntity9 as createEntity,
6976
- defineCanvasComponent22 as defineCanvasComponent,
7219
+ createEntity10 as createEntity,
7220
+ defineCanvasComponent23 as defineCanvasComponent,
6977
7221
  defineCanvasSingleton3 as defineCanvasSingleton,
6978
7222
  defineCommand,
6979
7223
  defineEditorState,
@@ -6983,7 +7227,7 @@ export {
6983
7227
  deselectAll,
6984
7228
  deselectBlock,
6985
7229
  detectEmbedProvider,
6986
- field36 as field,
7230
+ field37 as field,
6987
7231
  findFrameAtPoint,
6988
7232
  generateUuidBySeed,
6989
7233
  getBackrefs5 as getBackrefs,
@@ -7001,18 +7245,21 @@ export {
7001
7245
  getResources15 as getResources,
7002
7246
  getTopmostBlockAtPoint,
7003
7247
  getWorldPosition,
7004
- hasComponent20 as hasComponent,
7248
+ hasComponent21 as hasComponent,
7005
7249
  intersectAabb,
7006
7250
  intersectCapsule,
7007
7251
  intersectPoint,
7008
7252
  isAlive3 as isAlive,
7009
7253
  isAncestor,
7010
7254
  isHeldByRemote,
7255
+ isLayerInteractive,
7256
+ isReadonly,
7011
7257
  on,
7012
7258
  parsePlugin,
7013
7259
  removeComponent7 as removeComponent,
7014
7260
  removeEntity7 as removeEntity,
7015
7261
  resolveEmbedUrl,
7262
+ resolveLayerRank,
7016
7263
  resolveRefFields,
7017
7264
  runMachine,
7018
7265
  selectAll,