@woven-canvas/core 0.1.0

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 ADDED
@@ -0,0 +1,4036 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/index.ts
8
+ import {
9
+ CanvasComponentDef as CanvasComponentDef7,
10
+ CanvasSingletonDef as CanvasSingletonDef12,
11
+ defineCanvasComponent as defineCanvasComponent13,
12
+ defineCanvasSingleton as defineCanvasSingleton3,
13
+ Synced as Synced4
14
+ } from "@woven-ecs/canvas-store";
15
+ import {
16
+ addComponent as addComponent6,
17
+ ComponentDef,
18
+ createEntity as createEntity5,
19
+ defineQuery as defineQuery11,
20
+ defineSystem,
21
+ EventType,
22
+ field as field28,
23
+ getBackrefs,
24
+ getResources as getResources12,
25
+ hasComponent as hasComponent7,
26
+ isAlive,
27
+ MainThreadSystem as MainThreadSystem2,
28
+ removeComponent as removeComponent2,
29
+ removeEntity as removeEntity3,
30
+ SINGLETON_ENTITY_ID
31
+ } from "@woven-ecs/core";
32
+
33
+ // src/CorePlugin.ts
34
+ import {
35
+ CanvasComponentDef as CanvasComponentDef6,
36
+ CanvasSingletonDef as CanvasSingletonDef10
37
+ } from "@woven-ecs/canvas-store";
38
+ import { getResources as getResources11 } from "@woven-ecs/core";
39
+
40
+ // src/components/index.ts
41
+ var components_exports = {};
42
+ __export(components_exports, {
43
+ Aabb: () => Aabb,
44
+ Asset: () => Asset,
45
+ Block: () => Block,
46
+ Color: () => Color,
47
+ Connector: () => Connector,
48
+ Edited: () => Edited,
49
+ Held: () => Held,
50
+ HitGeometry: () => HitGeometry,
51
+ Hovered: () => Hovered,
52
+ Image: () => Image,
53
+ MAX_HIT_ARCS: () => MAX_HIT_ARCS,
54
+ MAX_HIT_CAPSULES: () => MAX_HIT_CAPSULES,
55
+ Opacity: () => Opacity,
56
+ Pointer: () => Pointer,
57
+ PointerButton: () => PointerButton,
58
+ PointerType: () => PointerType,
59
+ ScaleWithZoom: () => ScaleWithZoom,
60
+ Shape: () => Shape,
61
+ StrokeKind: () => StrokeKind,
62
+ Text: () => Text,
63
+ UploadState: () => UploadState,
64
+ User: () => User,
65
+ VerticalAlign: () => VerticalAlign,
66
+ addPointerSample: () => addPointerSample
67
+ });
68
+
69
+ // src/components/Aabb.ts
70
+ import { Aabb as AabbMath } from "@woven-canvas/math";
71
+ import { CanvasComponentDef as CanvasComponentDef2 } from "@woven-ecs/canvas-store";
72
+ import { field as field2 } from "@woven-ecs/core";
73
+
74
+ // src/components/Block.ts
75
+ import { Rect, Vec2 } from "@woven-canvas/math";
76
+ import { CanvasComponentDef } from "@woven-ecs/canvas-store";
77
+ import { field } from "@woven-ecs/core";
78
+ var _aabbCorners = [
79
+ [0, 0],
80
+ [0, 0],
81
+ [0, 0],
82
+ [0, 0]
83
+ ];
84
+ var _blockCorners = [
85
+ [0, 0],
86
+ [0, 0],
87
+ [0, 0],
88
+ [0, 0]
89
+ ];
90
+ var _blockAxes = [
91
+ [0, 0],
92
+ [0, 0]
93
+ ];
94
+ var BlockSchema = {
95
+ /** Element tag name (e.g., "div", "img") */
96
+ tag: field.string().max(36).default("div"),
97
+ /** Position as [left, top] */
98
+ position: field.tuple(field.float64(), 2).default([0, 0]),
99
+ /** Size as [width, height] */
100
+ size: field.tuple(field.float64(), 2).default([100, 100]),
101
+ /** Rotation around center in radians */
102
+ rotateZ: field.float64().default(0),
103
+ /** Flip state as [flipX, flipY] */
104
+ flip: field.tuple(field.boolean(), 2).default([false, false]),
105
+ /** Z-order rank (LexoRank string) */
106
+ rank: field.string().max(36).default("")
107
+ };
108
+ var BlockDef = class extends CanvasComponentDef {
109
+ constructor() {
110
+ super({ name: "block", sync: "document" }, BlockSchema);
111
+ }
112
+ /**
113
+ * Get the center point of a block.
114
+ * @param out - Optional output vector to write to (avoids allocation)
115
+ */
116
+ getCenter(ctx, entityId, out) {
117
+ const { position, size } = this.read(ctx, entityId);
118
+ const result = out ?? [0, 0];
119
+ Rect.getCenter(position, size, result);
120
+ return result;
121
+ }
122
+ /**
123
+ * Set the center point of a block (adjusts position accordingly).
124
+ */
125
+ setCenter(ctx, entityId, center) {
126
+ const block = this.write(ctx, entityId);
127
+ Rect.setCenter(block.position, block.size, center);
128
+ }
129
+ /**
130
+ * Get the four corner points of a block (accounting for rotation).
131
+ * Returns corners in order: top-left, top-right, bottom-right, bottom-left.
132
+ * @param out - Optional output array to write to (avoids allocation)
133
+ */
134
+ getCorners(ctx, entityId, out) {
135
+ const { position, size, rotateZ } = this.read(ctx, entityId);
136
+ const result = out ?? [
137
+ [0, 0],
138
+ [0, 0],
139
+ [0, 0],
140
+ [0, 0]
141
+ ];
142
+ Rect.getCorners(position, size, rotateZ, result);
143
+ return result;
144
+ }
145
+ /**
146
+ * Check if a point intersects a block (accounting for rotation).
147
+ */
148
+ containsPoint(ctx, entityId, point) {
149
+ const { position, size, rotateZ } = this.read(ctx, entityId);
150
+ return Rect.containsPoint(position, size, rotateZ, point);
151
+ }
152
+ /**
153
+ * Move a block by a delta offset.
154
+ */
155
+ translate(ctx, entityId, delta) {
156
+ const block = this.write(ctx, entityId);
157
+ Rect.translate(block.position, delta);
158
+ }
159
+ /**
160
+ * Set the position of a block.
161
+ */
162
+ setPosition(ctx, entityId, position) {
163
+ const block = this.write(ctx, entityId);
164
+ Vec2.copy(block.position, position);
165
+ }
166
+ /**
167
+ * Set the size of a block.
168
+ */
169
+ setSize(ctx, entityId, size) {
170
+ const block = this.write(ctx, entityId);
171
+ Vec2.copy(block.size, size);
172
+ }
173
+ /**
174
+ * Rotate a block by a delta angle (in radians).
175
+ */
176
+ rotateBy(ctx, entityId, deltaAngle) {
177
+ const block = this.write(ctx, entityId);
178
+ block.rotateZ += deltaAngle;
179
+ }
180
+ /**
181
+ * Rotate a block around a pivot point.
182
+ */
183
+ rotateAround(ctx, entityId, pivot, angle) {
184
+ const block = this.write(ctx, entityId);
185
+ block.rotateZ = Rect.rotateAround(block.position, block.size, block.rotateZ, pivot, angle);
186
+ }
187
+ /**
188
+ * Scale a block uniformly around its center.
189
+ */
190
+ scaleBy(ctx, entityId, scaleFactor) {
191
+ const block = this.write(ctx, entityId);
192
+ Rect.scaleBy(block.position, block.size, scaleFactor);
193
+ }
194
+ /**
195
+ * Scale a block around a pivot point.
196
+ */
197
+ scaleAround(ctx, entityId, pivot, scaleFactor) {
198
+ const block = this.write(ctx, entityId);
199
+ Rect.scaleAround(block.position, block.size, pivot, scaleFactor);
200
+ }
201
+ /**
202
+ * Check if an AABB intersects with this block using Separating Axis Theorem.
203
+ * This handles all intersection cases including narrow AABBs that pass through
204
+ * the middle of a rotated block without touching any corners.
205
+ * Optimized to avoid allocations for hot path usage.
206
+ */
207
+ intersectsAabb(ctx, entityId, aabb) {
208
+ const { position, size, rotateZ } = this.read(ctx, entityId);
209
+ return Rect.intersectsAabb(position, size, rotateZ, aabb, _blockCorners, _aabbCorners, _blockAxes);
210
+ }
211
+ worldToUv(ctx, entityId, worldPos) {
212
+ const { position, size, rotateZ, flip } = this.read(ctx, entityId);
213
+ const uv = Rect.worldToUv(position, size, rotateZ, worldPos);
214
+ if (flip[0]) uv[0] = 1 - uv[0];
215
+ if (flip[1]) uv[1] = 1 - uv[1];
216
+ return uv;
217
+ }
218
+ uvToWorld(ctx, entityId, uv) {
219
+ const { position, size, rotateZ, flip } = this.read(ctx, entityId);
220
+ const flippedUv = [flip[0] ? 1 - uv[0] : uv[0], flip[1] ? 1 - uv[1] : uv[1]];
221
+ return Rect.uvToWorld(position, size, rotateZ, flippedUv);
222
+ }
223
+ /**
224
+ * Get the UV-to-world transformation matrix for a block.
225
+ * More efficient than multiple uvToWorld() calls since sin/cos is computed once.
226
+ * Use with Mat2.transformPoint() to transform UV coordinates to world.
227
+ * Note: This matrix does NOT account for flip - use uvToWorld() for flipped blocks.
228
+ *
229
+ * @param ctx - ECS context
230
+ * @param entityId - Entity ID
231
+ * @param out - Output matrix to write to [a, b, c, d, tx, ty]
232
+ */
233
+ getUvToWorldMatrix(ctx, entityId, out) {
234
+ const { position, size, rotateZ, flip } = this.read(ctx, entityId);
235
+ Rect.getUvToWorldMatrix(position, size, rotateZ, out);
236
+ if (flip[0]) {
237
+ out[0] = -out[0];
238
+ out[2] = -out[2];
239
+ out[4] += size[0] * Math.cos(rotateZ);
240
+ out[5] += size[0] * Math.sin(rotateZ);
241
+ }
242
+ if (flip[1]) {
243
+ out[1] = -out[1];
244
+ out[3] = -out[3];
245
+ out[4] -= size[1] * Math.sin(rotateZ);
246
+ out[5] += size[1] * Math.cos(rotateZ);
247
+ }
248
+ }
249
+ };
250
+ var Block = new BlockDef();
251
+
252
+ // src/components/Aabb.ts
253
+ var LEFT = 0;
254
+ var TOP = 1;
255
+ var RIGHT = 2;
256
+ var BOTTOM = 3;
257
+ var AabbSchema = {
258
+ /** Bounds as [left, top, right, bottom] */
259
+ value: field2.tuple(field2.float32(), 4).default([0, 0, 0, 0])
260
+ };
261
+ var AabbDef = class extends CanvasComponentDef2 {
262
+ constructor() {
263
+ super({ name: "aabb" }, AabbSchema);
264
+ }
265
+ /**
266
+ * Check if a point is contained within an entity's AABB.
267
+ */
268
+ containsPoint(ctx, entityId, point, inclusive = true) {
269
+ const { value } = this.read(ctx, entityId);
270
+ return AabbMath.containsPoint(value, point, inclusive);
271
+ }
272
+ /**
273
+ * Expand AABB to include a point.
274
+ */
275
+ expandByPoint(ctx, entityId, point) {
276
+ const { value } = this.write(ctx, entityId);
277
+ AabbMath.expand(value, point);
278
+ }
279
+ /**
280
+ * Expand AABB to include another entity's block.
281
+ */
282
+ expandByBlock(ctx, entityId, blockEntityId) {
283
+ const corners = Block.getCorners(ctx, blockEntityId);
284
+ const { value } = this.write(ctx, entityId);
285
+ for (const corner of corners) {
286
+ AabbMath.expand(value, corner);
287
+ }
288
+ }
289
+ /**
290
+ * Expand AABB to include another AABB.
291
+ */
292
+ expandByAabb(ctx, entityId, other) {
293
+ const { value } = this.write(ctx, entityId);
294
+ AabbMath.union(value, other);
295
+ }
296
+ /**
297
+ * Copy bounds from another entity's AABB.
298
+ */
299
+ copyFrom(ctx, entityId, otherEntityId) {
300
+ const { value: src } = this.read(ctx, otherEntityId);
301
+ const { value: dst } = this.write(ctx, entityId);
302
+ AabbMath.copy(dst, src);
303
+ }
304
+ /**
305
+ * Set AABB from an array of points.
306
+ */
307
+ setByPoints(ctx, entityId, points) {
308
+ if (points.length === 0) return;
309
+ const { value } = this.write(ctx, entityId);
310
+ AabbMath.setFromPoints(value, points);
311
+ }
312
+ /**
313
+ * Get the center point of an entity's AABB.
314
+ */
315
+ getCenter(ctx, entityId) {
316
+ const { value } = this.read(ctx, entityId);
317
+ return AabbMath.center(value);
318
+ }
319
+ /**
320
+ * Get the width of an entity's AABB.
321
+ */
322
+ getWidth(ctx, entityId) {
323
+ const { value } = this.read(ctx, entityId);
324
+ return AabbMath.width(value);
325
+ }
326
+ /**
327
+ * Get the height of an entity's AABB.
328
+ */
329
+ getHeight(ctx, entityId) {
330
+ const { value } = this.read(ctx, entityId);
331
+ return AabbMath.height(value);
332
+ }
333
+ /**
334
+ * Get the distance from AABB to a point.
335
+ */
336
+ distanceToPoint(ctx, entityId, point) {
337
+ const { value } = this.read(ctx, entityId);
338
+ return AabbMath.distanceToPoint(value, point);
339
+ }
340
+ /**
341
+ * Check if two entity AABBs intersect.
342
+ */
343
+ intersectsEntity(ctx, entityIdA, entityIdB) {
344
+ const { value: a } = this.read(ctx, entityIdA);
345
+ const { value: b } = this.read(ctx, entityIdB);
346
+ return AabbMath.intersects(a, b);
347
+ }
348
+ /**
349
+ * Check if entity A's AABB completely surrounds entity B's AABB.
350
+ */
351
+ surroundsEntity(ctx, entityIdA, entityIdB) {
352
+ const { value: a } = this.read(ctx, entityIdA);
353
+ const { value: b } = this.read(ctx, entityIdB);
354
+ return AabbMath.contains(a, b);
355
+ }
356
+ /**
357
+ * Get the four corner points of an entity's AABB.
358
+ * Returns corners in order: top-left, top-right, bottom-right, bottom-left.
359
+ * @param out - Optional output array to write to (avoids allocation)
360
+ */
361
+ getCorners(ctx, entityId, out) {
362
+ const { value } = this.read(ctx, entityId);
363
+ const result = out ?? [
364
+ [0, 0],
365
+ [0, 0],
366
+ [0, 0],
367
+ [0, 0]
368
+ ];
369
+ result[0][0] = value[LEFT];
370
+ result[0][1] = value[TOP];
371
+ result[1][0] = value[RIGHT];
372
+ result[1][1] = value[TOP];
373
+ result[2][0] = value[RIGHT];
374
+ result[2][1] = value[BOTTOM];
375
+ result[3][0] = value[LEFT];
376
+ result[3][1] = value[BOTTOM];
377
+ return result;
378
+ }
379
+ /**
380
+ * Apply padding to all sides of the AABB.
381
+ */
382
+ applyPadding(ctx, entityId, padding) {
383
+ const { value } = this.write(ctx, entityId);
384
+ AabbMath.pad(value, padding);
385
+ }
386
+ };
387
+ var Aabb = new AabbDef();
388
+
389
+ // src/components/Asset.ts
390
+ import { defineCanvasComponent } from "@woven-ecs/canvas-store";
391
+ import { field as field3 } from "@woven-ecs/core";
392
+ var UploadState = {
393
+ /** Asset queued for upload, not yet started */
394
+ Pending: "pending",
395
+ /** Upload in progress */
396
+ Uploading: "uploading",
397
+ /** Upload completed successfully */
398
+ Complete: "complete",
399
+ /** Upload failed */
400
+ Failed: "failed"
401
+ };
402
+ var Asset = defineCanvasComponent(
403
+ { name: "asset", sync: "document", excludeFromHistory: ["uploadState"] },
404
+ {
405
+ /** Permanent identifier from AssetProvider (empty until upload complete) */
406
+ identifier: field3.string().max(512).default(""),
407
+ /** Current upload state */
408
+ uploadState: field3.enum(UploadState).default(UploadState.Pending)
409
+ }
410
+ );
411
+
412
+ // src/components/Color.ts
413
+ import { CanvasComponentDef as CanvasComponentDef3 } from "@woven-ecs/canvas-store";
414
+ import { field as field4 } from "@woven-ecs/core";
415
+ var ColorSchema = {
416
+ red: field4.uint8().default(0),
417
+ green: field4.uint8().default(0),
418
+ blue: field4.uint8().default(0),
419
+ alpha: field4.uint8().default(255)
420
+ };
421
+ var ColorDef = class extends CanvasComponentDef3 {
422
+ constructor() {
423
+ super({ name: "color", sync: "document" }, ColorSchema);
424
+ }
425
+ /**
426
+ * Convert a color to a hex string.
427
+ */
428
+ toHex(ctx, entityId) {
429
+ const { red, green, blue, alpha } = this.read(ctx, entityId);
430
+ const rHex = red.toString(16).padStart(2, "0");
431
+ const gHex = green.toString(16).padStart(2, "0");
432
+ const bHex = blue.toString(16).padStart(2, "0");
433
+ const aHex = alpha.toString(16).padStart(2, "0");
434
+ return `#${rHex}${gHex}${bHex}${aHex}`;
435
+ }
436
+ /**
437
+ * Set color from a hex string.
438
+ */
439
+ fromHex(ctx, entityId, hex) {
440
+ const color = this.write(ctx, entityId);
441
+ color.red = Number.parseInt(hex.slice(1, 3), 16);
442
+ color.green = Number.parseInt(hex.slice(3, 5), 16);
443
+ color.blue = Number.parseInt(hex.slice(5, 7), 16);
444
+ color.alpha = hex.length > 7 ? Number.parseInt(hex.slice(7, 9), 16) : 255;
445
+ }
446
+ };
447
+ var Color = new ColorDef();
448
+
449
+ // src/components/Connector.ts
450
+ import { defineCanvasComponent as defineCanvasComponent2 } from "@woven-ecs/canvas-store";
451
+ import { field as field5 } from "@woven-ecs/core";
452
+ var Connector = defineCanvasComponent2(
453
+ { name: "connector", sync: "document" },
454
+ {
455
+ startBlock: field5.ref(),
456
+ startBlockUv: field5.tuple(field5.float64(), 2).default([0, 0]),
457
+ startUv: field5.tuple(field5.float64(), 2).default([0, 0]),
458
+ endBlock: field5.ref(),
459
+ endBlockUv: field5.tuple(field5.float64(), 2).default([0, 0]),
460
+ endUv: field5.tuple(field5.float64(), 2).default([1, 1])
461
+ }
462
+ );
463
+
464
+ // src/components/Edited.ts
465
+ import { defineCanvasComponent as defineCanvasComponent3 } from "@woven-ecs/canvas-store";
466
+ var Edited = defineCanvasComponent3({ name: "edited" }, {});
467
+
468
+ // src/components/Held.ts
469
+ import { defineCanvasComponent as defineCanvasComponent4 } from "@woven-ecs/canvas-store";
470
+ import { field as field6 } from "@woven-ecs/core";
471
+ var Held = defineCanvasComponent4(
472
+ { name: "held", sync: "ephemeral" },
473
+ {
474
+ sessionId: field6.string().max(36).default("")
475
+ }
476
+ );
477
+
478
+ // src/components/HitGeometry.ts
479
+ import { Arc, Capsule, Mat2 } from "@woven-canvas/math";
480
+ import { CanvasComponentDef as CanvasComponentDef4 } from "@woven-ecs/canvas-store";
481
+ import { field as field7 } from "@woven-ecs/core";
482
+ var _uvToWorldMatrix = [1, 0, 0, 1, 0, 0];
483
+ var MAX_HIT_CAPSULES = 64;
484
+ var FLOATS_PER_CAPSULE = 5;
485
+ var MAX_HIT_ARCS = 2;
486
+ var FLOATS_PER_ARC = 7;
487
+ var HitGeometrySchema = {
488
+ hitCapsules: field7.buffer(field7.float32()).size(MAX_HIT_CAPSULES * FLOATS_PER_CAPSULE),
489
+ capsuleCount: field7.uint16().default(0),
490
+ hitArcs: field7.buffer(field7.float32()).size(MAX_HIT_ARCS * FLOATS_PER_ARC),
491
+ arcCount: field7.uint16().default(0)
492
+ };
493
+ var HitGeometryDef = class extends CanvasComponentDef4 {
494
+ constructor() {
495
+ super({ name: "hitHeometry" }, HitGeometrySchema);
496
+ }
497
+ /**
498
+ * Get a capsule at a specific index from the hitCapsules buffer.
499
+ * @param ctx - ECS context
500
+ * @param entityId - Entity ID
501
+ * @param index - Index of the capsule (0-based)
502
+ * @returns Capsule tuple [ax, ay, bx, by, radius]
503
+ */
504
+ getCapsuleAt(ctx, entityId, index) {
505
+ const hitGeometry = this.read(ctx, entityId);
506
+ const offset = index * FLOATS_PER_CAPSULE;
507
+ return [
508
+ hitGeometry.hitCapsules[offset],
509
+ hitGeometry.hitCapsules[offset + 1],
510
+ hitGeometry.hitCapsules[offset + 2],
511
+ hitGeometry.hitCapsules[offset + 3],
512
+ hitGeometry.hitCapsules[offset + 4]
513
+ ];
514
+ }
515
+ /**
516
+ * Set a capsule at a specific index in the hitCapsules buffer.
517
+ * @param ctx - ECS context
518
+ * @param entityId - Entity ID
519
+ * @param index - Index of the capsule (0-based)
520
+ * @param capsule - Capsule tuple [ax, ay, bx, by, radius]
521
+ */
522
+ setCapsuleAt(ctx, entityId, index, capsule) {
523
+ const hitGeometry = this.write(ctx, entityId);
524
+ const offset = index * FLOATS_PER_CAPSULE;
525
+ for (let i = 0; i < FLOATS_PER_CAPSULE; i++) {
526
+ hitGeometry.hitCapsules[offset + i] = capsule[i];
527
+ }
528
+ }
529
+ /**
530
+ * Get an arc at a specific index from the hitArcs buffer.
531
+ * @param ctx - ECS context
532
+ * @param entityId - Entity ID
533
+ * @param index - Index of the arc (0-based)
534
+ * @returns Arc tuple [ax, ay, bx, by, cx, cy, thickness]
535
+ */
536
+ getArcAt(ctx, entityId, index) {
537
+ const hitGeometry = this.read(ctx, entityId);
538
+ const offset = index * FLOATS_PER_ARC;
539
+ return [
540
+ hitGeometry.hitArcs[offset],
541
+ hitGeometry.hitArcs[offset + 1],
542
+ hitGeometry.hitArcs[offset + 2],
543
+ hitGeometry.hitArcs[offset + 3],
544
+ hitGeometry.hitArcs[offset + 4],
545
+ hitGeometry.hitArcs[offset + 5],
546
+ hitGeometry.hitArcs[offset + 6]
547
+ ];
548
+ }
549
+ /**
550
+ * Set an arc at a specific index in the hitArcs buffer.
551
+ * @param ctx - ECS context
552
+ * @param entityId - Entity ID
553
+ * @param index - Index of the arc (0-based)
554
+ * @param arc - Arc tuple [ax, ay, bx, by, cx, cy, thickness]
555
+ */
556
+ setArcAt(ctx, entityId, index, arc) {
557
+ const hitGeometry = this.write(ctx, entityId);
558
+ const offset = index * FLOATS_PER_ARC;
559
+ for (let i = 0; i < FLOATS_PER_ARC; i++) {
560
+ hitGeometry.hitArcs[offset + i] = arc[i];
561
+ }
562
+ }
563
+ /**
564
+ * Check if a point is inside the entity's hit geometry.
565
+ * Hit geometry is stored in UV coordinates (0-1) relative to the block.
566
+ * This method transforms UV geometry to world space for intersection testing.
567
+ *
568
+ * @param ctx - ECS context
569
+ * @param entityId - Entity ID
570
+ * @param point - Point to test in world coordinates [x, y]
571
+ * @returns True if the point is inside any of the entity's hit capsules or arc
572
+ */
573
+ containsPointWorld(ctx, entityId, point) {
574
+ const hitGeometry = this.read(ctx, entityId);
575
+ Block.getUvToWorldMatrix(ctx, entityId, _uvToWorldMatrix);
576
+ for (let i = 0; i < hitGeometry.arcCount; i++) {
577
+ const uvArc = this.getArcAt(ctx, entityId, i);
578
+ const worldA = [uvArc[0], uvArc[1]];
579
+ Mat2.transformPoint(_uvToWorldMatrix, worldA);
580
+ const worldB = [uvArc[2], uvArc[3]];
581
+ Mat2.transformPoint(_uvToWorldMatrix, worldB);
582
+ const worldC = [uvArc[4], uvArc[5]];
583
+ Mat2.transformPoint(_uvToWorldMatrix, worldC);
584
+ const thickness = uvArc[6];
585
+ const worldArc = Arc.create(worldA[0], worldA[1], worldB[0], worldB[1], worldC[0], worldC[1], thickness);
586
+ if (Arc.containsPoint(worldArc, point)) {
587
+ return true;
588
+ }
589
+ }
590
+ for (let i = 0; i < hitGeometry.capsuleCount; i++) {
591
+ const uvCapsule = this.getCapsuleAt(ctx, entityId, i);
592
+ const worldA = [uvCapsule[0], uvCapsule[1]];
593
+ Mat2.transformPoint(_uvToWorldMatrix, worldA);
594
+ const worldB = [uvCapsule[2], uvCapsule[3]];
595
+ Mat2.transformPoint(_uvToWorldMatrix, worldB);
596
+ const radius = uvCapsule[4];
597
+ const worldCapsule = Capsule.create(worldA[0], worldA[1], worldB[0], worldB[1], radius);
598
+ if (Capsule.containsPoint(worldCapsule, point)) {
599
+ return true;
600
+ }
601
+ }
602
+ return false;
603
+ }
604
+ /**
605
+ * Check if an AABB intersects with the entity's hit geometry.
606
+ * Hit geometry is stored in UV coordinates (0-1) relative to the block.
607
+ * This method transforms UV geometry to world space for intersection testing.
608
+ *
609
+ * @param ctx - ECS context
610
+ * @param entityId - Entity ID
611
+ * @param aabb - AABB to test in world coordinates [left, top, right, bottom]
612
+ * @returns True if the AABB intersects any of the entity's hit capsules or arc
613
+ */
614
+ intersectsAabbWorld(ctx, entityId, aabb) {
615
+ const hitGeometry = this.read(ctx, entityId);
616
+ Block.getUvToWorldMatrix(ctx, entityId, _uvToWorldMatrix);
617
+ for (let i = 0; i < hitGeometry.arcCount; i++) {
618
+ const uvArc = this.getArcAt(ctx, entityId, i);
619
+ const worldA = [uvArc[0], uvArc[1]];
620
+ Mat2.transformPoint(_uvToWorldMatrix, worldA);
621
+ const worldB = [uvArc[2], uvArc[3]];
622
+ Mat2.transformPoint(_uvToWorldMatrix, worldB);
623
+ const worldC = [uvArc[4], uvArc[5]];
624
+ Mat2.transformPoint(_uvToWorldMatrix, worldC);
625
+ const thickness = uvArc[6];
626
+ const worldArc = Arc.create(worldA[0], worldA[1], worldB[0], worldB[1], worldC[0], worldC[1], thickness);
627
+ if (Arc.intersectsAabb(worldArc, aabb)) {
628
+ return true;
629
+ }
630
+ }
631
+ for (let i = 0; i < hitGeometry.capsuleCount; i++) {
632
+ const uvCapsule = this.getCapsuleAt(ctx, entityId, i);
633
+ const worldA = [uvCapsule[0], uvCapsule[1]];
634
+ Mat2.transformPoint(_uvToWorldMatrix, worldA);
635
+ const worldB = [uvCapsule[2], uvCapsule[3]];
636
+ Mat2.transformPoint(_uvToWorldMatrix, worldB);
637
+ const radius = uvCapsule[4];
638
+ const worldCapsule = Capsule.create(worldA[0], worldA[1], worldB[0], worldB[1], radius);
639
+ if (Capsule.intersectsAabb(worldCapsule, aabb)) {
640
+ return true;
641
+ }
642
+ }
643
+ return false;
644
+ }
645
+ /**
646
+ * Get all extrema points of the hit geometry in world coordinates for AABB computation.
647
+ * Hit geometry is stored in UV coordinates; this transforms them to world space.
648
+ * Returns corner points expanded by radius/thickness for each capsule and arc.
649
+ *
650
+ * @param ctx - ECS context
651
+ * @param entityId - Entity ID
652
+ * @returns Array of extrema points in world coordinates
653
+ */
654
+ getExtremaWorld(ctx, entityId) {
655
+ const hitGeometry = this.read(ctx, entityId);
656
+ const pts = [];
657
+ Block.getUvToWorldMatrix(ctx, entityId, _uvToWorldMatrix);
658
+ for (let i = 0; i < hitGeometry.capsuleCount; i++) {
659
+ const uvCapsule = this.getCapsuleAt(ctx, entityId, i);
660
+ const worldA = [uvCapsule[0], uvCapsule[1]];
661
+ Mat2.transformPoint(_uvToWorldMatrix, worldA);
662
+ const worldB = [uvCapsule[2], uvCapsule[3]];
663
+ Mat2.transformPoint(_uvToWorldMatrix, worldB);
664
+ const radius = uvCapsule[4];
665
+ const worldCapsule = Capsule.create(worldA[0], worldA[1], worldB[0], worldB[1], radius);
666
+ pts.push(...Capsule.getExtrema(worldCapsule));
667
+ }
668
+ for (let i = 0; i < hitGeometry.arcCount; i++) {
669
+ const uvArc = this.getArcAt(ctx, entityId, i);
670
+ const worldA = [uvArc[0], uvArc[1]];
671
+ Mat2.transformPoint(_uvToWorldMatrix, worldA);
672
+ const worldB = [uvArc[2], uvArc[3]];
673
+ Mat2.transformPoint(_uvToWorldMatrix, worldB);
674
+ const worldC = [uvArc[4], uvArc[5]];
675
+ Mat2.transformPoint(_uvToWorldMatrix, worldC);
676
+ const thickness = uvArc[6];
677
+ const worldArc = Arc.create(worldA[0], worldA[1], worldB[0], worldB[1], worldC[0], worldC[1], thickness);
678
+ pts.push(...Arc.getExtrema(worldArc));
679
+ }
680
+ return pts;
681
+ }
682
+ // ============================================
683
+ // User-facing helper methods for adding hit geometry
684
+ // ============================================
685
+ /**
686
+ * Add a capsule from a capsule tuple [ax, ay, bx, by, radius].
687
+ * Positions are in UV coordinates (0-1), radius is in world units.
688
+ *
689
+ * @param ctx - ECS context
690
+ * @param entityId - Entity ID
691
+ * @param capsule - Capsule tuple [ax, ay, bx, by, radius]
692
+ */
693
+ addCapsule(ctx, entityId, capsule) {
694
+ const hitGeometry = this.write(ctx, entityId);
695
+ const index = hitGeometry.capsuleCount;
696
+ if (index >= MAX_HIT_CAPSULES) {
697
+ console.warn(`HitGeometry: Max capsules (${MAX_HIT_CAPSULES}) reached`);
698
+ return;
699
+ }
700
+ this.setCapsuleAt(ctx, entityId, index, capsule);
701
+ hitGeometry.capsuleCount = index + 1;
702
+ }
703
+ /**
704
+ * Add a capsule using UV coordinates.
705
+ * UV (0,0) is top-left, (1,1) is bottom-right of the block.
706
+ *
707
+ * @param ctx - ECS context
708
+ * @param entityId - Entity ID
709
+ * @param uvA - First endpoint in UV coordinates [0-1, 0-1]
710
+ * @param uvB - Second endpoint in UV coordinates [0-1, 0-1]
711
+ * @param worldRadius - Radius in world units (pixels)
712
+ */
713
+ addCapsuleUv(ctx, entityId, uvA, uvB, worldRadius) {
714
+ const capsule = [uvA[0], uvA[1], uvB[0], uvB[1], worldRadius];
715
+ this.addCapsule(ctx, entityId, capsule);
716
+ }
717
+ /**
718
+ * Add a capsule using world coordinates.
719
+ * Automatically converts to UV coordinates relative to the block.
720
+ *
721
+ * @param ctx - ECS context
722
+ * @param entityId - Entity ID
723
+ * @param worldA - First endpoint in world coordinates
724
+ * @param worldB - Second endpoint in world coordinates
725
+ * @param worldRadius - Radius in world units (pixels)
726
+ */
727
+ addCapsuleWorld(ctx, entityId, worldA, worldB, worldRadius) {
728
+ const uvA = Block.worldToUv(ctx, entityId, worldA);
729
+ const uvB = Block.worldToUv(ctx, entityId, worldB);
730
+ this.addCapsuleUv(ctx, entityId, uvA, uvB, worldRadius);
731
+ }
732
+ /**
733
+ * Add an arc from an arc tuple [aX, aY, bX, bY, cX, cY, thickness].
734
+ * Positions are in UV coordinates (0-1), thickness is in world units.
735
+ *
736
+ * @param ctx - ECS context
737
+ * @param entityId - Entity ID
738
+ * @param arc - Arc tuple [aX, aY, bX, bY, cX, cY, thickness]
739
+ */
740
+ addArc(ctx, entityId, arc) {
741
+ const hitGeometry = this.write(ctx, entityId);
742
+ const index = hitGeometry.arcCount;
743
+ if (index >= MAX_HIT_ARCS) {
744
+ console.warn(`HitGeometry: Max arcs (${MAX_HIT_ARCS}) reached`);
745
+ return;
746
+ }
747
+ this.setArcAt(ctx, entityId, index, arc);
748
+ hitGeometry.arcCount = index + 1;
749
+ }
750
+ /**
751
+ * Add an arc using UV coordinates.
752
+ * UV (0,0) is top-left, (1,1) is bottom-right of the block.
753
+ *
754
+ * @param ctx - ECS context
755
+ * @param entityId - Entity ID
756
+ * @param uvA - Start point in UV coordinates
757
+ * @param uvB - Control point in UV coordinates
758
+ * @param uvC - End point in UV coordinates
759
+ * @param worldThickness - Thickness in world units (pixels)
760
+ */
761
+ addArcUv(ctx, entityId, uvA, uvB, uvC, worldThickness) {
762
+ const arc = [uvA[0], uvA[1], uvB[0], uvB[1], uvC[0], uvC[1], worldThickness];
763
+ this.addArc(ctx, entityId, arc);
764
+ }
765
+ /**
766
+ * Add an arc using world coordinates.
767
+ * Automatically converts to UV coordinates relative to the block.
768
+ *
769
+ * @param ctx - ECS context
770
+ * @param entityId - Entity ID
771
+ * @param worldA - Start point in world coordinates
772
+ * @param worldB - Control point in world coordinates
773
+ * @param worldC - End point in world coordinates
774
+ * @param worldThickness - Thickness in world units (pixels)
775
+ */
776
+ addArcWorld(ctx, entityId, worldA, worldB, worldC, worldThickness) {
777
+ const uvA = Block.worldToUv(ctx, entityId, worldA);
778
+ const uvB = Block.worldToUv(ctx, entityId, worldB);
779
+ const uvC = Block.worldToUv(ctx, entityId, worldC);
780
+ this.addArcUv(ctx, entityId, uvA, uvB, uvC, worldThickness);
781
+ }
782
+ /**
783
+ * Clear all hit geometry from an entity.
784
+ *
785
+ * @param ctx - ECS context
786
+ * @param entityId - Entity ID
787
+ */
788
+ clear(ctx, entityId) {
789
+ const hitGeometry = this.write(ctx, entityId);
790
+ hitGeometry.capsuleCount = 0;
791
+ hitGeometry.arcCount = 0;
792
+ }
793
+ };
794
+ var HitGeometry = new HitGeometryDef();
795
+
796
+ // src/components/Hovered.ts
797
+ import { defineCanvasComponent as defineCanvasComponent5 } from "@woven-ecs/canvas-store";
798
+ var Hovered = defineCanvasComponent5({ name: "hovered" }, {});
799
+
800
+ // src/components/Image.ts
801
+ import { defineCanvasComponent as defineCanvasComponent6 } from "@woven-ecs/canvas-store";
802
+ import { field as field8 } from "@woven-ecs/core";
803
+ var Image = defineCanvasComponent6(
804
+ { name: "image", sync: "document" },
805
+ {
806
+ /** Image width in pixels */
807
+ width: field8.uint16().default(0),
808
+ /** Image height in pixels */
809
+ height: field8.uint16().default(0),
810
+ /** Alt text for accessibility */
811
+ alt: field8.string().max(256).default("")
812
+ }
813
+ );
814
+
815
+ // src/components/Opacity.ts
816
+ import { defineCanvasComponent as defineCanvasComponent7 } from "@woven-ecs/canvas-store";
817
+ import { field as field9 } from "@woven-ecs/core";
818
+ var Opacity = defineCanvasComponent7(
819
+ { name: "opacity" },
820
+ {
821
+ value: field9.uint8().default(255)
822
+ }
823
+ );
824
+
825
+ // src/components/Pointer.ts
826
+ import { CanvasComponentDef as CanvasComponentDef5 } from "@woven-ecs/canvas-store";
827
+ import { field as field10 } from "@woven-ecs/core";
828
+ var PointerButton = {
829
+ None: "none",
830
+ Left: "left",
831
+ Middle: "middle",
832
+ Right: "right",
833
+ Back: "back",
834
+ Forward: "forward"
835
+ };
836
+ var PointerType = {
837
+ Mouse: "mouse",
838
+ Pen: "pen",
839
+ Touch: "touch"
840
+ };
841
+ var SAMPLE_COUNT = 6;
842
+ var PointerSchema = {
843
+ /** Unique pointer ID (from PointerEvent.pointerId) */
844
+ pointerId: field10.uint16().default(0),
845
+ /** Current position relative to the editor element [x, y] */
846
+ position: field10.tuple(field10.float32(), 2).default([0, 0]),
847
+ /** Position where the pointer went down [x, y] */
848
+ downPosition: field10.tuple(field10.float32(), 2).default([0, 0]),
849
+ /** Frame number when the pointer went down (for click detection) */
850
+ downFrame: field10.uint32().default(0),
851
+ /** Which button is pressed */
852
+ button: field10.enum(PointerButton).default(PointerButton.None),
853
+ /** Type of pointer device */
854
+ pointerType: field10.enum(PointerType).default(PointerType.Mouse),
855
+ /** Pressure from 0 to 1 (for pen/touch) */
856
+ pressure: field10.float32().default(0),
857
+ /** Whether the pointer event target was not the editor element */
858
+ obscured: field10.boolean().default(false),
859
+ // Velocity tracking (ring buffer for position samples)
860
+ /** Ring buffer of previous positions [x0, y0, x1, y1, ...] @internal */
861
+ _prevPositions: field10.array(field10.float32(), SAMPLE_COUNT * 2),
862
+ /** Ring buffer of timestamps for each position sample @internal */
863
+ _prevTimes: field10.array(field10.float32(), SAMPLE_COUNT),
864
+ /** Total number of samples added (used for ring buffer indexing) @internal */
865
+ _sampleCount: field10.int32().default(0),
866
+ /** Computed velocity [vx, vy] in pixels per second @internal */
867
+ _velocity: field10.tuple(field10.float32(), 2).default([0, 0])
868
+ };
869
+ var PointerDef = class extends CanvasComponentDef5 {
870
+ constructor() {
871
+ super({ name: "pointer" }, PointerSchema);
872
+ }
873
+ /** Get the computed velocity of a pointer */
874
+ getVelocity(ctx, entityId) {
875
+ const p = this.read(ctx, entityId);
876
+ return [p._velocity[0], p._velocity[1]];
877
+ }
878
+ /**
879
+ * Get the pointer button from a PointerEvent button number.
880
+ * @param button - PointerEvent.button value
881
+ * @returns PointerButton enum value
882
+ */
883
+ getButton(button) {
884
+ switch (button) {
885
+ case 0:
886
+ return PointerButton.Left;
887
+ case 1:
888
+ return PointerButton.Middle;
889
+ case 2:
890
+ return PointerButton.Right;
891
+ case 3:
892
+ return PointerButton.Back;
893
+ case 4:
894
+ return PointerButton.Forward;
895
+ default:
896
+ return PointerButton.None;
897
+ }
898
+ }
899
+ /**
900
+ * Get the pointer type from a PointerEvent pointerType string.
901
+ * @param pointerType - PointerEvent.pointerType value
902
+ * @returns PointerType enum value
903
+ */
904
+ getType(pointerType) {
905
+ switch (pointerType) {
906
+ case "pen":
907
+ return PointerType.Pen;
908
+ case "touch":
909
+ return PointerType.Touch;
910
+ default:
911
+ return PointerType.Mouse;
912
+ }
913
+ }
914
+ };
915
+ var Pointer = new PointerDef();
916
+ function addPointerSample(pointer, position, time) {
917
+ const currentIndex = pointer._sampleCount % SAMPLE_COUNT;
918
+ const mostRecentTime = pointer._prevTimes[currentIndex] || 0;
919
+ if (Math.abs(mostRecentTime - time) < 1e-3) return;
920
+ pointer.position = position;
921
+ pointer._sampleCount++;
922
+ const writeIndex = pointer._sampleCount % SAMPLE_COUNT;
923
+ pointer._prevPositions[writeIndex * 2] = position[0];
924
+ pointer._prevPositions[writeIndex * 2 + 1] = position[1];
925
+ pointer._prevTimes[writeIndex] = time;
926
+ const pointCount = Math.min(pointer._sampleCount, SAMPLE_COUNT);
927
+ if (pointCount <= 1) {
928
+ pointer._velocity = [0, 0];
929
+ return;
930
+ }
931
+ const mod = (n) => (n % SAMPLE_COUNT + SAMPLE_COUNT) % SAMPLE_COUNT;
932
+ const TAU = 0.04;
933
+ const EPS = 1e-6;
934
+ let W = 0;
935
+ let WU = 0;
936
+ let WUU = 0;
937
+ let WX = 0;
938
+ let WY = 0;
939
+ let WU_X = 0;
940
+ let WU_Y = 0;
941
+ for (let j = 0; j < pointCount; j++) {
942
+ const idx = mod(pointer._sampleCount - pointCount + 1 + j);
943
+ const t = pointer._prevTimes[idx] || 0;
944
+ const u = t - time;
945
+ const recency = -u;
946
+ if (recency > 5 * TAU) continue;
947
+ const w = Math.exp(-recency / TAU);
948
+ const x = pointer._prevPositions[idx * 2] || 0;
949
+ const y = pointer._prevPositions[idx * 2 + 1] || 0;
950
+ W += w;
951
+ WU += w * u;
952
+ WUU += w * u * u;
953
+ WX += w * x;
954
+ WY += w * y;
955
+ WU_X += w * u * x;
956
+ WU_Y += w * u * y;
957
+ }
958
+ const denom = W * WUU - WU * WU;
959
+ if (Math.abs(denom) <= EPS) {
960
+ const iCurr = pointer._sampleCount % SAMPLE_COUNT;
961
+ const iPrev = mod(pointer._sampleCount - 1);
962
+ const dt = (pointer._prevTimes[iCurr] || 0) - (pointer._prevTimes[iPrev] || 0);
963
+ if (dt > EPS) {
964
+ const dx = (pointer._prevPositions[iCurr * 2] || 0) - (pointer._prevPositions[iPrev * 2] || 0);
965
+ const dy = (pointer._prevPositions[iCurr * 2 + 1] || 0) - (pointer._prevPositions[iPrev * 2 + 1] || 0);
966
+ pointer._velocity = [dx / dt, dy / dt];
967
+ } else {
968
+ pointer._velocity = [0, 0];
969
+ }
970
+ return;
971
+ }
972
+ const vx = (W * WU_X - WU * WX) / denom;
973
+ const vy = (W * WU_Y - WU * WY) / denom;
974
+ pointer._velocity = [vx, vy];
975
+ }
976
+
977
+ // src/components/ScaleWithZoom.ts
978
+ import { defineCanvasComponent as defineCanvasComponent8 } from "@woven-ecs/canvas-store";
979
+ import { field as field11 } from "@woven-ecs/core";
980
+ var ScaleWithZoom = defineCanvasComponent8(
981
+ { name: "scaleWithZoom" },
982
+ {
983
+ /** Pivot point for scaling as [x, y] (0-1, default 0.5,0.5 = center) */
984
+ anchor: field11.tuple(field11.float64(), 2).default([0.5, 0.5]),
985
+ /** Initial position as [left, top] at zoom=1 */
986
+ startPosition: field11.tuple(field11.float64(), 2).default([0, 0]),
987
+ /** Initial size as [width, height] at zoom=1 */
988
+ startSize: field11.tuple(field11.float64(), 2).default([0, 0]),
989
+ /** Scale multiplier per dimension: [x, y] (0 = no zoom effect, 1 = full zoom effect, 0.5 = half effect) */
990
+ scaleMultiplier: field11.tuple(field11.float64(), 2).default([1, 1])
991
+ }
992
+ );
993
+
994
+ // src/components/Shape.ts
995
+ import { defineCanvasComponent as defineCanvasComponent9 } from "@woven-ecs/canvas-store";
996
+ import { field as field12 } from "@woven-ecs/core";
997
+ var StrokeKind = {
998
+ Solid: "solid",
999
+ Dashed: "dashed",
1000
+ None: "none"
1001
+ };
1002
+ var Shape = defineCanvasComponent9(
1003
+ { name: "shape", sync: "document" },
1004
+ {
1005
+ /** The kind of shape to render (e.g. 'rectangle', 'ellipse', or custom shape key) */
1006
+ kind: field12.string().default("rectangle"),
1007
+ /** Stroke style */
1008
+ strokeKind: field12.enum(StrokeKind).default(StrokeKind.Solid),
1009
+ /** Stroke width in pixels */
1010
+ strokeWidth: field12.uint16().default(2),
1011
+ /** Stroke color - red component (0-255) */
1012
+ strokeRed: field12.uint8().default(0),
1013
+ /** Stroke color - green component (0-255) */
1014
+ strokeGreen: field12.uint8().default(0),
1015
+ /** Stroke color - blue component (0-255) */
1016
+ strokeBlue: field12.uint8().default(0),
1017
+ /** Stroke color - alpha component (0-255) */
1018
+ strokeAlpha: field12.uint8().default(255),
1019
+ /** Fill color - red component (0-255) */
1020
+ fillRed: field12.uint8().default(255),
1021
+ /** Fill color - green component (0-255) */
1022
+ fillGreen: field12.uint8().default(255),
1023
+ /** Fill color - blue component (0-255) */
1024
+ fillBlue: field12.uint8().default(255),
1025
+ /** Fill color - alpha component (0-255) */
1026
+ fillAlpha: field12.uint8().default(0)
1027
+ }
1028
+ );
1029
+
1030
+ // src/components/Text.ts
1031
+ import { defineCanvasComponent as defineCanvasComponent10 } from "@woven-ecs/canvas-store";
1032
+ import { field as field13 } from "@woven-ecs/core";
1033
+
1034
+ // src/types.ts
1035
+ import { z } from "zod";
1036
+ function generateUserColor() {
1037
+ const colors = [
1038
+ "#f43f5e",
1039
+ // rose
1040
+ "#ec4899",
1041
+ // pink
1042
+ "#a855f7",
1043
+ // purple
1044
+ "#6366f1",
1045
+ // indigo
1046
+ "#3b82f6",
1047
+ // blue
1048
+ "#0ea5e9",
1049
+ // sky
1050
+ "#14b8a6",
1051
+ // teal
1052
+ "#22c55e",
1053
+ // green
1054
+ "#eab308",
1055
+ // yellow
1056
+ "#f97316"
1057
+ // orange
1058
+ ];
1059
+ return colors[Math.floor(Math.random() * colors.length)];
1060
+ }
1061
+ var UserData = z.object({
1062
+ userId: z.string().max(36).default(() => crypto.randomUUID()),
1063
+ sessionId: z.string().max(36).default(() => crypto.randomUUID()),
1064
+ color: z.string().max(7).default(generateUserColor),
1065
+ name: z.string().max(100).default("Anonymous"),
1066
+ avatar: z.string().max(500).default("")
1067
+ });
1068
+ function getPluginResources(ctx, pluginName) {
1069
+ const resources = ctx.resources;
1070
+ return resources.pluginResources[pluginName];
1071
+ }
1072
+ var GridOptions = z.object({
1073
+ /**
1074
+ * Whether grid snapping is enabled.
1075
+ * @default true
1076
+ */
1077
+ enabled: z.boolean().default(true),
1078
+ /**
1079
+ * Whether resized/rotated objects must stay aligned to the grid.
1080
+ * If true, objects snap to grid during resize/rotate.
1081
+ * If false, objects scale proportionally to the transform box, which may
1082
+ * cause them to be unaligned with the grid.
1083
+ * @default false
1084
+ */
1085
+ strict: z.boolean().default(false),
1086
+ /**
1087
+ * Width of each grid column in world units.
1088
+ * @default 20
1089
+ */
1090
+ colWidth: z.number().nonnegative().default(20),
1091
+ /**
1092
+ * Height of each grid row in world units.
1093
+ * @default 20
1094
+ */
1095
+ rowHeight: z.number().nonnegative().default(20),
1096
+ /**
1097
+ * Angular snap increment in radians when grid is enabled.
1098
+ * @default Math.PI / 36 (5 degrees)
1099
+ */
1100
+ snapAngleRad: z.number().nonnegative().default(Math.PI / 36),
1101
+ /**
1102
+ * Angular snap increment in radians when shift key is held.
1103
+ * @default Math.PI / 12 (15 degrees)
1104
+ */
1105
+ shiftSnapAngleRad: z.number().nonnegative().default(Math.PI / 12)
1106
+ });
1107
+ var ControlsOptions = z.object({
1108
+ /**
1109
+ * Tool activated by left mouse button.
1110
+ * @default 'select'
1111
+ */
1112
+ leftMouseTool: z.string().max(32).default("select"),
1113
+ /**
1114
+ * Tool activated by middle mouse button.
1115
+ * @default 'hand'
1116
+ */
1117
+ middleMouseTool: z.string().max(32).default("hand"),
1118
+ /**
1119
+ * Tool activated by right mouse button.
1120
+ * @default 'menu'
1121
+ */
1122
+ rightMouseTool: z.string().max(32).default("menu"),
1123
+ /**
1124
+ * Tool activated by mouse wheel.
1125
+ * @default 'scroll'
1126
+ */
1127
+ wheelTool: z.string().max(32).default("scroll"),
1128
+ /**
1129
+ * Tool activated by mouse wheel with modifier key held.
1130
+ * @default 'zoom'
1131
+ */
1132
+ modWheelTool: z.string().max(32).default("zoom")
1133
+ });
1134
+ var EditorOptionsSchema = z.object({
1135
+ /**
1136
+ * Plugins to load.
1137
+ * Plugins are sorted by dependencies automatically.
1138
+ */
1139
+ plugins: z.array(z.custom()).default([]),
1140
+ /**
1141
+ * Maximum number of entities.
1142
+ * @default 5_000
1143
+ */
1144
+ maxEntities: z.number().default(5e3),
1145
+ /**
1146
+ * User data for presence tracking.
1147
+ * All fields are optional - defaults will be applied.
1148
+ */
1149
+ user: z.custom().default({}),
1150
+ /**
1151
+ * Grid configuration for snap-to-grid behavior.
1152
+ * All fields are optional - defaults will be applied.
1153
+ */
1154
+ grid: z.custom().default({}),
1155
+ /**
1156
+ * Custom block definitions.
1157
+ * Accepts partial block definitions - defaults will be applied automatically.
1158
+ */
1159
+ blockDefs: z.array(z.custom()).default([]),
1160
+ /**
1161
+ * Keybind definitions for keyboard shortcuts.
1162
+ * These map key combinations to plugin commands.
1163
+ */
1164
+ keybinds: z.array(z.custom()).default([]),
1165
+ /**
1166
+ * Custom cursor definitions.
1167
+ * Override default cursors or define new ones for transform operations.
1168
+ */
1169
+ cursors: z.record(z.string(), z.custom()).default({}),
1170
+ /**
1171
+ * Custom components to register without creating a plugin.
1172
+ */
1173
+ components: z.array(z.custom()).default([]),
1174
+ /**
1175
+ * Custom singletons to register without creating a plugin.
1176
+ */
1177
+ singletons: z.array(z.custom()).default([]),
1178
+ /**
1179
+ * Custom systems to register without creating a plugin.
1180
+ * Each system specifies its phase and priority.
1181
+ */
1182
+ systems: z.array(z.custom()).default([]),
1183
+ /**
1184
+ * Custom font families to load and make available in the font selector.
1185
+ * Fonts will be loaded automatically during editor initialization.
1186
+ */
1187
+ fonts: z.array(z.custom()).default([]),
1188
+ /**
1189
+ * If true, keybinds from plugins will be ignored.
1190
+ * Use this when you want full control over keybinds.
1191
+ */
1192
+ omitPluginKeybinds: z.boolean().default(false),
1193
+ /**
1194
+ * If true, cursors from plugins will be ignored.
1195
+ * Use this when you want full control over cursors.
1196
+ */
1197
+ omitPluginCursors: z.boolean().default(false),
1198
+ /**
1199
+ * If true, fonts from plugins will be ignored.
1200
+ * Use this when you want full control over fonts.
1201
+ */
1202
+ omitPluginFonts: z.boolean().default(false),
1203
+ /**
1204
+ * Initial controls configuration.
1205
+ * Override default tool mappings for mouse buttons, wheel, etc.
1206
+ *
1207
+ * @example
1208
+ * ```typescript
1209
+ * controls: {
1210
+ * leftMouseTool: 'pen', // Start with pen tool active
1211
+ * middleMouseTool: 'hand', // Middle mouse pans
1212
+ * }
1213
+ * ```
1214
+ */
1215
+ controls: z.custom().optional()
1216
+ });
1217
+ var Keybind = z.object({
1218
+ /** The command to execute when this keybind is triggered */
1219
+ command: z.string(),
1220
+ /** The key index from the Key constants (e.g., Key.A, Key.Delete) */
1221
+ key: z.number(),
1222
+ /** Whether the modifier key (Cmd on Mac, Ctrl on Windows) must be held */
1223
+ mod: z.boolean().optional(),
1224
+ /** Whether the Shift key must be held */
1225
+ shift: z.boolean().optional()
1226
+ });
1227
+ var CursorDef = z.object({
1228
+ /** Function that generates the SVG string for a given rotation angle (in radians) */
1229
+ makeSvg: z.function({ input: z.tuple([z.number()]), output: z.string() }),
1230
+ /** Hotspot coordinates [x, y] for the cursor */
1231
+ hotspot: z.tuple([z.number(), z.number()]),
1232
+ /** Base rotation offset applied before the dynamic rotation */
1233
+ rotationOffset: z.number()
1234
+ });
1235
+ var VerticalAlignment = {
1236
+ Top: "top",
1237
+ Center: "center",
1238
+ Bottom: "bottom"
1239
+ };
1240
+ var TextAlignment = {
1241
+ Left: "left",
1242
+ Center: "center",
1243
+ Right: "right",
1244
+ Justify: "justify"
1245
+ };
1246
+ var BlockDefEditOptions = z.object({
1247
+ canEdit: z.boolean().default(false),
1248
+ removeWhenTextEmpty: z.boolean().default(false)
1249
+ });
1250
+ var BlockDefConnectors = z.object({
1251
+ /** Whether connectors are enabled for this block type */
1252
+ enabled: z.boolean().default(true),
1253
+ /** UV coordinates of terminal positions (0-1 range, where [0,0] is top-left) */
1254
+ terminals: z.array(z.tuple([z.number(), z.number()])).default([
1255
+ [0.5, 0],
1256
+ // top
1257
+ [0.5, 1],
1258
+ // bottom
1259
+ [0.5, 0.5],
1260
+ // center
1261
+ [0, 0.5],
1262
+ // left
1263
+ [1, 0.5]
1264
+ // right
1265
+ ])
1266
+ });
1267
+ var Stratum = z.enum(["background", "content", "overlay"]);
1268
+ var BlockDef2 = z.object({
1269
+ tag: z.string(),
1270
+ stratum: Stratum.default("content"),
1271
+ editOptions: BlockDefEditOptions.default(BlockDefEditOptions.parse({})),
1272
+ components: z.array(z.custom()).default([]),
1273
+ resizeMode: z.enum(["scale", "text", "free", "groupOnly"]).default("scale"),
1274
+ canRotate: z.boolean().default(true),
1275
+ canScale: z.boolean().default(true),
1276
+ connectors: BlockDefConnectors.default(BlockDefConnectors.parse({}))
1277
+ });
1278
+
1279
+ // src/components/Text.ts
1280
+ var Text = defineCanvasComponent10(
1281
+ { name: "text", sync: "document" },
1282
+ {
1283
+ /** HTML content (supports rich text formatting) */
1284
+ content: field13.string().max(1e4).default(""),
1285
+ /** Font size in pixels */
1286
+ fontSizePx: field13.float64().default(24),
1287
+ /** Font family name */
1288
+ fontFamily: field13.string().max(64).default("Figtree"),
1289
+ /** Line height multiplier */
1290
+ lineHeight: field13.float64().default(1.2),
1291
+ /** Letter spacing in em units */
1292
+ letterSpacingEm: field13.float64().default(0),
1293
+ /** Whether width is constrained (text wraps) */
1294
+ constrainWidth: field13.boolean().default(true),
1295
+ /** Default text alignment for new paragraphs */
1296
+ defaultAlignment: field13.enum(TextAlignment).default(TextAlignment.Left)
1297
+ }
1298
+ );
1299
+
1300
+ // src/components/User.ts
1301
+ import { defineCanvasComponent as defineCanvasComponent11 } from "@woven-ecs/canvas-store";
1302
+ import { field as field14 } from "@woven-ecs/core";
1303
+ var User = defineCanvasComponent11(
1304
+ { name: "user", sync: "ephemeral" },
1305
+ {
1306
+ userId: field14.string().max(36),
1307
+ sessionId: field14.string().max(36),
1308
+ color: field14.string().max(7),
1309
+ name: field14.string().max(100),
1310
+ avatar: field14.string().max(500),
1311
+ position: field14.tuple(field14.float32(), 2).default([0, 0])
1312
+ }
1313
+ );
1314
+
1315
+ // src/components/VerticalAlign.ts
1316
+ import { defineCanvasComponent as defineCanvasComponent12 } from "@woven-ecs/canvas-store";
1317
+ import { field as field15 } from "@woven-ecs/core";
1318
+ var VerticalAlign = defineCanvasComponent12(
1319
+ { name: "verticalAlign", sync: "document" },
1320
+ {
1321
+ value: field15.enum(VerticalAlignment).default(VerticalAlignment.Top)
1322
+ }
1323
+ );
1324
+
1325
+ // src/constants.ts
1326
+ var PLUGIN_NAME = "core";
1327
+ var STRATUM_ORDER = {
1328
+ background: 0,
1329
+ content: 1,
1330
+ overlay: 2
1331
+ };
1332
+
1333
+ // src/singletons/index.ts
1334
+ var singletons_exports = {};
1335
+ __export(singletons_exports, {
1336
+ Camera: () => Camera,
1337
+ Controls: () => Controls,
1338
+ Cursor: () => Cursor,
1339
+ Frame: () => Frame,
1340
+ Grid: () => Grid,
1341
+ Intersect: () => Intersect,
1342
+ Key: () => Key,
1343
+ Keyboard: () => Keyboard,
1344
+ Mouse: () => Mouse,
1345
+ RankBounds: () => RankBounds,
1346
+ ScaleWithZoomState: () => ScaleWithZoomState,
1347
+ Screen: () => Screen,
1348
+ clearBits: () => clearBits,
1349
+ codeToIndex: () => codeToIndex,
1350
+ setBit: () => setBit
1351
+ });
1352
+
1353
+ // src/singletons/Camera.ts
1354
+ import { CanvasSingletonDef as CanvasSingletonDef2 } from "@woven-ecs/canvas-store";
1355
+ import { field as field17 } from "@woven-ecs/core";
1356
+
1357
+ // src/singletons/Screen.ts
1358
+ import { CanvasSingletonDef } from "@woven-ecs/canvas-store";
1359
+ import { field as field16 } from "@woven-ecs/core";
1360
+ var ScreenSchema = {
1361
+ /** Width of the editor element in pixels */
1362
+ width: field16.float64().default(0),
1363
+ /** Height of the editor element in pixels */
1364
+ height: field16.float64().default(0),
1365
+ /** Left offset of the editor element relative to the viewport */
1366
+ left: field16.float64().default(0),
1367
+ /** Top offset of the editor element relative to the viewport */
1368
+ top: field16.float64().default(0)
1369
+ };
1370
+ var ScreenDef = class extends CanvasSingletonDef {
1371
+ constructor() {
1372
+ super({ name: "screen" }, ScreenSchema);
1373
+ }
1374
+ /** Get screen dimensions as [width, height] */
1375
+ getSize(ctx) {
1376
+ const s = this.read(ctx);
1377
+ return [s.width, s.height];
1378
+ }
1379
+ /** Get screen position as [left, top] */
1380
+ getPosition(ctx) {
1381
+ const s = this.read(ctx);
1382
+ return [s.left, s.top];
1383
+ }
1384
+ /** Get the center point of the screen */
1385
+ getCenter(ctx) {
1386
+ const s = this.read(ctx);
1387
+ return [s.left + s.width / 2, s.top + s.height / 2];
1388
+ }
1389
+ };
1390
+ var Screen = new ScreenDef();
1391
+
1392
+ // src/singletons/Camera.ts
1393
+ var CameraSchema = {
1394
+ /** Top position of the camera in world coordinates */
1395
+ top: field17.float64().default(0),
1396
+ /** Left position of the camera in world coordinates */
1397
+ left: field17.float64().default(0),
1398
+ /** Zoom level (1 = 100%, 2 = 200%, 0.5 = 50%) */
1399
+ zoom: field17.float64().default(1),
1400
+ /** Whether the camera viewport intersects any blocks */
1401
+ canSeeBlocks: field17.boolean().default(true),
1402
+ /** Reference to a block that the camera can currently see (for optimization) */
1403
+ lastSeenBlock: field17.ref()
1404
+ };
1405
+ var CameraDef = class extends CanvasSingletonDef2 {
1406
+ constructor() {
1407
+ super({ name: "camera", sync: "local" }, CameraSchema);
1408
+ }
1409
+ /**
1410
+ * Convert screen coordinates to world coordinates.
1411
+ * @param ctx - ECS context
1412
+ * @param screenPos - Position in screen pixels [x, y]
1413
+ * @returns Position in world coordinates [x, y]
1414
+ */
1415
+ toWorld(ctx, screenPos) {
1416
+ const camera = this.read(ctx);
1417
+ const worldX = camera.left + screenPos[0] / camera.zoom;
1418
+ const worldY = camera.top + screenPos[1] / camera.zoom;
1419
+ return [worldX, worldY];
1420
+ }
1421
+ /**
1422
+ * Convert world coordinates to screen coordinates.
1423
+ * @param ctx - ECS context
1424
+ * @param worldPos - Position in world coordinates [x, y]
1425
+ * @returns Position in screen pixels [x, y]
1426
+ */
1427
+ toScreen(ctx, worldPos) {
1428
+ const camera = this.read(ctx);
1429
+ const screenX = (worldPos[0] - camera.left) * camera.zoom;
1430
+ const screenY = (worldPos[1] - camera.top) * camera.zoom;
1431
+ return [screenX, screenY];
1432
+ }
1433
+ /**
1434
+ * Get the world coordinates of the viewport center.
1435
+ * @param ctx - ECS context
1436
+ * @returns Center position in world coordinates [x, y]
1437
+ */
1438
+ getWorldCenter(ctx) {
1439
+ const camera = this.read(ctx);
1440
+ const screen = Screen.read(ctx);
1441
+ return [camera.left + screen.width / camera.zoom / 2, camera.top + screen.height / camera.zoom / 2];
1442
+ }
1443
+ /**
1444
+ * Get the world-space bounds of the visible viewport.
1445
+ * @param ctx - ECS context
1446
+ * @returns Bounds as { left, top, right, bottom } in world coordinates
1447
+ */
1448
+ getWorldBounds(ctx) {
1449
+ const camera = this.read(ctx);
1450
+ const screen = Screen.read(ctx);
1451
+ return {
1452
+ left: camera.left,
1453
+ top: camera.top,
1454
+ right: camera.left + screen.width / camera.zoom,
1455
+ bottom: camera.top + screen.height / camera.zoom
1456
+ };
1457
+ }
1458
+ /**
1459
+ * Get the camera viewport as an AABB tuple [left, top, right, bottom].
1460
+ * @param ctx - ECS context
1461
+ * @param out - Optional output array to write to (avoids allocation)
1462
+ * @returns AABB tuple in world coordinates
1463
+ */
1464
+ getAabb(ctx, out) {
1465
+ const camera = this.read(ctx);
1466
+ const screen = Screen.read(ctx);
1467
+ const result = out ?? [0, 0, 0, 0];
1468
+ result[0] = camera.left;
1469
+ result[1] = camera.top;
1470
+ result[2] = camera.left + screen.width / camera.zoom;
1471
+ result[3] = camera.top + screen.height / camera.zoom;
1472
+ return result;
1473
+ }
1474
+ };
1475
+ var Camera = new CameraDef();
1476
+
1477
+ // src/singletons/Controls.ts
1478
+ import { CanvasSingletonDef as CanvasSingletonDef3 } from "@woven-ecs/canvas-store";
1479
+ import { field as field18 } from "@woven-ecs/core";
1480
+ var ControlsSchema = {
1481
+ /** Tool activated by left mouse button */
1482
+ leftMouseTool: field18.string().max(32).default("select"),
1483
+ /** Tool activated by middle mouse button */
1484
+ middleMouseTool: field18.string().max(32).default("hand"),
1485
+ /** Tool activated by right mouse button */
1486
+ rightMouseTool: field18.string().max(32).default("menu"),
1487
+ /** Tool activated by mouse wheel */
1488
+ wheelTool: field18.string().max(32).default("scroll"),
1489
+ /** Tool activated by mouse wheel with modifier key held */
1490
+ modWheelTool: field18.string().max(32).default("zoom"),
1491
+ /** JSON snapshot of block to place on next click (empty string = no placement active) */
1492
+ heldSnapshot: field18.string().max(4096).default("")
1493
+ };
1494
+ var ControlsDef = class extends CanvasSingletonDef3 {
1495
+ constructor() {
1496
+ super({ name: "controls" }, ControlsSchema);
1497
+ }
1498
+ /**
1499
+ * Get the pointer buttons that are mapped to the given tools.
1500
+ * @param ctx - ECS context
1501
+ * @param tools - Tool names to check
1502
+ * @returns Array of PointerButton values that activate any of the given tools
1503
+ */
1504
+ getButtons(ctx, ...tools) {
1505
+ const controls = this.read(ctx);
1506
+ const buttons = [];
1507
+ if (tools.includes(controls.leftMouseTool)) {
1508
+ buttons.push(PointerButton.Left);
1509
+ }
1510
+ if (tools.includes(controls.middleMouseTool)) {
1511
+ buttons.push(PointerButton.Middle);
1512
+ }
1513
+ if (tools.includes(controls.rightMouseTool)) {
1514
+ buttons.push(PointerButton.Right);
1515
+ }
1516
+ return buttons;
1517
+ }
1518
+ /**
1519
+ * Check if a wheel tool is active based on modifier key state.
1520
+ * @param ctx - ECS context
1521
+ * @param tool - Tool name to check
1522
+ * @param modDown - Whether the modifier key is held
1523
+ * @returns True if the given tool is active for wheel input
1524
+ */
1525
+ wheelActive(ctx, tool, modDown) {
1526
+ const controls = this.read(ctx);
1527
+ return controls.wheelTool === tool && !modDown || controls.modWheelTool === tool && modDown;
1528
+ }
1529
+ };
1530
+ var Controls = new ControlsDef();
1531
+
1532
+ // src/singletons/Cursor.ts
1533
+ import { CanvasSingletonDef as CanvasSingletonDef4 } from "@woven-ecs/canvas-store";
1534
+ import { field as field19 } from "@woven-ecs/core";
1535
+ var CursorSchema = {
1536
+ /** Base cursor kind (from current tool) */
1537
+ cursorKind: field19.string().max(64).default("select"),
1538
+ /** Base cursor rotation in radians */
1539
+ rotation: field19.float64().default(0),
1540
+ /** Context-specific cursor kind (overrides cursorKind when set, e.g., during drag/hover) */
1541
+ contextCursorKind: field19.string().max(64).default(""),
1542
+ /** Context cursor rotation in radians */
1543
+ contextRotation: field19.float64().default(0)
1544
+ };
1545
+ var CursorDef2 = class extends CanvasSingletonDef4 {
1546
+ constructor() {
1547
+ super({ name: "cursor" }, CursorSchema);
1548
+ }
1549
+ /**
1550
+ * Get the effective cursor kind and rotation (context if set, otherwise base).
1551
+ */
1552
+ getEffective(ctx) {
1553
+ const cursor = this.read(ctx);
1554
+ if (cursor.contextCursorKind) {
1555
+ return {
1556
+ cursorKind: cursor.contextCursorKind,
1557
+ rotation: cursor.contextRotation
1558
+ };
1559
+ }
1560
+ return { cursorKind: cursor.cursorKind, rotation: cursor.rotation };
1561
+ }
1562
+ /**
1563
+ * Set the base cursor kind and rotation.
1564
+ */
1565
+ setCursor(ctx, cursorKind, rotation = 0) {
1566
+ const cursor = this.write(ctx);
1567
+ cursor.cursorKind = cursorKind;
1568
+ cursor.rotation = rotation;
1569
+ }
1570
+ /**
1571
+ * Set the context cursor kind and rotation (temporary override).
1572
+ */
1573
+ setContextCursor(ctx, cursorKind, rotation = 0) {
1574
+ const cursor = this.write(ctx);
1575
+ cursor.contextCursorKind = cursorKind;
1576
+ cursor.contextRotation = rotation;
1577
+ }
1578
+ /**
1579
+ * Clear the context cursor.
1580
+ */
1581
+ clearContextCursor(ctx) {
1582
+ const cursor = this.write(ctx);
1583
+ cursor.contextCursorKind = "";
1584
+ cursor.contextRotation = 0;
1585
+ }
1586
+ };
1587
+ var Cursor = new CursorDef2();
1588
+
1589
+ // src/singletons/Frame.ts
1590
+ import { defineCanvasSingleton } from "@woven-ecs/canvas-store";
1591
+ import { field as field20 } from "@woven-ecs/core";
1592
+ var Frame = defineCanvasSingleton(
1593
+ { name: "frame" },
1594
+ {
1595
+ /** Current frame number (increments each tick) */
1596
+ number: field20.uint32().default(0),
1597
+ /** Time since last frame in seconds */
1598
+ delta: field20.float64().default(0),
1599
+ /** Timestamp of current frame in milliseconds (from performance.now()) */
1600
+ time: field20.float64().default(0),
1601
+ /** Timestamp of previous frame in milliseconds (0 if first frame) */
1602
+ lastTime: field20.float64().default(0)
1603
+ }
1604
+ );
1605
+
1606
+ // src/singletons/Grid.ts
1607
+ import { CanvasSingletonDef as CanvasSingletonDef5 } from "@woven-ecs/canvas-store";
1608
+ import { field as field21 } from "@woven-ecs/core";
1609
+ var GridSchema = {
1610
+ /** Whether grid snapping is enabled */
1611
+ enabled: field21.boolean().default(false),
1612
+ /** Whether resized/rotated objects must stay aligned to the grid */
1613
+ strict: field21.boolean().default(false),
1614
+ /** Width of each grid column in world units */
1615
+ colWidth: field21.float64().default(20),
1616
+ /** Height of each grid row in world units */
1617
+ rowHeight: field21.float64().default(20),
1618
+ /** Angular snap increment in radians when grid is enabled */
1619
+ snapAngleRad: field21.float64().default(Math.PI / 36),
1620
+ /** Angular snap increment in radians when shift key is held */
1621
+ shiftSnapAngleRad: field21.float64().default(Math.PI / 12)
1622
+ };
1623
+ var GridDef = class extends CanvasSingletonDef5 {
1624
+ constructor() {
1625
+ super({ name: "grid" }, GridSchema);
1626
+ }
1627
+ /**
1628
+ * Snap a position to the grid.
1629
+ * @param ctx - ECS context
1630
+ * @param position - Position to snap [x, y] (mutated in place)
1631
+ */
1632
+ snapPosition(ctx, position) {
1633
+ const grid = this.read(ctx);
1634
+ if (!grid.enabled) return;
1635
+ if (grid.colWidth !== 0) {
1636
+ position[0] = Math.round(position[0] / grid.colWidth) * grid.colWidth;
1637
+ }
1638
+ if (grid.rowHeight !== 0) {
1639
+ position[1] = Math.round(position[1] / grid.rowHeight) * grid.rowHeight;
1640
+ }
1641
+ }
1642
+ /**
1643
+ * Snap a size to the grid (minimum one grid cell).
1644
+ * @param ctx - ECS context
1645
+ * @param size - Size to snap [width, height] (mutated in place)
1646
+ */
1647
+ snapSize(ctx, size) {
1648
+ const grid = this.read(ctx);
1649
+ if (!grid.enabled) return;
1650
+ if (grid.colWidth !== 0) {
1651
+ size[0] = Math.max(grid.colWidth, Math.round(size[0] / grid.colWidth) * grid.colWidth);
1652
+ }
1653
+ if (grid.rowHeight !== 0) {
1654
+ size[1] = Math.max(grid.rowHeight, Math.round(size[1] / grid.rowHeight) * grid.rowHeight);
1655
+ }
1656
+ }
1657
+ /**
1658
+ * Snap a value to the grid column width.
1659
+ * @param ctx - ECS context
1660
+ * @param value - Value to snap
1661
+ * @returns Snapped value, or original if grid disabled
1662
+ */
1663
+ snapX(ctx, value) {
1664
+ const grid = this.read(ctx);
1665
+ if (!grid.enabled || grid.colWidth === 0) return value;
1666
+ return Math.round(value / grid.colWidth) * grid.colWidth;
1667
+ }
1668
+ /**
1669
+ * Snap a value to the grid row height.
1670
+ * @param ctx - ECS context
1671
+ * @param value - Value to snap
1672
+ * @returns Snapped value, or original if grid disabled
1673
+ */
1674
+ snapY(ctx, value) {
1675
+ const grid = this.read(ctx);
1676
+ if (!grid.enabled || grid.rowHeight === 0) return value;
1677
+ return Math.round(value / grid.rowHeight) * grid.rowHeight;
1678
+ }
1679
+ };
1680
+ var Grid = new GridDef();
1681
+
1682
+ // src/singletons/Intersect.ts
1683
+ import { CanvasSingletonDef as CanvasSingletonDef6 } from "@woven-ecs/canvas-store";
1684
+ import { field as field22 } from "@woven-ecs/core";
1685
+ var IntersectSchema = {
1686
+ // Store up to 5 intersected entity IDs
1687
+ entity1: field22.ref(),
1688
+ entity2: field22.ref(),
1689
+ entity3: field22.ref(),
1690
+ entity4: field22.ref(),
1691
+ entity5: field22.ref()
1692
+ };
1693
+ var IntersectDef = class extends CanvasSingletonDef6 {
1694
+ constructor() {
1695
+ super({ name: "intersect" }, IntersectSchema);
1696
+ }
1697
+ /**
1698
+ * Get the topmost intersected entity.
1699
+ */
1700
+ getTop(ctx) {
1701
+ return this.read(ctx).entity1;
1702
+ }
1703
+ /**
1704
+ * Get all intersected entities as an array.
1705
+ */
1706
+ getAll(ctx) {
1707
+ const intersect = this.read(ctx);
1708
+ const result = [];
1709
+ if (intersect.entity1 !== null) result.push(intersect.entity1);
1710
+ if (intersect.entity2 !== null) result.push(intersect.entity2);
1711
+ if (intersect.entity3 !== null) result.push(intersect.entity3);
1712
+ if (intersect.entity4 !== null) result.push(intersect.entity4);
1713
+ if (intersect.entity5 !== null) result.push(intersect.entity5);
1714
+ return result;
1715
+ }
1716
+ /**
1717
+ * Set intersected entities from an array.
1718
+ */
1719
+ setAll(ctx, entities) {
1720
+ const intersect = this.write(ctx);
1721
+ intersect.entity1 = entities[0] ?? null;
1722
+ intersect.entity2 = entities[1] ?? null;
1723
+ intersect.entity3 = entities[2] ?? null;
1724
+ intersect.entity4 = entities[3] ?? null;
1725
+ intersect.entity5 = entities[4] ?? null;
1726
+ }
1727
+ /**
1728
+ * Clear all intersections.
1729
+ */
1730
+ clear(ctx) {
1731
+ const intersect = this.write(ctx);
1732
+ intersect.entity1 = null;
1733
+ intersect.entity2 = null;
1734
+ intersect.entity3 = null;
1735
+ intersect.entity4 = null;
1736
+ intersect.entity5 = null;
1737
+ }
1738
+ };
1739
+ var Intersect = new IntersectDef();
1740
+
1741
+ // src/singletons/Keyboard.ts
1742
+ import { CanvasSingletonDef as CanvasSingletonDef7 } from "@woven-ecs/canvas-store";
1743
+ import { field as field23 } from "@woven-ecs/core";
1744
+ var KEY_BUFFER_SIZE = 32;
1745
+ var KeyboardSchema = {
1746
+ /**
1747
+ * Buffer where each bit represents whether a key is currently pressed.
1748
+ * Uses field.buffer for zero-allocation subarray views.
1749
+ */
1750
+ keysDown: field23.buffer(field23.uint8()).size(KEY_BUFFER_SIZE),
1751
+ /**
1752
+ * Buffer for key-down triggers (true for exactly 1 frame when key is pressed).
1753
+ * Uses field.buffer for zero-allocation subarray views.
1754
+ */
1755
+ keysDownTrigger: field23.buffer(field23.uint8()).size(KEY_BUFFER_SIZE),
1756
+ /**
1757
+ * Buffer for key-up triggers (true for exactly 1 frame when key is released).
1758
+ * Uses field.buffer for zero-allocation subarray views.
1759
+ */
1760
+ keysUpTrigger: field23.buffer(field23.uint8()).size(KEY_BUFFER_SIZE),
1761
+ /** Common modifier - Shift key is down */
1762
+ shiftDown: field23.boolean().default(false),
1763
+ /** Common modifier - Alt/Option key is down */
1764
+ altDown: field23.boolean().default(false),
1765
+ /** Common modifier - Ctrl (Windows/Linux) or Cmd (Mac) is down */
1766
+ modDown: field23.boolean().default(false)
1767
+ };
1768
+ function getBit(buffer, bitIndex) {
1769
+ if (bitIndex < 0 || bitIndex >= buffer.length * 8) return false;
1770
+ const byteIndex = Math.floor(bitIndex / 8);
1771
+ const bitOffset = bitIndex % 8;
1772
+ return (buffer[byteIndex] & 1 << bitOffset) !== 0;
1773
+ }
1774
+ var KeyboardDef = class extends CanvasSingletonDef7 {
1775
+ constructor() {
1776
+ super({ name: "keyboard" }, KeyboardSchema);
1777
+ }
1778
+ /**
1779
+ * Check if a key is currently pressed.
1780
+ * @param ctx - Editor context
1781
+ * @param key - The key index to check (use Key.A, Key.Space, etc.)
1782
+ */
1783
+ isKeyDown(ctx, key) {
1784
+ return getBit(this.read(ctx).keysDown, key);
1785
+ }
1786
+ /**
1787
+ * Check if a key was just pressed this frame.
1788
+ * @param ctx - Editor context
1789
+ * @param key - The key index to check (use Key.A, Key.Space, etc.)
1790
+ */
1791
+ isKeyDownTrigger(ctx, key) {
1792
+ return getBit(this.read(ctx).keysDownTrigger, key);
1793
+ }
1794
+ /**
1795
+ * Check if a key was just released this frame.
1796
+ * @param ctx - Editor context
1797
+ * @param key - The key index to check (use Key.A, Key.Space, etc.)
1798
+ */
1799
+ isKeyUpTrigger(ctx, key) {
1800
+ return getBit(this.read(ctx).keysUpTrigger, key);
1801
+ }
1802
+ };
1803
+ var Keyboard = new KeyboardDef();
1804
+ function setBit(buffer, bitIndex, value) {
1805
+ if (bitIndex < 0 || bitIndex >= buffer.length * 8) return;
1806
+ const byteIndex = Math.floor(bitIndex / 8);
1807
+ const bitOffset = bitIndex % 8;
1808
+ if (value) {
1809
+ buffer[byteIndex] |= 1 << bitOffset;
1810
+ } else {
1811
+ buffer[byteIndex] &= ~(1 << bitOffset);
1812
+ }
1813
+ }
1814
+ function clearBits(buffer) {
1815
+ for (let i = 0; i < buffer.length; i++) {
1816
+ buffer[i] = 0;
1817
+ }
1818
+ }
1819
+ var codeToIndex = {
1820
+ // Letters (0-25)
1821
+ KeyA: 0,
1822
+ KeyB: 1,
1823
+ KeyC: 2,
1824
+ KeyD: 3,
1825
+ KeyE: 4,
1826
+ KeyF: 5,
1827
+ KeyG: 6,
1828
+ KeyH: 7,
1829
+ KeyI: 8,
1830
+ KeyJ: 9,
1831
+ KeyK: 10,
1832
+ KeyL: 11,
1833
+ KeyM: 12,
1834
+ KeyN: 13,
1835
+ KeyO: 14,
1836
+ KeyP: 15,
1837
+ KeyQ: 16,
1838
+ KeyR: 17,
1839
+ KeyS: 18,
1840
+ KeyT: 19,
1841
+ KeyU: 20,
1842
+ KeyV: 21,
1843
+ KeyW: 22,
1844
+ KeyX: 23,
1845
+ KeyY: 24,
1846
+ KeyZ: 25,
1847
+ // Numbers (26-35)
1848
+ Digit0: 26,
1849
+ Digit1: 27,
1850
+ Digit2: 28,
1851
+ Digit3: 29,
1852
+ Digit4: 30,
1853
+ Digit5: 31,
1854
+ Digit6: 32,
1855
+ Digit7: 33,
1856
+ Digit8: 34,
1857
+ Digit9: 35,
1858
+ // Function keys (36-47)
1859
+ F1: 36,
1860
+ F2: 37,
1861
+ F3: 38,
1862
+ F4: 39,
1863
+ F5: 40,
1864
+ F6: 41,
1865
+ F7: 42,
1866
+ F8: 43,
1867
+ F9: 44,
1868
+ F10: 45,
1869
+ F11: 46,
1870
+ F12: 47,
1871
+ // Modifiers (48-51)
1872
+ ShiftLeft: 48,
1873
+ ShiftRight: 49,
1874
+ ControlLeft: 50,
1875
+ ControlRight: 51,
1876
+ AltLeft: 52,
1877
+ AltRight: 53,
1878
+ MetaLeft: 54,
1879
+ MetaRight: 55,
1880
+ // Navigation (56-71)
1881
+ Escape: 56,
1882
+ Space: 57,
1883
+ Enter: 58,
1884
+ Tab: 59,
1885
+ Backspace: 60,
1886
+ Delete: 61,
1887
+ ArrowLeft: 62,
1888
+ ArrowUp: 63,
1889
+ ArrowRight: 64,
1890
+ ArrowDown: 65,
1891
+ Home: 66,
1892
+ End: 67,
1893
+ PageUp: 68,
1894
+ PageDown: 69,
1895
+ Insert: 70,
1896
+ // Punctuation (72-83)
1897
+ Semicolon: 72,
1898
+ Equal: 73,
1899
+ Comma: 74,
1900
+ Minus: 75,
1901
+ Period: 76,
1902
+ Slash: 77,
1903
+ Backquote: 78,
1904
+ BracketLeft: 79,
1905
+ Backslash: 80,
1906
+ BracketRight: 81,
1907
+ Quote: 82,
1908
+ // Numpad (84-99)
1909
+ Numpad0: 84,
1910
+ Numpad1: 85,
1911
+ Numpad2: 86,
1912
+ Numpad3: 87,
1913
+ Numpad4: 88,
1914
+ Numpad5: 89,
1915
+ Numpad6: 90,
1916
+ Numpad7: 91,
1917
+ Numpad8: 92,
1918
+ Numpad9: 93,
1919
+ NumpadAdd: 94,
1920
+ NumpadSubtract: 95,
1921
+ NumpadMultiply: 96,
1922
+ NumpadDivide: 97,
1923
+ NumpadDecimal: 98,
1924
+ NumpadEnter: 99
1925
+ };
1926
+ var Key = {
1927
+ // Letters
1928
+ A: codeToIndex.KeyA,
1929
+ B: codeToIndex.KeyB,
1930
+ C: codeToIndex.KeyC,
1931
+ D: codeToIndex.KeyD,
1932
+ E: codeToIndex.KeyE,
1933
+ F: codeToIndex.KeyF,
1934
+ G: codeToIndex.KeyG,
1935
+ H: codeToIndex.KeyH,
1936
+ I: codeToIndex.KeyI,
1937
+ J: codeToIndex.KeyJ,
1938
+ K: codeToIndex.KeyK,
1939
+ L: codeToIndex.KeyL,
1940
+ M: codeToIndex.KeyM,
1941
+ N: codeToIndex.KeyN,
1942
+ O: codeToIndex.KeyO,
1943
+ P: codeToIndex.KeyP,
1944
+ Q: codeToIndex.KeyQ,
1945
+ R: codeToIndex.KeyR,
1946
+ S: codeToIndex.KeyS,
1947
+ T: codeToIndex.KeyT,
1948
+ U: codeToIndex.KeyU,
1949
+ V: codeToIndex.KeyV,
1950
+ W: codeToIndex.KeyW,
1951
+ X: codeToIndex.KeyX,
1952
+ Y: codeToIndex.KeyY,
1953
+ Z: codeToIndex.KeyZ,
1954
+ // Numbers
1955
+ Digit0: codeToIndex.Digit0,
1956
+ Digit1: codeToIndex.Digit1,
1957
+ Digit2: codeToIndex.Digit2,
1958
+ Digit3: codeToIndex.Digit3,
1959
+ Digit4: codeToIndex.Digit4,
1960
+ Digit5: codeToIndex.Digit5,
1961
+ Digit6: codeToIndex.Digit6,
1962
+ Digit7: codeToIndex.Digit7,
1963
+ Digit8: codeToIndex.Digit8,
1964
+ Digit9: codeToIndex.Digit9,
1965
+ // Function keys
1966
+ F1: codeToIndex.F1,
1967
+ F2: codeToIndex.F2,
1968
+ F3: codeToIndex.F3,
1969
+ F4: codeToIndex.F4,
1970
+ F5: codeToIndex.F5,
1971
+ F6: codeToIndex.F6,
1972
+ F7: codeToIndex.F7,
1973
+ F8: codeToIndex.F8,
1974
+ F9: codeToIndex.F9,
1975
+ F10: codeToIndex.F10,
1976
+ F11: codeToIndex.F11,
1977
+ F12: codeToIndex.F12,
1978
+ // Modifiers
1979
+ ShiftLeft: codeToIndex.ShiftLeft,
1980
+ ShiftRight: codeToIndex.ShiftRight,
1981
+ ControlLeft: codeToIndex.ControlLeft,
1982
+ ControlRight: codeToIndex.ControlRight,
1983
+ AltLeft: codeToIndex.AltLeft,
1984
+ AltRight: codeToIndex.AltRight,
1985
+ MetaLeft: codeToIndex.MetaLeft,
1986
+ MetaRight: codeToIndex.MetaRight,
1987
+ // Navigation
1988
+ Escape: codeToIndex.Escape,
1989
+ Space: codeToIndex.Space,
1990
+ Enter: codeToIndex.Enter,
1991
+ Tab: codeToIndex.Tab,
1992
+ Backspace: codeToIndex.Backspace,
1993
+ Delete: codeToIndex.Delete,
1994
+ ArrowLeft: codeToIndex.ArrowLeft,
1995
+ ArrowUp: codeToIndex.ArrowUp,
1996
+ ArrowRight: codeToIndex.ArrowRight,
1997
+ ArrowDown: codeToIndex.ArrowDown,
1998
+ Home: codeToIndex.Home,
1999
+ End: codeToIndex.End,
2000
+ PageUp: codeToIndex.PageUp,
2001
+ PageDown: codeToIndex.PageDown,
2002
+ Insert: codeToIndex.Insert,
2003
+ // Punctuation
2004
+ Semicolon: codeToIndex.Semicolon,
2005
+ Equal: codeToIndex.Equal,
2006
+ Comma: codeToIndex.Comma,
2007
+ Minus: codeToIndex.Minus,
2008
+ Period: codeToIndex.Period,
2009
+ Slash: codeToIndex.Slash,
2010
+ Backquote: codeToIndex.Backquote,
2011
+ BracketLeft: codeToIndex.BracketLeft,
2012
+ Backslash: codeToIndex.Backslash,
2013
+ BracketRight: codeToIndex.BracketRight,
2014
+ Quote: codeToIndex.Quote,
2015
+ // Numpad
2016
+ Numpad0: codeToIndex.Numpad0,
2017
+ Numpad1: codeToIndex.Numpad1,
2018
+ Numpad2: codeToIndex.Numpad2,
2019
+ Numpad3: codeToIndex.Numpad3,
2020
+ Numpad4: codeToIndex.Numpad4,
2021
+ Numpad5: codeToIndex.Numpad5,
2022
+ Numpad6: codeToIndex.Numpad6,
2023
+ Numpad7: codeToIndex.Numpad7,
2024
+ Numpad8: codeToIndex.Numpad8,
2025
+ Numpad9: codeToIndex.Numpad9,
2026
+ NumpadAdd: codeToIndex.NumpadAdd,
2027
+ NumpadSubtract: codeToIndex.NumpadSubtract,
2028
+ NumpadMultiply: codeToIndex.NumpadMultiply,
2029
+ NumpadDivide: codeToIndex.NumpadDivide,
2030
+ NumpadDecimal: codeToIndex.NumpadDecimal,
2031
+ NumpadEnter: codeToIndex.NumpadEnter
2032
+ };
2033
+
2034
+ // src/singletons/Mouse.ts
2035
+ import { CanvasSingletonDef as CanvasSingletonDef8 } from "@woven-ecs/canvas-store";
2036
+ import { field as field24 } from "@woven-ecs/core";
2037
+ var MouseSchema = {
2038
+ /** Current mouse position relative to the editor element [x, y] */
2039
+ position: field24.tuple(field24.float32(), 2).default([0, 0]),
2040
+ /** Horizontal wheel delta (positive = scroll right) */
2041
+ wheelDeltaX: field24.float32().default(0),
2042
+ /** Vertical wheel delta (positive = scroll down), normalized across browsers */
2043
+ wheelDeltaY: field24.float32().default(0),
2044
+ /** True for 1 frame when mouse moves */
2045
+ moveTrigger: field24.boolean().default(false),
2046
+ /** True for 1 frame when wheel is scrolled */
2047
+ wheelTrigger: field24.boolean().default(false),
2048
+ /** True for 1 frame when mouse enters the editor element */
2049
+ enterTrigger: field24.boolean().default(false),
2050
+ /** True for 1 frame when mouse leaves the editor element */
2051
+ leaveTrigger: field24.boolean().default(false)
2052
+ };
2053
+ var MouseDef = class extends CanvasSingletonDef8 {
2054
+ constructor() {
2055
+ super({ name: "mouse" }, MouseSchema);
2056
+ }
2057
+ /** Check if mouse moved this frame */
2058
+ didMove(ctx) {
2059
+ return this.read(ctx).moveTrigger;
2060
+ }
2061
+ /** Check if wheel was scrolled this frame */
2062
+ didScroll(ctx) {
2063
+ return this.read(ctx).wheelTrigger;
2064
+ }
2065
+ /** Check if mouse entered the editor element this frame */
2066
+ didEnter(ctx) {
2067
+ return this.read(ctx).enterTrigger;
2068
+ }
2069
+ /** Check if mouse left the editor element this frame */
2070
+ didLeave(ctx) {
2071
+ return this.read(ctx).leaveTrigger;
2072
+ }
2073
+ /** Get current mouse position as [x, y] */
2074
+ getPosition(ctx) {
2075
+ const m = this.read(ctx);
2076
+ return [m.position[0], m.position[1]];
2077
+ }
2078
+ /** Get wheel delta as [dx, dy] */
2079
+ getWheelDelta(ctx) {
2080
+ const m = this.read(ctx);
2081
+ return [m.wheelDeltaX, m.wheelDeltaY];
2082
+ }
2083
+ };
2084
+ var Mouse = new MouseDef();
2085
+
2086
+ // src/singletons/RankBounds.ts
2087
+ import { CanvasSingletonDef as CanvasSingletonDef9 } from "@woven-ecs/canvas-store";
2088
+ import { field as field25 } from "@woven-ecs/core";
2089
+ import { generateJitteredKeyBetween } from "fractional-indexing-jittered";
2090
+ var RankBoundsSchema = {
2091
+ minRank: field25.string().max(36).default(""),
2092
+ maxRank: field25.string().max(36).default("")
2093
+ };
2094
+ var RankBoundsDef = class extends CanvasSingletonDef9 {
2095
+ constructor() {
2096
+ super({ name: "rankBounds" }, RankBoundsSchema);
2097
+ }
2098
+ /**
2099
+ * Generate the next rank (higher z-order than all existing).
2100
+ * @returns A new rank string that sorts after maxRank
2101
+ */
2102
+ genNext(ctx) {
2103
+ const bounds = this.write(ctx);
2104
+ const next = generateJitteredKeyBetween(bounds.maxRank || null, null);
2105
+ bounds.maxRank = next;
2106
+ if (!bounds.minRank) {
2107
+ bounds.minRank = next;
2108
+ }
2109
+ return next;
2110
+ }
2111
+ /**
2112
+ * Generate the previous rank (lower z-order than all existing).
2113
+ * @returns A new rank string that sorts before minRank
2114
+ */
2115
+ genPrev(ctx) {
2116
+ const bounds = this.write(ctx);
2117
+ const prev = generateJitteredKeyBetween(null, bounds.minRank || null);
2118
+ bounds.minRank = prev;
2119
+ if (!bounds.maxRank) {
2120
+ bounds.maxRank = prev;
2121
+ }
2122
+ return prev;
2123
+ }
2124
+ /**
2125
+ * Generate a rank between two existing ranks.
2126
+ * @param before - Rank to come after (or null for start)
2127
+ * @param after - Rank to come before (or null for end)
2128
+ * @returns A new rank string that sorts between the two
2129
+ */
2130
+ genBetween(before, after) {
2131
+ return generateJitteredKeyBetween(before, after);
2132
+ }
2133
+ /**
2134
+ * Add a rank to tracking (expands bounds if needed).
2135
+ */
2136
+ add(ctx, rank) {
2137
+ if (!rank) return;
2138
+ const bounds = this.write(ctx);
2139
+ if (!bounds.minRank || rank < bounds.minRank) {
2140
+ bounds.minRank = rank;
2141
+ }
2142
+ if (!bounds.maxRank || rank > bounds.maxRank) {
2143
+ bounds.maxRank = rank;
2144
+ }
2145
+ }
2146
+ };
2147
+ var RankBounds = new RankBoundsDef();
2148
+
2149
+ // src/singletons/ScaleWithZoomState.ts
2150
+ import { defineCanvasSingleton as defineCanvasSingleton2 } from "@woven-ecs/canvas-store";
2151
+ import { field as field26 } from "@woven-ecs/core";
2152
+ var ScaleWithZoomState = defineCanvasSingleton2(
2153
+ { name: "scaleWithZoomState" },
2154
+ {
2155
+ /** Last processed zoom level */
2156
+ lastZoom: field26.float64().default(1)
2157
+ }
2158
+ );
2159
+
2160
+ // src/systems/capture/keybindSystem.ts
2161
+ import { addComponent as addComponent2, createEntity as createEntity2, getResources as getResources2 } from "@woven-ecs/core";
2162
+
2163
+ // src/command.ts
2164
+ import {
2165
+ addComponent,
2166
+ createEntity,
2167
+ defineComponent,
2168
+ defineQuery,
2169
+ field as field27,
2170
+ getResources,
2171
+ removeEntity
2172
+ } from "@woven-ecs/core";
2173
+ var CommandMarker = defineComponent({
2174
+ name: field27.string().max(128)
2175
+ });
2176
+ var editorPayloads = /* @__PURE__ */ new WeakMap();
2177
+ function getPayloadMap(ctx) {
2178
+ const { editor } = getResources(ctx);
2179
+ let map = editorPayloads.get(editor);
2180
+ if (!map) {
2181
+ map = /* @__PURE__ */ new Map();
2182
+ editorPayloads.set(editor, map);
2183
+ }
2184
+ return map;
2185
+ }
2186
+ var commands = defineQuery((q) => q.with(CommandMarker));
2187
+ function defineCommand(name) {
2188
+ return {
2189
+ name,
2190
+ spawn(ctx, payload) {
2191
+ const eid = createEntity(ctx);
2192
+ addComponent(ctx, eid, CommandMarker, { name });
2193
+ getPayloadMap(ctx).set(eid, payload);
2194
+ return eid;
2195
+ },
2196
+ *iter(ctx) {
2197
+ const payloads = getPayloadMap(ctx);
2198
+ for (const eid of commands.current(ctx)) {
2199
+ const marker = CommandMarker.read(ctx, eid);
2200
+ if (marker.name === name) {
2201
+ const payload = payloads.get(eid);
2202
+ yield { eid, payload };
2203
+ }
2204
+ }
2205
+ },
2206
+ didSpawnLastFrame(ctx) {
2207
+ for (const eid of commands.removed(ctx)) {
2208
+ const marker = CommandMarker.read(ctx, eid);
2209
+ if (marker.name === name) {
2210
+ return true;
2211
+ }
2212
+ }
2213
+ return false;
2214
+ }
2215
+ };
2216
+ }
2217
+ function cleanupCommands(ctx) {
2218
+ const payloads = getPayloadMap(ctx);
2219
+ for (const eid of commands.current(ctx)) {
2220
+ payloads.delete(eid);
2221
+ removeEntity(ctx, eid);
2222
+ }
2223
+ }
2224
+ function on(ctx, def, handler) {
2225
+ for (const { payload } of def.iter(ctx)) {
2226
+ handler(ctx, payload);
2227
+ }
2228
+ }
2229
+ var Undo = defineCommand("undo");
2230
+ var Redo = defineCommand("redo");
2231
+
2232
+ // src/EditorSystem.ts
2233
+ import { MainThreadSystem } from "@woven-ecs/core";
2234
+ var DEFAULT_PRIORITY = 0;
2235
+ function defineEditorSystem(options, execute) {
2236
+ return {
2237
+ _system: new MainThreadSystem(execute),
2238
+ phase: options.phase,
2239
+ priority: options.priority ?? DEFAULT_PRIORITY
2240
+ };
2241
+ }
2242
+
2243
+ // src/systems/capture/keybindSystem.ts
2244
+ var keybindSystem = defineEditorSystem({ phase: "capture" }, (ctx) => {
2245
+ const keyboard = Keyboard.read(ctx);
2246
+ const { editor } = getResources2(ctx);
2247
+ for (const keybind of editor.keybinds) {
2248
+ let triggered = Keyboard.isKeyDownTrigger(ctx, keybind.key);
2249
+ triggered &&= !!keybind.mod === keyboard.modDown;
2250
+ triggered &&= !!keybind.shift === keyboard.shiftDown;
2251
+ if (triggered) {
2252
+ const eid = createEntity2(ctx);
2253
+ addComponent2(ctx, eid, CommandMarker, { name: keybind.command });
2254
+ break;
2255
+ }
2256
+ }
2257
+ });
2258
+
2259
+ // src/systems/input/frameSystem.ts
2260
+ var frameSystem = defineEditorSystem({ phase: "input", priority: 100 }, (ctx) => {
2261
+ const now = performance.now();
2262
+ const buffer = Frame._getInstance(ctx).buffer;
2263
+ const last = buffer.lastTime[0];
2264
+ buffer.number[0]++;
2265
+ buffer.lastTime[0] = now;
2266
+ buffer.time[0] = now;
2267
+ if (last === 0) {
2268
+ buffer.delta[0] = 0.016;
2269
+ } else {
2270
+ buffer.delta[0] = Math.min((now - last) / 1e3, 0.1);
2271
+ }
2272
+ });
2273
+
2274
+ // src/systems/input/keyboardSystem.ts
2275
+ import { getResources as getResources3 } from "@woven-ecs/core";
2276
+ var instanceState = /* @__PURE__ */ new WeakMap();
2277
+ function attachKeyboardListeners(domElement) {
2278
+ if (instanceState.has(domElement)) return;
2279
+ if (!domElement.hasAttribute("tabindex")) {
2280
+ domElement.setAttribute("tabindex", "0");
2281
+ }
2282
+ const state = {
2283
+ eventsBuffer: [],
2284
+ onKeyDown: (e) => {
2285
+ if (e.key === "Tab" || e.key === "Alt" || e.key === " ") {
2286
+ e.preventDefault();
2287
+ }
2288
+ state.eventsBuffer.push(e);
2289
+ },
2290
+ onKeyUp: (e) => {
2291
+ state.eventsBuffer.push(e);
2292
+ },
2293
+ onBlur: () => {
2294
+ state.eventsBuffer.push({ type: "blur" });
2295
+ },
2296
+ // Reusable buffers - allocated once per instance
2297
+ keysDown: new Uint8Array(32),
2298
+ keysDownTrigger: new Uint8Array(32),
2299
+ keysUpTrigger: new Uint8Array(32)
2300
+ };
2301
+ instanceState.set(domElement, state);
2302
+ domElement.addEventListener("keydown", state.onKeyDown);
2303
+ domElement.addEventListener("keyup", state.onKeyUp);
2304
+ domElement.addEventListener("blur", state.onBlur);
2305
+ }
2306
+ function detachKeyboardListeners(domElement) {
2307
+ const state = instanceState.get(domElement);
2308
+ if (!state) return;
2309
+ domElement.removeEventListener("keydown", state.onKeyDown);
2310
+ domElement.removeEventListener("keyup", state.onKeyUp);
2311
+ domElement.removeEventListener("blur", state.onBlur);
2312
+ instanceState.delete(domElement);
2313
+ }
2314
+ var keyboardSystem = defineEditorSystem({ phase: "input" }, (ctx) => {
2315
+ const resources = getResources3(ctx);
2316
+ const state = instanceState.get(resources.domElement);
2317
+ if (!state) return;
2318
+ const hasEvents = state.eventsBuffer.length > 0;
2319
+ const triggersNeedClearing = !isZeroed(state.keysDownTrigger) || !isZeroed(state.keysUpTrigger);
2320
+ if (!hasEvents && !triggersNeedClearing) return;
2321
+ const keyboard = Keyboard.write(ctx);
2322
+ const keysDown = state.keysDown;
2323
+ const keysDownTrigger = state.keysDownTrigger;
2324
+ const keysUpTrigger = state.keysUpTrigger;
2325
+ keysDownTrigger.fill(0);
2326
+ keysUpTrigger.fill(0);
2327
+ keysDown.set(keyboard.keysDown);
2328
+ for (const event of state.eventsBuffer) {
2329
+ if (event.type === "blur") {
2330
+ keysDown.fill(0);
2331
+ keyboard.shiftDown = false;
2332
+ keyboard.altDown = false;
2333
+ keyboard.modDown = false;
2334
+ continue;
2335
+ }
2336
+ const keyIndex = codeToIndex[event.code];
2337
+ if (keyIndex === void 0) continue;
2338
+ if (event.type === "keydown") {
2339
+ const wasDown = getBit2(keysDown, keyIndex);
2340
+ if (!wasDown) {
2341
+ setBit(keysDownTrigger, keyIndex, true);
2342
+ }
2343
+ setBit(keysDown, keyIndex, true);
2344
+ } else if (event.type === "keyup") {
2345
+ setBit(keysDown, keyIndex, false);
2346
+ setBit(keysUpTrigger, keyIndex, true);
2347
+ }
2348
+ keyboard.shiftDown = event.shiftKey;
2349
+ keyboard.altDown = event.altKey;
2350
+ keyboard.modDown = event.ctrlKey || event.metaKey;
2351
+ }
2352
+ keyboard.keysDown = keysDown;
2353
+ keyboard.keysDownTrigger = keysDownTrigger;
2354
+ keyboard.keysUpTrigger = keysUpTrigger;
2355
+ state.eventsBuffer.length = 0;
2356
+ });
2357
+ function getBit2(buffer, bitIndex) {
2358
+ if (bitIndex < 0 || bitIndex >= buffer.length * 8) return false;
2359
+ const byteIndex = Math.floor(bitIndex / 8);
2360
+ const bitOffset = bitIndex % 8;
2361
+ return (buffer[byteIndex] & 1 << bitOffset) !== 0;
2362
+ }
2363
+ function isZeroed(buffer) {
2364
+ for (let i = 0; i < buffer.length; i++) {
2365
+ if (buffer[i] !== 0) return false;
2366
+ }
2367
+ return true;
2368
+ }
2369
+
2370
+ // src/systems/input/mouseSystem.ts
2371
+ import { getResources as getResources4 } from "@woven-ecs/core";
2372
+ var instanceState2 = /* @__PURE__ */ new WeakMap();
2373
+ function attachMouseListeners(domElement) {
2374
+ if (instanceState2.has(domElement)) return;
2375
+ const state = {
2376
+ eventsBuffer: [],
2377
+ onMouseMove: (e) => {
2378
+ state.eventsBuffer.push({
2379
+ type: "mousemove",
2380
+ clientX: e.clientX,
2381
+ clientY: e.clientY
2382
+ });
2383
+ },
2384
+ onWheel: (e) => {
2385
+ e.preventDefault();
2386
+ state.eventsBuffer.push({
2387
+ type: "wheel",
2388
+ clientX: e.clientX,
2389
+ clientY: e.clientY,
2390
+ deltaX: e.deltaX,
2391
+ deltaY: e.deltaY,
2392
+ deltaMode: e.deltaMode
2393
+ });
2394
+ },
2395
+ onMouseEnter: () => {
2396
+ state.eventsBuffer.push({ type: "mouseenter" });
2397
+ },
2398
+ onMouseLeave: () => {
2399
+ state.eventsBuffer.push({ type: "mouseleave" });
2400
+ }
2401
+ };
2402
+ instanceState2.set(domElement, state);
2403
+ window.addEventListener("mousemove", state.onMouseMove);
2404
+ domElement.addEventListener("wheel", state.onWheel, { passive: false });
2405
+ domElement.addEventListener("mouseenter", state.onMouseEnter);
2406
+ domElement.addEventListener("mouseleave", state.onMouseLeave);
2407
+ }
2408
+ function detachMouseListeners(domElement) {
2409
+ const state = instanceState2.get(domElement);
2410
+ if (!state) return;
2411
+ window.removeEventListener("mousemove", state.onMouseMove);
2412
+ domElement.removeEventListener("wheel", state.onWheel);
2413
+ domElement.removeEventListener("mouseenter", state.onMouseEnter);
2414
+ domElement.removeEventListener("mouseleave", state.onMouseLeave);
2415
+ instanceState2.delete(domElement);
2416
+ }
2417
+ var mouseSystem = defineEditorSystem({ phase: "input" }, (ctx) => {
2418
+ const resources = getResources4(ctx);
2419
+ const state = instanceState2.get(resources.domElement);
2420
+ if (!state) return;
2421
+ const currentMouse = Mouse.read(ctx);
2422
+ const hadTriggers = currentMouse.moveTrigger || currentMouse.wheelTrigger || currentMouse.enterTrigger || currentMouse.leaveTrigger;
2423
+ const hasEvents = state.eventsBuffer.length > 0;
2424
+ if (!hadTriggers && !hasEvents) return;
2425
+ const mouse = Mouse.write(ctx);
2426
+ const screen = Screen.read(ctx);
2427
+ mouse.moveTrigger = false;
2428
+ mouse.wheelTrigger = false;
2429
+ mouse.enterTrigger = false;
2430
+ mouse.leaveTrigger = false;
2431
+ mouse.wheelDeltaX = 0;
2432
+ mouse.wheelDeltaY = 0;
2433
+ for (const event of state.eventsBuffer) {
2434
+ switch (event.type) {
2435
+ case "mousemove":
2436
+ mouse.position = [event.clientX - screen.left, event.clientY - screen.top];
2437
+ mouse.moveTrigger = true;
2438
+ break;
2439
+ case "wheel":
2440
+ mouse.wheelDeltaX = event.deltaX;
2441
+ mouse.wheelDeltaY = normalizeWheelDelta(event.deltaY, event.deltaMode);
2442
+ mouse.wheelTrigger = true;
2443
+ break;
2444
+ case "mouseenter":
2445
+ mouse.enterTrigger = true;
2446
+ break;
2447
+ case "mouseleave":
2448
+ mouse.leaveTrigger = true;
2449
+ break;
2450
+ }
2451
+ }
2452
+ state.eventsBuffer.length = 0;
2453
+ });
2454
+ function normalizeWheelDelta(deltaY, deltaMode) {
2455
+ const LINE_HEIGHT = 16;
2456
+ const PAGE_HEIGHT = typeof window !== "undefined" ? window.innerHeight : 800;
2457
+ let normalized = deltaY;
2458
+ if (deltaMode === 1) {
2459
+ normalized *= LINE_HEIGHT;
2460
+ } else if (deltaMode === 2) {
2461
+ normalized *= PAGE_HEIGHT;
2462
+ }
2463
+ if (typeof navigator !== "undefined" && navigator.userAgent.includes("Firefox")) {
2464
+ normalized *= 4;
2465
+ }
2466
+ if (typeof navigator !== "undefined" && (navigator.userAgent.includes("Mac") || navigator.userAgent.includes("Macintosh"))) {
2467
+ normalized *= 1.5;
2468
+ }
2469
+ return Math.min(Math.max(normalized, -100), 100);
2470
+ }
2471
+
2472
+ // src/systems/input/pointerSystem.ts
2473
+ import { addComponent as addComponent3, createEntity as createEntity3, getResources as getResources5, removeEntity as removeEntity2 } from "@woven-ecs/core";
2474
+ var instanceState3 = /* @__PURE__ */ new WeakMap();
2475
+ function attachPointerListeners(domElement) {
2476
+ if (instanceState3.has(domElement)) return;
2477
+ const state = {
2478
+ eventsBuffer: [],
2479
+ frameCount: 0,
2480
+ pointerEntityMap: /* @__PURE__ */ new Map(),
2481
+ onPointerDown: (e) => {
2482
+ state.eventsBuffer.push({
2483
+ type: "pointerdown",
2484
+ pointerId: e.pointerId,
2485
+ clientX: e.clientX,
2486
+ clientY: e.clientY,
2487
+ button: e.button,
2488
+ pointerType: e.pointerType,
2489
+ pressure: e.pressure,
2490
+ target: e.target
2491
+ });
2492
+ },
2493
+ onContextMenu: (e) => {
2494
+ e.preventDefault();
2495
+ const hasPointerDown = state.eventsBuffer.some((evt) => evt.type === "pointerdown");
2496
+ if (hasPointerDown) return;
2497
+ state.eventsBuffer.push({
2498
+ type: "pointerdown",
2499
+ pointerId: 0,
2500
+ clientX: e.clientX,
2501
+ clientY: e.clientY,
2502
+ button: 2,
2503
+ // Right button
2504
+ pointerType: "mouse",
2505
+ pressure: 0.5,
2506
+ target: e.target
2507
+ });
2508
+ },
2509
+ onPointerMove: (e) => {
2510
+ state.eventsBuffer.push({
2511
+ type: "pointermove",
2512
+ pointerId: e.pointerId,
2513
+ clientX: e.clientX,
2514
+ clientY: e.clientY,
2515
+ button: e.button,
2516
+ pointerType: e.pointerType,
2517
+ pressure: e.pressure,
2518
+ target: e.target
2519
+ });
2520
+ },
2521
+ onPointerUp: (e) => {
2522
+ state.eventsBuffer.push({
2523
+ type: "pointerup",
2524
+ pointerId: e.pointerId,
2525
+ clientX: e.clientX,
2526
+ clientY: e.clientY,
2527
+ button: e.button,
2528
+ pointerType: e.pointerType,
2529
+ pressure: e.pressure,
2530
+ target: e.target
2531
+ });
2532
+ },
2533
+ onPointerCancel: (e) => {
2534
+ state.eventsBuffer.push({
2535
+ type: "pointercancel",
2536
+ pointerId: e.pointerId,
2537
+ clientX: e.clientX,
2538
+ clientY: e.clientY,
2539
+ button: e.button,
2540
+ pointerType: e.pointerType,
2541
+ pressure: e.pressure,
2542
+ target: e.target
2543
+ });
2544
+ }
2545
+ };
2546
+ instanceState3.set(domElement, state);
2547
+ domElement.addEventListener("pointerdown", state.onPointerDown);
2548
+ domElement.addEventListener("contextmenu", state.onContextMenu);
2549
+ window.addEventListener("pointermove", state.onPointerMove);
2550
+ window.addEventListener("pointerup", state.onPointerUp);
2551
+ window.addEventListener("pointercancel", state.onPointerCancel);
2552
+ }
2553
+ function detachPointerListeners(domElement) {
2554
+ const state = instanceState3.get(domElement);
2555
+ if (!state) return;
2556
+ domElement.removeEventListener("pointerdown", state.onPointerDown);
2557
+ domElement.removeEventListener("contextmenu", state.onContextMenu);
2558
+ window.removeEventListener("pointermove", state.onPointerMove);
2559
+ window.removeEventListener("pointerup", state.onPointerUp);
2560
+ window.removeEventListener("pointercancel", state.onPointerCancel);
2561
+ instanceState3.delete(domElement);
2562
+ }
2563
+ var pointerSystem = defineEditorSystem({ phase: "input" }, (ctx) => {
2564
+ const resources = getResources5(ctx);
2565
+ const { domElement } = resources;
2566
+ const state = instanceState3.get(domElement);
2567
+ if (!state) return;
2568
+ state.frameCount++;
2569
+ const screen = Screen.read(ctx);
2570
+ const time = state.frameCount / 60;
2571
+ for (const event of state.eventsBuffer) {
2572
+ switch (event.type) {
2573
+ case "pointerdown": {
2574
+ const position = [event.clientX - screen.left, event.clientY - screen.top];
2575
+ const entityId = createEntity3(ctx);
2576
+ addComponent3(ctx, entityId, Pointer, {
2577
+ pointerId: event.pointerId,
2578
+ position,
2579
+ downPosition: position,
2580
+ downFrame: state.frameCount,
2581
+ button: Pointer.getButton(event.button),
2582
+ pointerType: Pointer.getType(event.pointerType),
2583
+ pressure: event.pressure,
2584
+ obscured: event.target !== domElement
2585
+ });
2586
+ const pointer = Pointer.write(ctx, entityId);
2587
+ addPointerSample(pointer, position, time);
2588
+ state.pointerEntityMap.set(event.pointerId, entityId);
2589
+ break;
2590
+ }
2591
+ case "pointermove": {
2592
+ const entityId = state.pointerEntityMap.get(event.pointerId);
2593
+ if (entityId === void 0) break;
2594
+ const position = [event.clientX - screen.left, event.clientY - screen.top];
2595
+ const pointer = Pointer.write(ctx, entityId);
2596
+ addPointerSample(pointer, position, time);
2597
+ pointer.pressure = event.pressure;
2598
+ pointer.obscured = event.target !== domElement;
2599
+ break;
2600
+ }
2601
+ case "pointerup":
2602
+ case "pointercancel": {
2603
+ const entityId = state.pointerEntityMap.get(event.pointerId);
2604
+ if (entityId === void 0) break;
2605
+ const position = [event.clientX - screen.left, event.clientY - screen.top];
2606
+ const pointer = Pointer.write(ctx, entityId);
2607
+ addPointerSample(pointer, position, time);
2608
+ removeEntity2(ctx, entityId);
2609
+ state.pointerEntityMap.delete(event.pointerId);
2610
+ break;
2611
+ }
2612
+ }
2613
+ }
2614
+ state.eventsBuffer.length = 0;
2615
+ });
2616
+
2617
+ // src/systems/input/screenSystem.ts
2618
+ import { getResources as getResources6 } from "@woven-ecs/core";
2619
+ var instanceState4 = /* @__PURE__ */ new WeakMap();
2620
+ function attachScreenObserver(domElement) {
2621
+ if (instanceState4.has(domElement)) return;
2622
+ const state = {
2623
+ needsUpdate: false,
2624
+ resizeObserver: new ResizeObserver(() => {
2625
+ state.needsUpdate = true;
2626
+ }),
2627
+ onScroll: () => {
2628
+ state.needsUpdate = true;
2629
+ }
2630
+ };
2631
+ instanceState4.set(domElement, state);
2632
+ state.resizeObserver.observe(domElement);
2633
+ window.addEventListener("scroll", state.onScroll, true);
2634
+ }
2635
+ function detachScreenObserver(domElement) {
2636
+ const state = instanceState4.get(domElement);
2637
+ if (!state) return;
2638
+ state.resizeObserver.disconnect();
2639
+ window.removeEventListener("scroll", state.onScroll, true);
2640
+ instanceState4.delete(domElement);
2641
+ }
2642
+ var screenSystem = defineEditorSystem({ phase: "input" }, (ctx) => {
2643
+ const resources = getResources6(ctx);
2644
+ const { domElement } = resources;
2645
+ const state = instanceState4.get(domElement);
2646
+ if (!state) return;
2647
+ const frame = Frame.read(ctx);
2648
+ if (frame.number === 1) {
2649
+ state.needsUpdate = true;
2650
+ }
2651
+ if (!state.needsUpdate) return;
2652
+ const screen = Screen.write(ctx);
2653
+ const rect = domElement.getBoundingClientRect();
2654
+ screen.left = rect.left;
2655
+ screen.top = rect.top;
2656
+ screen.width = rect.width;
2657
+ screen.height = rect.height;
2658
+ state.needsUpdate = false;
2659
+ });
2660
+
2661
+ // src/systems/postRender/cursorSystem.ts
2662
+ import { defineQuery as defineQuery2, getResources as getResources7 } from "@woven-ecs/core";
2663
+ var cursorQuery = defineQuery2((q) => q.tracking(Cursor));
2664
+ function getCursorSvg(cursors, kind, rotateZ) {
2665
+ const def = cursors[kind];
2666
+ if (!def) return "auto";
2667
+ const svg = def.makeSvg(rotateZ + def.rotationOffset);
2668
+ return `url("data:image/svg+xml,${encodeURIComponent(svg.trim())}") ${def.hotspot[0]} ${def.hotspot[1]}, auto`;
2669
+ }
2670
+ var cursorSystem = defineEditorSystem({ phase: "render", priority: -100 }, (ctx) => {
2671
+ const changedCursors = cursorQuery.changed(ctx);
2672
+ const frame = Frame.read(ctx);
2673
+ if (changedCursors.length === 0 && frame.number !== 1) {
2674
+ return;
2675
+ }
2676
+ const { cursorKind, rotation } = Cursor.getEffective(ctx);
2677
+ if (!cursorKind) {
2678
+ document.body.style.cursor = "default";
2679
+ return;
2680
+ }
2681
+ const { editor } = getResources7(ctx);
2682
+ const cursorValue = getCursorSvg(editor.cursors, cursorKind, rotation);
2683
+ document.body.style.cursor = cursorValue;
2684
+ });
2685
+
2686
+ // src/systems/postRender/presenceSystem.ts
2687
+ import { defineQuery as defineQuery5 } from "@woven-ecs/core";
2688
+
2689
+ // src/helpers/blockDefs.ts
2690
+ import { getResources as getResources8 } from "@woven-ecs/core";
2691
+ function getBlockDefs(ctx) {
2692
+ const { editor } = getResources8(ctx);
2693
+ return editor.blockDefs;
2694
+ }
2695
+ function getBlockDef(ctx, tag) {
2696
+ const blockDefs = getBlockDefs(ctx);
2697
+ return blockDefs[tag] ?? BlockDef2.parse({ tag });
2698
+ }
2699
+ function canBlockEdit(ctx, tag) {
2700
+ return getBlockDef(ctx, tag).editOptions.canEdit;
2701
+ }
2702
+
2703
+ // src/helpers/computeAabb.ts
2704
+ import { Aabb as Aabb2 } from "@woven-canvas/math";
2705
+ import { hasComponent } from "@woven-ecs/core";
2706
+ function computeAabb(ctx, entityId, out) {
2707
+ if (hasComponent(ctx, entityId, HitGeometry)) {
2708
+ const pts = HitGeometry.getExtremaWorld(ctx, entityId);
2709
+ Aabb2.setFromPoints(out, pts);
2710
+ } else {
2711
+ const corners = Block.getCorners(ctx, entityId);
2712
+ Aabb2.setFromPoints(out, corners);
2713
+ }
2714
+ }
2715
+
2716
+ // src/helpers/held.ts
2717
+ import { getResources as getResources9, hasComponent as hasComponent2 } from "@woven-ecs/core";
2718
+ function isHeldByRemote(ctx, entityId) {
2719
+ if (!hasComponent2(ctx, entityId, Held)) return false;
2720
+ const { sessionId } = getResources9(ctx);
2721
+ const held = Held.read(ctx, entityId);
2722
+ return held.sessionId !== "" && held.sessionId !== sessionId;
2723
+ }
2724
+
2725
+ // src/helpers/intersect.ts
2726
+ import { Aabb as Aabb3 } from "@woven-canvas/math";
2727
+ import { defineQuery as defineQuery3, hasComponent as hasComponent3 } from "@woven-ecs/core";
2728
+ var blocksWithAabb = defineQuery3((q) => q.with(Block, Aabb));
2729
+ function intersectPoint(ctx, point, entityIds) {
2730
+ const intersects = [];
2731
+ const entities = entityIds ?? blocksWithAabb.current(ctx);
2732
+ for (const entityId of entities) {
2733
+ if (!Aabb.containsPoint(ctx, entityId, point)) {
2734
+ continue;
2735
+ }
2736
+ if (hasComponent3(ctx, entityId, HitGeometry)) {
2737
+ if (!HitGeometry.containsPointWorld(ctx, entityId, point)) {
2738
+ continue;
2739
+ }
2740
+ } else {
2741
+ if (!Block.containsPoint(ctx, entityId, point)) {
2742
+ continue;
2743
+ }
2744
+ }
2745
+ intersects.push(entityId);
2746
+ }
2747
+ return sortByRankDescending(ctx, intersects);
2748
+ }
2749
+ function intersectAabb(ctx, bounds, entityIds) {
2750
+ const intersecting = [];
2751
+ const entities = entityIds ?? blocksWithAabb.current(ctx);
2752
+ for (const entityId of entities) {
2753
+ const { value: entityAabb } = Aabb.read(ctx, entityId);
2754
+ if (!Aabb3.intersects(bounds, entityAabb)) {
2755
+ continue;
2756
+ }
2757
+ if (Aabb3.contains(bounds, entityAabb)) {
2758
+ intersecting.push(entityId);
2759
+ continue;
2760
+ }
2761
+ if (hasComponent3(ctx, entityId, HitGeometry)) {
2762
+ if (HitGeometry.intersectsAabbWorld(ctx, entityId, bounds)) {
2763
+ intersecting.push(entityId);
2764
+ }
2765
+ } else {
2766
+ if (Block.intersectsAabb(ctx, entityId, bounds)) {
2767
+ intersecting.push(entityId);
2768
+ }
2769
+ }
2770
+ }
2771
+ return intersecting;
2772
+ }
2773
+ function sortByRankDescending(ctx, entityIds) {
2774
+ return entityIds.sort((a, b) => {
2775
+ const blockA = Block.read(ctx, a);
2776
+ const blockB = Block.read(ctx, b);
2777
+ const stratumA = getBlockDef(ctx, blockA.tag).stratum;
2778
+ const stratumB = getBlockDef(ctx, blockB.tag).stratum;
2779
+ const stratumOrderA = STRATUM_ORDER[stratumA];
2780
+ const stratumOrderB = STRATUM_ORDER[stratumB];
2781
+ if (stratumOrderB !== stratumOrderA) {
2782
+ return stratumOrderB - stratumOrderA;
2783
+ }
2784
+ const rankA = blockA.rank;
2785
+ const rankB = blockB.rank;
2786
+ if (!rankA && !rankB) return 0;
2787
+ if (!rankA) return 1;
2788
+ if (!rankB) return -1;
2789
+ if (rankB > rankA) return 1;
2790
+ if (rankB < rankA) return -1;
2791
+ return 0;
2792
+ });
2793
+ }
2794
+ function getTopmostBlockAtPoint(ctx, point) {
2795
+ const intersects = intersectPoint(ctx, point);
2796
+ return intersects[0];
2797
+ }
2798
+
2799
+ // src/helpers/intersectCapsule.ts
2800
+ import { Aabb as AabbNs, Arc as Arc2, Capsule as Capsule2, Mat2 as Mat22 } from "@woven-canvas/math";
2801
+ import { hasComponent as hasComponent4 } from "@woven-ecs/core";
2802
+ var _uvToWorldMatrix2 = [1, 0, 0, 1, 0, 0];
2803
+ function intersectCapsule(ctx, capsule, entityIds) {
2804
+ const intersects = [];
2805
+ const capsuleBounds = Capsule2.bounds(capsule);
2806
+ for (const entityId of entityIds) {
2807
+ const aabb = Aabb.read(ctx, entityId);
2808
+ if (!AabbNs.intersects(capsuleBounds, aabb.value)) {
2809
+ continue;
2810
+ }
2811
+ if (hasComponent4(ctx, entityId, HitGeometry)) {
2812
+ if (intersectsCapsuleHitGeometry(ctx, entityId, capsule)) {
2813
+ intersects.push(entityId);
2814
+ }
2815
+ } else {
2816
+ if (Capsule2.intersectsAabb(capsule, aabb.value)) {
2817
+ intersects.push(entityId);
2818
+ }
2819
+ }
2820
+ }
2821
+ return intersects;
2822
+ }
2823
+ function intersectsCapsuleHitGeometry(ctx, entityId, capsule) {
2824
+ const hitGeometry = HitGeometry.read(ctx, entityId);
2825
+ Block.getUvToWorldMatrix(ctx, entityId, _uvToWorldMatrix2);
2826
+ for (let i = 0; i < hitGeometry.arcCount; i++) {
2827
+ const uvArc = HitGeometry.getArcAt(ctx, entityId, i);
2828
+ const worldA = [uvArc[0], uvArc[1]];
2829
+ Mat22.transformPoint(_uvToWorldMatrix2, worldA);
2830
+ const worldB = [uvArc[2], uvArc[3]];
2831
+ Mat22.transformPoint(_uvToWorldMatrix2, worldB);
2832
+ const worldC = [uvArc[4], uvArc[5]];
2833
+ Mat22.transformPoint(_uvToWorldMatrix2, worldC);
2834
+ const thickness = uvArc[6];
2835
+ const worldArc = Arc2.create(worldA[0], worldA[1], worldB[0], worldB[1], worldC[0], worldC[1], thickness);
2836
+ const capsuleA = Capsule2.pointA(capsule);
2837
+ const capsuleB = Capsule2.pointB(capsule);
2838
+ if (Arc2.intersectsCapsule(worldArc, capsuleA, capsuleB, Capsule2.radius(capsule))) {
2839
+ return true;
2840
+ }
2841
+ }
2842
+ for (let i = 0; i < hitGeometry.capsuleCount; i++) {
2843
+ const uvCapsule = HitGeometry.getCapsuleAt(ctx, entityId, i);
2844
+ const worldA = [uvCapsule[0], uvCapsule[1]];
2845
+ Mat22.transformPoint(_uvToWorldMatrix2, worldA);
2846
+ const worldB = [uvCapsule[2], uvCapsule[3]];
2847
+ Mat22.transformPoint(_uvToWorldMatrix2, worldB);
2848
+ const radius = uvCapsule[4];
2849
+ const worldCapsule = Capsule2.create(worldA[0], worldA[1], worldB[0], worldB[1], radius);
2850
+ if (Capsule2.intersectsCapsule(capsule, worldCapsule)) {
2851
+ return true;
2852
+ }
2853
+ }
2854
+ return false;
2855
+ }
2856
+
2857
+ // src/helpers/user.ts
2858
+ import { defineQuery as defineQuery4, getResources as getResources10 } from "@woven-ecs/core";
2859
+ var userQuery = defineQuery4((q) => q.with(User));
2860
+ function getMyUserEntityId(ctx) {
2861
+ const mySessionId = getResources10(ctx).sessionId;
2862
+ for (const eid of userQuery.current(ctx)) {
2863
+ const user = User.read(ctx, eid);
2864
+ if (user.sessionId === mySessionId) {
2865
+ return eid;
2866
+ }
2867
+ }
2868
+ return null;
2869
+ }
2870
+
2871
+ // src/systems/postRender/presenceSystem.ts
2872
+ var pointerQuery = defineQuery5((q) => q.tracking(Pointer));
2873
+ var presenceSystem = defineEditorSystem({ phase: "render", priority: -100 }, (ctx) => {
2874
+ if (Mouse.didMove(ctx)) {
2875
+ const mouse = Mouse.read(ctx);
2876
+ updateUserPosition(ctx, mouse.position);
2877
+ return;
2878
+ }
2879
+ const changedPointers = pointerQuery.changed(ctx);
2880
+ if (changedPointers.length === 0) return;
2881
+ const pointers = pointerQuery.current(ctx);
2882
+ let mainPointerEid = null;
2883
+ let lowestId = Number.MAX_SAFE_INTEGER;
2884
+ for (const eid of pointers) {
2885
+ const pointer2 = Pointer.read(ctx, eid);
2886
+ if (pointer2.pointerId < lowestId) {
2887
+ lowestId = pointer2.pointerId;
2888
+ mainPointerEid = eid;
2889
+ }
2890
+ }
2891
+ const pointer = Pointer.read(ctx, mainPointerEid);
2892
+ updateUserPosition(ctx, pointer.position);
2893
+ });
2894
+ function updateUserPosition(ctx, position) {
2895
+ const myUserEid = getMyUserEntityId(ctx);
2896
+ if (myUserEid === null) return;
2897
+ const user = User.write(ctx, myUserEid);
2898
+ user.position = Camera.toWorld(ctx, position);
2899
+ }
2900
+
2901
+ // src/systems/preCapture/intersectSystem.ts
2902
+ import { addComponent as addComponent4, defineQuery as defineQuery6, hasComponent as hasComponent5, removeComponent } from "@woven-ecs/core";
2903
+ var blocksChanged = defineQuery6((q) => q.tracking(Block));
2904
+ var hitGeometryChanged = defineQuery6((q) => q.tracking(HitGeometry));
2905
+ var heldQuery = defineQuery6((q) => q.with(Held).tracking(Held));
2906
+ var hoveredQuery = defineQuery6((q) => q.with(Hovered));
2907
+ var pointerQuery2 = defineQuery6((q) => q.with(Pointer));
2908
+ function clearHovered(ctx) {
2909
+ for (const entityId of hoveredQuery.current(ctx)) {
2910
+ if (hasComponent5(ctx, entityId, Hovered)) {
2911
+ removeComponent(ctx, entityId, Hovered);
2912
+ }
2913
+ }
2914
+ }
2915
+ function findValidHoverTarget(ctx, intersected) {
2916
+ for (const entityId of intersected) {
2917
+ if (isHeldByRemote(ctx, entityId)) return void 0;
2918
+ return entityId;
2919
+ }
2920
+ return void 0;
2921
+ }
2922
+ function updateHovered(ctx, intersected) {
2923
+ const controls = Controls.read(ctx);
2924
+ const selectToolActive = controls.leftMouseTool === "select";
2925
+ clearHovered(ctx);
2926
+ if (!selectToolActive) return;
2927
+ const newHoveredId = findValidHoverTarget(ctx, intersected);
2928
+ if (newHoveredId !== void 0) {
2929
+ addComponent4(ctx, newHoveredId, Hovered, {});
2930
+ }
2931
+ }
2932
+ function arraysEqual(a, b) {
2933
+ if (a.length !== b.length) return false;
2934
+ for (let i = 0; i < a.length; i++) {
2935
+ if (a[i] !== b[i]) return false;
2936
+ }
2937
+ return true;
2938
+ }
2939
+ var added = /* @__PURE__ */ new Set();
2940
+ var changed = /* @__PURE__ */ new Set();
2941
+ var intersectSystem = defineEditorSystem({ phase: "capture", priority: 100 }, (ctx) => {
2942
+ added.clear();
2943
+ for (const entityId of blocksChanged.added(ctx)) {
2944
+ added.add(entityId);
2945
+ }
2946
+ for (const entityId of hitGeometryChanged.added(ctx)) {
2947
+ added.add(entityId);
2948
+ }
2949
+ changed.clear();
2950
+ for (const entityId of blocksChanged.changed(ctx)) {
2951
+ changed.add(entityId);
2952
+ }
2953
+ for (const entityId of hitGeometryChanged.changed(ctx)) {
2954
+ changed.add(entityId);
2955
+ }
2956
+ for (const entityId of added) {
2957
+ if (!hasComponent5(ctx, entityId, Aabb)) {
2958
+ addComponent4(ctx, entityId, Aabb, { value: [0, 0, 0, 0] });
2959
+ }
2960
+ const aabb = Aabb.write(ctx, entityId);
2961
+ computeAabb(ctx, entityId, aabb.value);
2962
+ }
2963
+ for (const entityId of changed) {
2964
+ const aabb = Aabb.write(ctx, entityId);
2965
+ computeAabb(ctx, entityId, aabb.value);
2966
+ }
2967
+ const mouseDidMove = Mouse.didMove(ctx);
2968
+ const mouseDidLeave = Mouse.didLeave(ctx);
2969
+ const mouseDidScroll = Mouse.didScroll(ctx);
2970
+ const blocksHaveChanged = added.size > 0 || changed.size > 0;
2971
+ const heldAdded = heldQuery.added(ctx);
2972
+ const heldRemoved = heldQuery.removed(ctx);
2973
+ const heldChanged = heldAdded.length > 0 || heldRemoved.length > 0;
2974
+ if (!mouseDidMove && !mouseDidLeave && !mouseDidScroll && !blocksHaveChanged && !heldChanged) {
2975
+ return;
2976
+ }
2977
+ if (mouseDidLeave) {
2978
+ Intersect.clear(ctx);
2979
+ clearHovered(ctx);
2980
+ return;
2981
+ }
2982
+ const mousePos = Mouse.getPosition(ctx);
2983
+ const worldPos = Camera.toWorld(ctx, mousePos);
2984
+ const intersected = intersectPoint(ctx, worldPos);
2985
+ const prevIntersected = Intersect.getAll(ctx);
2986
+ const intersectsChanged = !arraysEqual(intersected, prevIntersected);
2987
+ if (intersectsChanged) {
2988
+ Intersect.setAll(ctx, intersected);
2989
+ }
2990
+ const pointers = pointerQuery2.current(ctx);
2991
+ if (pointers.length > 0) {
2992
+ return;
2993
+ }
2994
+ if (intersectsChanged || heldChanged) {
2995
+ updateHovered(ctx, intersected);
2996
+ }
2997
+ });
2998
+
2999
+ // src/systems/preInput/rankBoundsSystem.ts
3000
+ import { Synced } from "@woven-ecs/canvas-store";
3001
+ import { defineQuery as defineQuery7 } from "@woven-ecs/core";
3002
+ var blocksQuery = defineQuery7((q) => q.with(Synced, Block));
3003
+ var rankBoundsSystem = defineEditorSystem({ phase: "input", priority: 100 }, (ctx) => {
3004
+ const added2 = blocksQuery.added(ctx);
3005
+ for (const entityId of added2) {
3006
+ const block = Block.read(ctx, entityId);
3007
+ if (block.rank === "") {
3008
+ const writableBlock = Block.write(ctx, entityId);
3009
+ writableBlock.rank = RankBounds.genNext(ctx);
3010
+ } else {
3011
+ RankBounds.add(ctx, block.rank);
3012
+ }
3013
+ }
3014
+ });
3015
+
3016
+ // src/systems/preRender/canSeeBlocksSystem.ts
3017
+ import { Aabb as AabbMath2 } from "@woven-canvas/math";
3018
+ import { Synced as Synced2 } from "@woven-ecs/canvas-store";
3019
+ import { defineQuery as defineQuery8, hasComponent as hasComponent6 } from "@woven-ecs/core";
3020
+ var camerasQuery = defineQuery8((q) => q.tracking(Camera));
3021
+ var blocksQuery2 = defineQuery8((q) => q.with(Synced2, Block, Aabb));
3022
+ var syncedBlocksQuery = defineQuery8((q) => q.with(Synced2, Block, Aabb));
3023
+ var _cameraAabb = [0, 0, 0, 0];
3024
+ var canSeeBlocksSystem = defineEditorSystem({ phase: "render", priority: 90 }, (ctx) => {
3025
+ if (camerasQuery.changed(ctx).length === 0 && syncedBlocksQuery.addedOrRemoved(ctx).length === 0) {
3026
+ return;
3027
+ }
3028
+ const camera = Camera.read(ctx);
3029
+ const cameraAabb = Camera.getAabb(ctx, _cameraAabb);
3030
+ if (camera.canSeeBlocks && camera.lastSeenBlock !== null) {
3031
+ if (hasComponent6(ctx, camera.lastSeenBlock, Aabb)) {
3032
+ const aabb = Aabb.read(ctx, camera.lastSeenBlock);
3033
+ if (AabbMath2.intersects(cameraAabb, aabb.value)) {
3034
+ return;
3035
+ }
3036
+ }
3037
+ }
3038
+ let seenBlock = null;
3039
+ for (const entityId of blocksQuery2.current(ctx)) {
3040
+ const aabb = Aabb.read(ctx, entityId);
3041
+ if (AabbMath2.intersects(cameraAabb, aabb.value)) {
3042
+ seenBlock = entityId;
3043
+ break;
3044
+ }
3045
+ }
3046
+ const canSeeBlocks = seenBlock !== null;
3047
+ if (canSeeBlocks === camera.canSeeBlocks) {
3048
+ if (seenBlock !== camera.lastSeenBlock) {
3049
+ Camera.write(ctx).lastSeenBlock = seenBlock;
3050
+ }
3051
+ return;
3052
+ }
3053
+ const writableCamera = Camera.write(ctx);
3054
+ writableCamera.canSeeBlocks = canSeeBlocks;
3055
+ writableCamera.lastSeenBlock = seenBlock;
3056
+ });
3057
+
3058
+ // src/systems/preRender/scaleWithZoomSystem.ts
3059
+ import { Scalar, Vec2 as Vec22 } from "@woven-canvas/math";
3060
+ import { defineQuery as defineQuery9 } from "@woven-ecs/core";
3061
+ var scaleWithZoomQuery = defineQuery9((q) => q.with(Block).tracking(ScaleWithZoom));
3062
+ var _scaledSize = [0, 0];
3063
+ var _anchorOffset = [0, 0];
3064
+ var scaleWithZoomSystem = defineEditorSystem({ phase: "render", priority: 100 }, (ctx) => {
3065
+ const camera = Camera.read(ctx);
3066
+ const state = ScaleWithZoomState.read(ctx);
3067
+ const zoomChanged = !Scalar.approxEqual(camera.zoom, state.lastZoom);
3068
+ if (zoomChanged) {
3069
+ for (const entityId of scaleWithZoomQuery.current(ctx)) {
3070
+ scaleBlock(ctx, entityId, camera.zoom);
3071
+ }
3072
+ ScaleWithZoomState.write(ctx).lastZoom = camera.zoom;
3073
+ }
3074
+ for (const entityId of scaleWithZoomQuery.addedOrChanged(ctx)) {
3075
+ scaleBlock(ctx, entityId, camera.zoom);
3076
+ }
3077
+ });
3078
+ function scaleBlock(ctx, entityId, zoom) {
3079
+ const block = Block.write(ctx, entityId);
3080
+ const swz = ScaleWithZoom.read(ctx, entityId);
3081
+ const baseScale = 1 / zoom;
3082
+ const scaleX = 1 + (baseScale - 1) * swz.scaleMultiplier[0];
3083
+ const scaleY = 1 + (baseScale - 1) * swz.scaleMultiplier[1];
3084
+ _scaledSize[0] = swz.startSize[0] * scaleX;
3085
+ _scaledSize[1] = swz.startSize[1] * scaleY;
3086
+ Vec22.copy(_anchorOffset, swz.startSize);
3087
+ Vec22.sub(_anchorOffset, _scaledSize);
3088
+ Vec22.multiply(_anchorOffset, swz.anchor);
3089
+ Vec22.copy(block.position, swz.startPosition);
3090
+ Vec22.add(block.position, _anchorOffset);
3091
+ Vec22.copy(block.size, _scaledSize);
3092
+ }
3093
+
3094
+ // src/CorePlugin.ts
3095
+ var CorePlugin = {
3096
+ name: PLUGIN_NAME,
3097
+ singletons: Object.values(singletons_exports).filter((v) => v instanceof CanvasSingletonDef10),
3098
+ components: Object.values(components_exports).filter((v) => v instanceof CanvasComponentDef6),
3099
+ blockDefs: [
3100
+ {
3101
+ tag: "sticky-note",
3102
+ components: [Color, Text, VerticalAlign],
3103
+ editOptions: {
3104
+ canEdit: true
3105
+ }
3106
+ },
3107
+ {
3108
+ tag: "text",
3109
+ components: [Text],
3110
+ resizeMode: "text",
3111
+ editOptions: {
3112
+ canEdit: true,
3113
+ removeWhenTextEmpty: true
3114
+ }
3115
+ },
3116
+ {
3117
+ tag: "image",
3118
+ components: [Image, Asset],
3119
+ resizeMode: "scale"
3120
+ },
3121
+ {
3122
+ tag: "shape",
3123
+ components: [Shape, Text, VerticalAlign],
3124
+ resizeMode: "free",
3125
+ editOptions: {
3126
+ canEdit: true
3127
+ }
3128
+ }
3129
+ ],
3130
+ systems: [
3131
+ // Input phase
3132
+ frameSystem,
3133
+ // priority: 100
3134
+ rankBoundsSystem,
3135
+ // priority: 100
3136
+ keyboardSystem,
3137
+ mouseSystem,
3138
+ screenSystem,
3139
+ pointerSystem,
3140
+ // Capture phase
3141
+ intersectSystem,
3142
+ // priority: 100
3143
+ keybindSystem,
3144
+ // Render phase
3145
+ scaleWithZoomSystem,
3146
+ // priority: 100
3147
+ canSeeBlocksSystem,
3148
+ // priority: 90
3149
+ cursorSystem,
3150
+ // priority: -100
3151
+ presenceSystem
3152
+ // priority: -100
3153
+ ],
3154
+ setup(ctx) {
3155
+ const { domElement } = getResources11(ctx);
3156
+ attachKeyboardListeners(domElement);
3157
+ attachMouseListeners(domElement);
3158
+ attachScreenObserver(domElement);
3159
+ attachPointerListeners(domElement);
3160
+ },
3161
+ teardown(ctx) {
3162
+ const { domElement } = getResources11(ctx);
3163
+ detachKeyboardListeners(domElement);
3164
+ detachMouseListeners(domElement);
3165
+ detachScreenObserver(domElement);
3166
+ detachPointerListeners(domElement);
3167
+ }
3168
+ };
3169
+
3170
+ // src/Editor.ts
3171
+ import { Synced as Synced3 } from "@woven-ecs/canvas-store";
3172
+ import {
3173
+ addComponent as addComponent5,
3174
+ createEntity as createEntity4,
3175
+ World
3176
+ } from "@woven-ecs/core";
3177
+
3178
+ // src/FontLoader.ts
3179
+ import { z as z2 } from "zod";
3180
+ var FontFamily = z2.object({
3181
+ /** The CSS font-family name */
3182
+ name: z2.string(),
3183
+ /** Display name shown in the font selector UI */
3184
+ displayName: z2.string(),
3185
+ /** URL to the font stylesheet (e.g., Google Fonts URL) */
3186
+ url: z2.string(),
3187
+ /** Optional preview image URL for the font selector */
3188
+ previewImage: z2.string().optional(),
3189
+ /** Whether this font appears in the font selector (default: true) */
3190
+ selectable: z2.boolean().default(true),
3191
+ /**
3192
+ * Font weights to load (e.g., [400, 700] for regular and bold).
3193
+ * Only applies to Google Fonts URLs - will auto-construct the variant URL.
3194
+ * Default: [400, 700]
3195
+ */
3196
+ weights: z2.array(z2.number()).default([400, 700]),
3197
+ /**
3198
+ * Whether to also load italic variants for each weight.
3199
+ * Only applies to Google Fonts URLs.
3200
+ * Default: true
3201
+ */
3202
+ italics: z2.boolean().default(true)
3203
+ });
3204
+ var FontLoader = class {
3205
+ constructor() {
3206
+ this.loadedFonts = /* @__PURE__ */ new Set();
3207
+ }
3208
+ /**
3209
+ * Load multiple font families.
3210
+ * Fonts that have already been loaded will be skipped.
3211
+ *
3212
+ * @param families - Array of font families to load
3213
+ */
3214
+ async loadFonts(families) {
3215
+ const unloadedFamilies = families.filter((family) => !this.loadedFonts.has(family.name));
3216
+ if (unloadedFamilies.length === 0) {
3217
+ return;
3218
+ }
3219
+ const fontPromises = unloadedFamilies.map((family) => this.loadSingleFont(family));
3220
+ await Promise.all(fontPromises);
3221
+ }
3222
+ /**
3223
+ * Build a Google Fonts URL with weight and italic variants.
3224
+ * Format: https://fonts.googleapis.com/css2?family=FontName:ital,wght@0,400;0,700;1,400;1,700
3225
+ *
3226
+ * @param family - Font family configuration
3227
+ * @returns The constructed URL with variants
3228
+ */
3229
+ buildGoogleFontsUrl(family) {
3230
+ const baseUrl = family.url;
3231
+ if (!baseUrl.includes("fonts.googleapis.com")) {
3232
+ return baseUrl;
3233
+ }
3234
+ const urlObj = new URL(baseUrl);
3235
+ const familyParam = urlObj.searchParams.get("family");
3236
+ if (!familyParam) {
3237
+ return baseUrl;
3238
+ }
3239
+ const baseFamilyName = familyParam.split(":")[0];
3240
+ const weights = family.weights;
3241
+ const variants = [];
3242
+ if (family.italics) {
3243
+ for (const weight of weights) {
3244
+ variants.push(`0,${weight}`);
3245
+ }
3246
+ for (const weight of weights) {
3247
+ variants.push(`1,${weight}`);
3248
+ }
3249
+ urlObj.searchParams.set("family", `${baseFamilyName}:ital,wght@${variants.join(";")}`);
3250
+ } else {
3251
+ urlObj.searchParams.set("family", `${baseFamilyName}:wght@${weights.join(";")}`);
3252
+ }
3253
+ return urlObj.toString();
3254
+ }
3255
+ /**
3256
+ * Load a single font family.
3257
+ * Adds a link element to the document head and waits for the font to be ready.
3258
+ *
3259
+ * @param family - Font family to load
3260
+ */
3261
+ loadSingleFont(family) {
3262
+ this.loadedFonts.add(family.name);
3263
+ return new Promise((resolve, reject) => {
3264
+ const link = document.createElement("link");
3265
+ link.rel = "stylesheet";
3266
+ link.href = this.buildGoogleFontsUrl(family);
3267
+ link.onload = async () => {
3268
+ try {
3269
+ const loadPromises = this.getFontLoadPromises(family);
3270
+ await Promise.all(loadPromises);
3271
+ await document.fonts.ready;
3272
+ resolve();
3273
+ } catch (error) {
3274
+ reject(new Error(`Failed to load font face for: ${family.name}. ${error}`));
3275
+ }
3276
+ };
3277
+ link.onerror = () => {
3278
+ reject(new Error(`Failed to load font stylesheet: ${family.name}`));
3279
+ };
3280
+ document.head.appendChild(link);
3281
+ });
3282
+ }
3283
+ /**
3284
+ * Get promises for loading all font variants (weights and italics).
3285
+ *
3286
+ * @param family - Font family configuration
3287
+ * @returns Array of promises for loading each font variant
3288
+ */
3289
+ getFontLoadPromises(family) {
3290
+ const promises = [];
3291
+ const fontName = family.name;
3292
+ for (const weight of family.weights) {
3293
+ promises.push(document.fonts.load(`${weight} 12px "${fontName}"`));
3294
+ if (family.italics) {
3295
+ promises.push(document.fonts.load(`italic ${weight} 12px "${fontName}"`));
3296
+ }
3297
+ }
3298
+ return promises;
3299
+ }
3300
+ /**
3301
+ * Check if a font has been loaded.
3302
+ *
3303
+ * @param fontName - The font-family name to check
3304
+ * @returns True if the font has been loaded
3305
+ */
3306
+ isLoaded(fontName) {
3307
+ return this.loadedFonts.has(fontName);
3308
+ }
3309
+ /**
3310
+ * Get the set of loaded font names.
3311
+ */
3312
+ getLoadedFonts() {
3313
+ return this.loadedFonts;
3314
+ }
3315
+ };
3316
+
3317
+ // src/plugin.ts
3318
+ function parsePlugin(input) {
3319
+ if (typeof input === "function") {
3320
+ return input({});
3321
+ }
3322
+ return input;
3323
+ }
3324
+ function sortPluginsByDependencies(plugins) {
3325
+ const sorted = [];
3326
+ const visited = /* @__PURE__ */ new Set();
3327
+ const visiting = /* @__PURE__ */ new Set();
3328
+ const pluginMap = new Map(plugins.map((p) => [p.name, p]));
3329
+ function visit(plugin) {
3330
+ if (visited.has(plugin.name)) return;
3331
+ if (visiting.has(plugin.name)) {
3332
+ throw new Error(`Circular plugin dependency detected: ${plugin.name}`);
3333
+ }
3334
+ visiting.add(plugin.name);
3335
+ for (const depName of plugin.dependencies ?? []) {
3336
+ const dep = pluginMap.get(depName);
3337
+ if (!dep) {
3338
+ throw new Error(`Plugin "${plugin.name}" depends on "${depName}" which is not registered`);
3339
+ }
3340
+ visit(dep);
3341
+ }
3342
+ visiting.delete(plugin.name);
3343
+ visited.add(plugin.name);
3344
+ sorted.push(plugin);
3345
+ }
3346
+ for (const plugin of plugins) {
3347
+ visit(plugin);
3348
+ }
3349
+ return sorted;
3350
+ }
3351
+
3352
+ // src/Editor.ts
3353
+ var PHASE_ORDER = ["input", "capture", "update", "render"];
3354
+ function batchSystems(systems) {
3355
+ const byPhase = /* @__PURE__ */ new Map();
3356
+ for (const phase of PHASE_ORDER) {
3357
+ byPhase.set(phase, []);
3358
+ }
3359
+ for (const system of systems) {
3360
+ byPhase.get(system.phase).push(system);
3361
+ }
3362
+ const result = [];
3363
+ for (const phase of PHASE_ORDER) {
3364
+ const phaseSystems = byPhase.get(phase);
3365
+ const byPriority = /* @__PURE__ */ new Map();
3366
+ for (const system of phaseSystems) {
3367
+ const group = byPriority.get(system.priority) ?? [];
3368
+ group.push(system);
3369
+ byPriority.set(system.priority, group);
3370
+ }
3371
+ const priorities = [...byPriority.keys()].sort((a, b) => b - a);
3372
+ for (const p of priorities) {
3373
+ result.push(byPriority.get(p));
3374
+ }
3375
+ }
3376
+ return result;
3377
+ }
3378
+ var Editor = class {
3379
+ constructor(domElement, optionsInput) {
3380
+ this.cursors = {};
3381
+ this.blockDefs = {};
3382
+ this.fonts = [];
3383
+ this.components = [];
3384
+ this.singletons = [];
3385
+ const options = EditorOptionsSchema.parse(optionsInput ?? {});
3386
+ const { plugins: pluginInputs, maxEntities } = options;
3387
+ const user = UserData.parse(options.user ?? {});
3388
+ this.userData = user;
3389
+ this.gridOptions = GridOptions.parse(options.grid ?? {});
3390
+ this.controlsOptions = ControlsOptions.parse(options.controls ?? {});
3391
+ const plugins = pluginInputs.map(parsePlugin);
3392
+ const sortedPlugins = sortPluginsByDependencies([CorePlugin, ...plugins]);
3393
+ const allDefs = [CommandMarker, Synced3];
3394
+ for (const plugin of sortedPlugins) {
3395
+ if (plugin.components) {
3396
+ allDefs.push(...plugin.components);
3397
+ }
3398
+ if (plugin.singletons) {
3399
+ allDefs.push(...plugin.singletons);
3400
+ }
3401
+ }
3402
+ allDefs.push(...options.components);
3403
+ allDefs.push(...options.singletons);
3404
+ const keybinds = options.keybinds;
3405
+ if (!options.omitPluginKeybinds) {
3406
+ for (const plugin of sortedPlugins) {
3407
+ if (plugin.keybinds) {
3408
+ keybinds.push(...plugin.keybinds);
3409
+ }
3410
+ }
3411
+ }
3412
+ this.keybinds = keybinds;
3413
+ const blockDefs = {};
3414
+ for (const input of options.blockDefs) {
3415
+ const parsed = BlockDef2.parse(input);
3416
+ blockDefs[parsed.tag] = parsed;
3417
+ }
3418
+ for (const plugin of sortedPlugins) {
3419
+ if (plugin.blockDefs) {
3420
+ for (const input of plugin.blockDefs) {
3421
+ const parsed = BlockDef2.parse(input);
3422
+ blockDefs[parsed.tag] = parsed;
3423
+ }
3424
+ }
3425
+ }
3426
+ this.blockDefs = blockDefs;
3427
+ const cursors = {};
3428
+ Object.assign(cursors, options.cursors);
3429
+ if (!options.omitPluginCursors) {
3430
+ for (const plugin of sortedPlugins) {
3431
+ if (plugin.cursors) {
3432
+ Object.assign(cursors, plugin.cursors);
3433
+ }
3434
+ }
3435
+ }
3436
+ this.cursors = cursors;
3437
+ this.fontLoader = new FontLoader();
3438
+ const fontInputs = [...options.fonts];
3439
+ if (!options.omitPluginFonts) {
3440
+ for (const plugin of sortedPlugins) {
3441
+ if (plugin.fonts) {
3442
+ fontInputs.push(...plugin.fonts);
3443
+ }
3444
+ }
3445
+ }
3446
+ this.fonts = fontInputs.map((input) => FontFamily.parse(input));
3447
+ const pluginResources = {};
3448
+ for (const plugin of sortedPlugins) {
3449
+ if (plugin.resources !== void 0) {
3450
+ pluginResources[plugin.name] = plugin.resources;
3451
+ }
3452
+ }
3453
+ const componentsByName = /* @__PURE__ */ new Map();
3454
+ const singletonsByName = /* @__PURE__ */ new Map();
3455
+ const componentsById = /* @__PURE__ */ new Map();
3456
+ const singletonsById = /* @__PURE__ */ new Map();
3457
+ const allResources = {
3458
+ domElement,
3459
+ editor: this,
3460
+ userId: user.userId,
3461
+ sessionId: user.sessionId,
3462
+ pluginResources,
3463
+ componentsByName,
3464
+ singletonsByName,
3465
+ componentsById,
3466
+ singletonsById
3467
+ };
3468
+ this.world = new World(allDefs, {
3469
+ maxEntities,
3470
+ resources: allResources
3471
+ });
3472
+ const allSystems = [];
3473
+ for (const plugin of sortedPlugins) {
3474
+ if (plugin.systems) {
3475
+ allSystems.push(...plugin.systems);
3476
+ }
3477
+ }
3478
+ allSystems.push(...options.systems);
3479
+ this.systemBatches = batchSystems(allSystems);
3480
+ this.plugins = new Map(sortedPlugins.map((p) => [p.name, p]));
3481
+ for (const plugin of sortedPlugins) {
3482
+ if (plugin.components) {
3483
+ for (const comp of plugin.components) {
3484
+ const componentId = comp._getComponentId(this.ctx);
3485
+ componentsById.set(componentId, comp);
3486
+ componentsByName.set(comp.name, comp);
3487
+ }
3488
+ }
3489
+ if (plugin.singletons) {
3490
+ for (const singleton of plugin.singletons) {
3491
+ const componentId = singleton._getComponentId(this.ctx);
3492
+ singletonsById.set(componentId, singleton);
3493
+ singletonsByName.set(singleton.name, singleton);
3494
+ }
3495
+ }
3496
+ }
3497
+ for (const comp of options.components) {
3498
+ const componentId = comp._getComponentId(this.ctx);
3499
+ componentsById.set(componentId, comp);
3500
+ componentsByName.set(comp.name, comp);
3501
+ }
3502
+ for (const singleton of options.singletons) {
3503
+ const componentId = singleton._getComponentId(this.ctx);
3504
+ singletonsById.set(componentId, singleton);
3505
+ singletonsByName.set(singleton.name, singleton);
3506
+ }
3507
+ this.components = [...componentsByName.values()];
3508
+ this.singletons = [...singletonsByName.values()];
3509
+ }
3510
+ /**
3511
+ * Get the ECS context.
3512
+ * Use this to access ECS functions and resources.
3513
+ */
3514
+ get ctx() {
3515
+ return this.world._getContext();
3516
+ }
3517
+ /**
3518
+ * Initialize the editor.
3519
+ * Call this after construction to run async setup.
3520
+ */
3521
+ async initialize() {
3522
+ if (this.fonts.length > 0) {
3523
+ await this.fontLoader.loadFonts(this.fonts);
3524
+ }
3525
+ Grid.copy(this.ctx, this.gridOptions);
3526
+ Controls.copy(this.ctx, this.controlsOptions);
3527
+ this.nextTick((ctx) => {
3528
+ const userEntity = createEntity4(ctx);
3529
+ addComponent5(ctx, userEntity, Synced3, {
3530
+ id: crypto.randomUUID()
3531
+ });
3532
+ addComponent5(ctx, userEntity, User, {
3533
+ userId: this.userData.userId,
3534
+ sessionId: this.userData.sessionId,
3535
+ color: this.userData.color,
3536
+ name: this.userData.name,
3537
+ avatar: this.userData.avatar
3538
+ });
3539
+ });
3540
+ for (const plugin of this.plugins.values()) {
3541
+ if (plugin.setup) {
3542
+ await plugin.setup(this.ctx);
3543
+ }
3544
+ }
3545
+ }
3546
+ /**
3547
+ * Run one frame of the editor loop.
3548
+ *
3549
+ * Executes systems in phase order:
3550
+ * 1. Input - convert raw events to ECS state
3551
+ * 2. Capture - detect targets, compute intersections
3552
+ * 3. Update - modify document state, process commands
3553
+ * 4. Render - sync ECS state to output
3554
+ *
3555
+ * Commands spawned via `command()` are available during this frame
3556
+ * and automatically cleaned up at the end.
3557
+ */
3558
+ async tick() {
3559
+ this.world.sync();
3560
+ for (const batch of this.systemBatches) {
3561
+ const systems = batch.map((s) => s._system);
3562
+ await this.world.execute(...systems);
3563
+ }
3564
+ const currentEventIndex = this.ctx.eventBuffer.getWriteIndex();
3565
+ this.ctx.prevEventIndex = currentEventIndex;
3566
+ this.ctx.currEventIndex = currentEventIndex;
3567
+ cleanupCommands(this.ctx);
3568
+ }
3569
+ /**
3570
+ * Schedule work for the next tick.
3571
+ * Use this from event handlers to safely modify ECS state.
3572
+ *
3573
+ * @param callback - Function to execute at next tick
3574
+ *
3575
+ * @example
3576
+ * ```typescript
3577
+ * element.addEventListener('click', () => {
3578
+ * editor.nextTick((ctx) => {
3579
+ * const block = Block.write(ctx, entityId);
3580
+ * block.selected = true;
3581
+ * });
3582
+ * });
3583
+ * ```
3584
+ */
3585
+ nextTick(callback) {
3586
+ return this.world.nextSync((ctx) => {
3587
+ callback(ctx);
3588
+ });
3589
+ }
3590
+ /**
3591
+ * Spawn a command to be processed this frame.
3592
+ * Commands are ephemeral entities that systems can react to via `CommandDef.iter()`.
3593
+ *
3594
+ * @param def - The command definition created with `defineCommand()`
3595
+ * @param payload - Command payload data
3596
+ *
3597
+ * @example
3598
+ * ```typescript
3599
+ * const SelectAll = defineCommand<{ filter?: string }>("select-all");
3600
+ *
3601
+ * // From UI event handler
3602
+ * editor.command(SelectAll, { filter: "blocks" });
3603
+ * ```
3604
+ */
3605
+ command(def, ...args) {
3606
+ const payload = args[0];
3607
+ this.nextTick((ctx) => {
3608
+ def.spawn(ctx, payload);
3609
+ });
3610
+ }
3611
+ /**
3612
+ * Subscribe to query changes.
3613
+ *
3614
+ * @param query - Query definition
3615
+ * @param callback - Called when query results change
3616
+ * @returns Unsubscribe function
3617
+ *
3618
+ * @example
3619
+ * ```typescript
3620
+ * const unsubscribe = editor.subscribe(selectedBlocks, (ctx, { added, removed }) => {
3621
+ * console.log('Selection changed:', added, removed);
3622
+ * });
3623
+ *
3624
+ * // Later:
3625
+ * unsubscribe();
3626
+ * ```
3627
+ */
3628
+ subscribe(query, callback) {
3629
+ return this.world.subscribe(query, (ctx, result) => {
3630
+ callback(ctx, result);
3631
+ });
3632
+ }
3633
+ /**
3634
+ * Get the ECS context.
3635
+ * Only for testing and advanced use cases.
3636
+ * @internal
3637
+ */
3638
+ _getContext() {
3639
+ return this.ctx;
3640
+ }
3641
+ /**
3642
+ * Clean up the editor.
3643
+ * Call this when done to release resources.
3644
+ */
3645
+ dispose() {
3646
+ const plugins = Array.from(this.plugins.values()).reverse();
3647
+ for (const plugin of plugins) {
3648
+ if (plugin.teardown) {
3649
+ plugin.teardown(this.ctx);
3650
+ }
3651
+ }
3652
+ this.world.dispose();
3653
+ this.plugins.clear();
3654
+ this.systemBatches = [];
3655
+ }
3656
+ };
3657
+
3658
+ // src/EditorStateDef.ts
3659
+ import { CanvasSingletonDef as CanvasSingletonDef11 } from "@woven-ecs/canvas-store";
3660
+
3661
+ // src/machine.ts
3662
+ import { transition } from "xstate";
3663
+ function runMachine(machine, currentState, context, events) {
3664
+ if (events.length === 0) {
3665
+ return { value: currentState, context };
3666
+ }
3667
+ let state = machine.resolveState({
3668
+ value: String(currentState),
3669
+ context
3670
+ });
3671
+ for (const event of events) {
3672
+ const [nextState, actions] = transition(machine, state, event);
3673
+ state = nextState;
3674
+ for (const action of actions) {
3675
+ if (typeof action.exec === "function") {
3676
+ action.exec(action.info, action.params);
3677
+ }
3678
+ }
3679
+ }
3680
+ return {
3681
+ value: state.value,
3682
+ context: state.context
3683
+ };
3684
+ }
3685
+
3686
+ // src/EditorStateDef.ts
3687
+ var EditorStateDef = class extends CanvasSingletonDef11 {
3688
+ constructor(name, schema) {
3689
+ super({ name, sync: "none" }, schema);
3690
+ }
3691
+ /**
3692
+ * Get the current state value.
3693
+ *
3694
+ * @param ctx - The ECS context
3695
+ * @returns The current state machine state value
3696
+ */
3697
+ getState(ctx) {
3698
+ return this.read(ctx).state;
3699
+ }
3700
+ /**
3701
+ * Get the machine context (all fields except 'state').
3702
+ *
3703
+ * @param ctx - The ECS context
3704
+ * @returns Plain object with context field values
3705
+ */
3706
+ getContext(ctx) {
3707
+ const snapshot = this.snapshot(ctx);
3708
+ const result = {};
3709
+ for (const key of Object.keys(snapshot)) {
3710
+ if (key !== "state") {
3711
+ result[key] = snapshot[key];
3712
+ }
3713
+ }
3714
+ return result;
3715
+ }
3716
+ /**
3717
+ * Run an XState machine through events and update the singleton state.
3718
+ *
3719
+ * This method encapsulates the common pattern of:
3720
+ * 1. Reading the current state and context
3721
+ * 2. Running the machine through events
3722
+ * 3. Writing the updated state and context back
3723
+ *
3724
+ * @param ctx - The ECS context
3725
+ * @param machine - The XState machine definition
3726
+ * @param events - Array of events to process (must have a 'type' property)
3727
+ * @returns The resulting state value and context
3728
+ *
3729
+ * @example
3730
+ * ```typescript
3731
+ * const capturePanSystem = defineSystem((ctx) => {
3732
+ * const events = getPointerInput(ctx, ["middle"]);
3733
+ * if (events.length === 0) return;
3734
+ *
3735
+ * PanState.run(ctx, createPanMachine(ctx), events);
3736
+ * });
3737
+ * ```
3738
+ */
3739
+ run(ctx, machine, events) {
3740
+ const currentState = this.getState(ctx);
3741
+ const currentContext = this.getContext(ctx);
3742
+ const result = runMachine(
3743
+ machine,
3744
+ currentState,
3745
+ currentContext,
3746
+ events
3747
+ );
3748
+ const writable = this.write(ctx);
3749
+ writable.state = result.value;
3750
+ for (const [key, value] of Object.entries(result.context)) {
3751
+ ;
3752
+ writable[key] = value;
3753
+ }
3754
+ return result;
3755
+ }
3756
+ };
3757
+ function defineEditorState(name, schema) {
3758
+ return new EditorStateDef(name, schema);
3759
+ }
3760
+
3761
+ // src/events/frameInputEvents.ts
3762
+ function getFrameInput(ctx) {
3763
+ const frame = Frame.read(ctx);
3764
+ return {
3765
+ type: "frame",
3766
+ ctx,
3767
+ delta: frame.delta,
3768
+ frameNumber: frame.number,
3769
+ time: frame.time
3770
+ };
3771
+ }
3772
+
3773
+ // src/events/keyboardInputEvents.ts
3774
+ function getKeyboardInput(ctx, keys) {
3775
+ const events = [];
3776
+ for (const key of keys) {
3777
+ if (Keyboard.isKeyDownTrigger(ctx, key)) {
3778
+ events.push({ type: "keyDown", key, ctx });
3779
+ }
3780
+ if (Keyboard.isKeyUpTrigger(ctx, key)) {
3781
+ events.push({ type: "keyUp", key, ctx });
3782
+ }
3783
+ }
3784
+ return events;
3785
+ }
3786
+
3787
+ // src/events/mouseInputEvents.ts
3788
+ function getMouseInput(ctx) {
3789
+ const events = [];
3790
+ const mouse = Mouse.read(ctx);
3791
+ const screenPos = [mouse.position[0], mouse.position[1]];
3792
+ const worldPos = Camera.toWorld(ctx, screenPos);
3793
+ if (mouse.wheelTrigger) {
3794
+ events.push({
3795
+ type: "wheel",
3796
+ ctx,
3797
+ screenPosition: screenPos,
3798
+ worldPosition: worldPos,
3799
+ wheelDeltaX: mouse.wheelDeltaX,
3800
+ wheelDeltaY: mouse.wheelDeltaY
3801
+ });
3802
+ }
3803
+ if (mouse.moveTrigger) {
3804
+ events.push({
3805
+ type: "mouseMove",
3806
+ ctx,
3807
+ screenPosition: screenPos,
3808
+ worldPosition: worldPos,
3809
+ wheelDeltaX: 0,
3810
+ wheelDeltaY: 0
3811
+ });
3812
+ }
3813
+ return events;
3814
+ }
3815
+
3816
+ // src/events/pointerInputEvents.ts
3817
+ import { Vec2 as Vec23 } from "@woven-canvas/math";
3818
+ import { defineQuery as defineQuery10 } from "@woven-ecs/core";
3819
+ var DEFAULT_CLICK_MOVE_THRESHOLD = 3;
3820
+ var DEFAULT_CLICK_FRAME_THRESHOLD = 30;
3821
+ var pointerQuery3 = defineQuery10((q) => q.tracking(Pointer));
3822
+ var trackingState = /* @__PURE__ */ new Map();
3823
+ function getTrackingState(ctx) {
3824
+ const key = ctx.readerId;
3825
+ let state = trackingState.get(key);
3826
+ if (!state) {
3827
+ state = {
3828
+ prevPositions: /* @__PURE__ */ new Map(),
3829
+ prevModifiers: {
3830
+ shiftDown: false,
3831
+ altDown: false,
3832
+ modDown: false
3833
+ },
3834
+ prevCamera: {
3835
+ left: 0,
3836
+ top: 0,
3837
+ zoom: 1
3838
+ }
3839
+ };
3840
+ trackingState.set(key, state);
3841
+ }
3842
+ return state;
3843
+ }
3844
+ function getPointerInput(ctx, buttons, options = {}) {
3845
+ if (buttons.length === 0) return [];
3846
+ const {
3847
+ includeFrameEvent = false,
3848
+ clickMoveThreshold = DEFAULT_CLICK_MOVE_THRESHOLD,
3849
+ clickFrameThreshold = DEFAULT_CLICK_FRAME_THRESHOLD
3850
+ } = options;
3851
+ const state = getTrackingState(ctx);
3852
+ const frameNumber = Frame.read(ctx).number;
3853
+ const events = [];
3854
+ const keyboard = Keyboard.read(ctx);
3855
+ const modifiers = {
3856
+ shiftDown: keyboard.shiftDown,
3857
+ altDown: keyboard.altDown,
3858
+ modDown: keyboard.modDown
3859
+ };
3860
+ const camera = Camera.read(ctx);
3861
+ const matchesButtons = (entityId) => {
3862
+ const pointer = Pointer.read(ctx, entityId);
3863
+ return buttons.includes(pointer.button);
3864
+ };
3865
+ const intersects = Intersect.getAll(ctx);
3866
+ const createEvent = (type, entityId) => {
3867
+ const pointer = Pointer.read(ctx, entityId);
3868
+ const screenPos = [pointer.position[0], pointer.position[1]];
3869
+ const worldPos = Camera.toWorld(ctx, screenPos);
3870
+ return {
3871
+ type,
3872
+ ctx,
3873
+ screenPosition: screenPos,
3874
+ worldPosition: worldPos,
3875
+ velocity: [pointer._velocity[0], pointer._velocity[1]],
3876
+ pressure: pointer.pressure,
3877
+ button: pointer.button,
3878
+ pointerType: pointer.pointerType,
3879
+ obscured: pointer.obscured,
3880
+ shiftDown: modifiers.shiftDown,
3881
+ altDown: modifiers.altDown,
3882
+ modDown: modifiers.modDown,
3883
+ cameraLeft: camera.left,
3884
+ cameraTop: camera.top,
3885
+ cameraZoom: camera.zoom,
3886
+ pointerId: entityId,
3887
+ intersects
3888
+ };
3889
+ };
3890
+ const addedPointers = pointerQuery3.added(ctx);
3891
+ const removedPointers = pointerQuery3.removed(ctx);
3892
+ const changedPointers = pointerQuery3.changed(ctx);
3893
+ const currentPointers = pointerQuery3.current(ctx);
3894
+ const matchingAdded = addedPointers.filter(matchesButtons);
3895
+ const matchingRemoved = removedPointers.filter(matchesButtons);
3896
+ const matchingChanged = changedPointers.filter(matchesButtons);
3897
+ const matchingCurrent = Array.from(currentPointers).filter(matchesButtons);
3898
+ const escapePressed = Keyboard.isKeyDownTrigger(ctx, Key.Escape);
3899
+ const multiTouch = matchingCurrent.length > 1 && matchingAdded.length > 0;
3900
+ if ((escapePressed || multiTouch) && matchingCurrent.length > 0) {
3901
+ events.push(createEvent("cancel", matchingCurrent[0]));
3902
+ return events;
3903
+ }
3904
+ if (matchingAdded.length > 0 && matchingCurrent.length === 1) {
3905
+ const entityId = matchingCurrent[0];
3906
+ const pointer = Pointer.read(ctx, entityId);
3907
+ if (!pointer.obscured) {
3908
+ events.push(createEvent("pointerDown", entityId));
3909
+ state.prevPositions.set(entityId, [pointer.position[0], pointer.position[1]]);
3910
+ }
3911
+ }
3912
+ if (matchingRemoved.length > 0 && matchingCurrent.length === 0) {
3913
+ const entityId = matchingRemoved[0];
3914
+ const pointer = Pointer.read(ctx, entityId);
3915
+ events.push(createEvent("pointerUp", entityId));
3916
+ const downPos = state.prevPositions.get(entityId);
3917
+ if (downPos) {
3918
+ const currentPos = [pointer.position[0], pointer.position[1]];
3919
+ const dist = Vec23.distance(downPos, currentPos);
3920
+ const deltaFrame = frameNumber - pointer.downFrame;
3921
+ if (dist < clickMoveThreshold && deltaFrame < clickFrameThreshold) {
3922
+ events.push(createEvent("click", entityId));
3923
+ }
3924
+ state.prevPositions.delete(entityId);
3925
+ }
3926
+ }
3927
+ const modifiersChanged = modifiers.shiftDown !== state.prevModifiers.shiftDown || modifiers.altDown !== state.prevModifiers.altDown || modifiers.modDown !== state.prevModifiers.modDown;
3928
+ const cameraChanged = camera.left !== state.prevCamera.left || camera.top !== state.prevCamera.top || camera.zoom !== state.prevCamera.zoom;
3929
+ if ((matchingChanged.length > 0 || modifiersChanged || cameraChanged) && matchingCurrent.length === 1) {
3930
+ events.push(createEvent("pointerMove", matchingCurrent[0]));
3931
+ }
3932
+ if (includeFrameEvent && matchingCurrent.length === 1) {
3933
+ events.push(createEvent("frame", matchingCurrent[0]));
3934
+ }
3935
+ state.prevModifiers = { ...modifiers };
3936
+ state.prevCamera = { left: camera.left, top: camera.top, zoom: camera.zoom };
3937
+ return events;
3938
+ }
3939
+ function clearPointerTrackingState(ctx) {
3940
+ trackingState.delete(ctx.readerId);
3941
+ }
3942
+ export {
3943
+ Aabb,
3944
+ Asset,
3945
+ Block,
3946
+ BlockDef2 as BlockDef,
3947
+ Camera,
3948
+ CanvasComponentDef7 as CanvasComponentDef,
3949
+ CanvasSingletonDef12 as CanvasSingletonDef,
3950
+ Color,
3951
+ CommandMarker,
3952
+ ComponentDef,
3953
+ Connector,
3954
+ Controls,
3955
+ ControlsOptions,
3956
+ CorePlugin,
3957
+ Cursor,
3958
+ CursorDef,
3959
+ Edited,
3960
+ Editor,
3961
+ EditorStateDef,
3962
+ EventType,
3963
+ FontFamily,
3964
+ FontLoader,
3965
+ Frame,
3966
+ Grid,
3967
+ GridOptions,
3968
+ Held,
3969
+ HitGeometry,
3970
+ Hovered,
3971
+ Image,
3972
+ Intersect,
3973
+ Key,
3974
+ Keybind,
3975
+ Keyboard,
3976
+ MAX_HIT_ARCS,
3977
+ MAX_HIT_CAPSULES,
3978
+ MainThreadSystem2 as MainThreadSystem,
3979
+ Mouse,
3980
+ Opacity,
3981
+ Pointer,
3982
+ PointerButton,
3983
+ PointerType,
3984
+ RankBounds,
3985
+ Redo,
3986
+ SINGLETON_ENTITY_ID,
3987
+ STRATUM_ORDER,
3988
+ ScaleWithZoom,
3989
+ ScaleWithZoomState,
3990
+ Screen,
3991
+ Shape,
3992
+ Stratum,
3993
+ StrokeKind,
3994
+ Synced4 as Synced,
3995
+ Text,
3996
+ TextAlignment,
3997
+ Undo,
3998
+ UploadState,
3999
+ User,
4000
+ UserData,
4001
+ VerticalAlign,
4002
+ VerticalAlignment,
4003
+ addComponent6 as addComponent,
4004
+ canBlockEdit,
4005
+ clearPointerTrackingState,
4006
+ createEntity5 as createEntity,
4007
+ defineCanvasComponent13 as defineCanvasComponent,
4008
+ defineCanvasSingleton3 as defineCanvasSingleton,
4009
+ defineCommand,
4010
+ defineEditorState,
4011
+ defineEditorSystem,
4012
+ defineQuery11 as defineQuery,
4013
+ defineSystem,
4014
+ field28 as field,
4015
+ getBackrefs,
4016
+ getBlockDef,
4017
+ getFrameInput,
4018
+ getKeyboardInput,
4019
+ getMouseInput,
4020
+ getPluginResources,
4021
+ getPointerInput,
4022
+ getResources12 as getResources,
4023
+ getTopmostBlockAtPoint,
4024
+ hasComponent7 as hasComponent,
4025
+ intersectAabb,
4026
+ intersectCapsule,
4027
+ intersectPoint,
4028
+ isAlive,
4029
+ isHeldByRemote,
4030
+ on,
4031
+ parsePlugin,
4032
+ removeComponent2 as removeComponent,
4033
+ removeEntity3 as removeEntity,
4034
+ runMachine
4035
+ };
4036
+ //# sourceMappingURL=index.js.map