@jamesyong42/infinite-canvas 0.0.1

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.
@@ -0,0 +1,1578 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/advanced.ts
31
+ var advanced_exports = {};
32
+ __export(advanced_exports, {
33
+ ContainerRefProvider: () => ContainerRefProvider,
34
+ DEFAULT_GRID_CONFIG: () => DEFAULT_GRID_CONFIG,
35
+ DEFAULT_SELECTION_CONFIG: () => DEFAULT_SELECTION_CONFIG,
36
+ EngineProvider: () => EngineProvider,
37
+ GridRenderer: () => GridRenderer,
38
+ Profiler: () => Profiler,
39
+ SelectionFrame: () => SelectionFrame,
40
+ SelectionOverlaySlot: () => SelectionOverlaySlot,
41
+ SelectionRenderer: () => SelectionRenderer,
42
+ SpatialIndex: () => SpatialIndex,
43
+ WebGLWidgetLayer: () => WebGLWidgetLayer,
44
+ WebGLWidgetSlot: () => WebGLWidgetSlot,
45
+ WidgetSlot: () => WidgetSlot,
46
+ computeSnapGuides: () => computeSnapGuides,
47
+ deserializeWorld: () => deserializeWorld,
48
+ serializeEntities: () => serializeEntities,
49
+ serializeWorld: () => serializeWorld
50
+ });
51
+ module.exports = __toCommonJS(advanced_exports);
52
+
53
+ // src/react/webgl/GridRenderer.ts
54
+ var THREE = __toESM(require("three"), 1);
55
+ var DEFAULT_GRID_CONFIG = {
56
+ spacings: [8, 64, 512],
57
+ dotColor: [0, 0, 0],
58
+ dotAlpha: 0.18,
59
+ fadeIn: [4, 12],
60
+ fadeOut: [250, 500],
61
+ dotRadius: [0.5, 1.4],
62
+ levelWeight: [1, 0.4]
63
+ };
64
+ var vertexShader = (
65
+ /* glsl */
66
+ `
67
+ void main() {
68
+ gl_Position = vec4(position.xy, 0.0, 1.0);
69
+ }
70
+ `
71
+ );
72
+ var fragmentShader = (
73
+ /* glsl */
74
+ `
75
+ precision highp float;
76
+
77
+ uniform vec2 u_resolution; // device pixels
78
+ uniform vec2 u_camera; // world-space top-left
79
+ uniform float u_zoom; // CSS zoom
80
+ uniform float u_dpr; // device pixel ratio
81
+ uniform vec3 u_spacings; // world-unit grid spacings
82
+ uniform vec3 u_dotColor; // dot RGB
83
+ uniform float u_dotAlpha; // dot base alpha
84
+ uniform vec2 u_fadeIn; // CSS-px [start, end]
85
+ uniform vec2 u_fadeOut; // CSS-px [start, end]
86
+ uniform vec2 u_dotRadius; // CSS-px [min, max]
87
+ uniform vec2 u_levelWeight; // [base, step]
88
+
89
+ void main() {
90
+ vec2 devicePos = gl_FragCoord.xy;
91
+ devicePos.y = u_resolution.y - devicePos.y;
92
+
93
+ float effectiveZoom = u_zoom * u_dpr;
94
+ vec2 worldPos = devicePos / effectiveZoom + u_camera;
95
+
96
+ float totalAlpha = 0.0;
97
+
98
+ for (int i = 0; i < 3; i++) {
99
+ float spacing;
100
+ if (i == 0) spacing = u_spacings.x;
101
+ else if (i == 1) spacing = u_spacings.y;
102
+ else spacing = u_spacings.z;
103
+
104
+ // Screen spacing in CSS pixels (DPR-independent for consistent fading)
105
+ float cssSpacing = spacing * u_zoom;
106
+
107
+ // Fade curve
108
+ float opacity = 0.0;
109
+ if (cssSpacing >= u_fadeIn.x && cssSpacing < u_fadeIn.y) {
110
+ opacity = (cssSpacing - u_fadeIn.x) / (u_fadeIn.y - u_fadeIn.x);
111
+ } else if (cssSpacing >= u_fadeIn.y && cssSpacing < u_fadeOut.x) {
112
+ opacity = 1.0;
113
+ } else if (cssSpacing >= u_fadeOut.x && cssSpacing < u_fadeOut.y) {
114
+ opacity = 1.0 - (cssSpacing - u_fadeOut.x) / (u_fadeOut.y - u_fadeOut.x);
115
+ }
116
+ if (opacity <= 0.001) continue;
117
+
118
+ // Distance to nearest grid intersection in device pixels
119
+ vec2 f = fract(worldPos / spacing + 0.5) - 0.5;
120
+ float dist = length(f) * spacing * effectiveZoom;
121
+
122
+ // Dot radius in device pixels \u2014 grows as grid becomes sparser
123
+ float t = clamp((cssSpacing - u_fadeIn.x) / 40.0, 0.0, 1.0);
124
+ float radius = mix(u_dotRadius.x, u_dotRadius.y, t) * u_dpr;
125
+
126
+ // Anti-aliased dot (0.5 device pixel smoothstep)
127
+ float dot = 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist);
128
+
129
+ // Larger grid levels get progressively stronger dots
130
+ float weight = u_levelWeight.x + float(i) * u_levelWeight.y;
131
+ totalAlpha += dot * opacity * weight;
132
+ }
133
+
134
+ gl_FragColor = vec4(u_dotColor, clamp(totalAlpha * u_dotAlpha, 0.0, 1.0));
135
+ }
136
+ `
137
+ );
138
+ var GridRenderer = class {
139
+ renderer;
140
+ scene;
141
+ camera;
142
+ material;
143
+ mesh;
144
+ constructor(canvas) {
145
+ this.renderer = new THREE.WebGLRenderer({
146
+ canvas,
147
+ alpha: true,
148
+ antialias: false,
149
+ premultipliedAlpha: false
150
+ });
151
+ this.renderer.setClearColor(0, 0);
152
+ this.scene = new THREE.Scene();
153
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
154
+ this.material = new THREE.ShaderMaterial({
155
+ vertexShader,
156
+ fragmentShader,
157
+ uniforms: {
158
+ u_resolution: { value: new THREE.Vector2(1, 1) },
159
+ u_camera: { value: new THREE.Vector2(0, 0) },
160
+ u_zoom: { value: 1 },
161
+ u_dpr: { value: 1 },
162
+ u_spacings: { value: new THREE.Vector3(8, 64, 512) },
163
+ u_dotColor: { value: new THREE.Vector3(0, 0, 0) },
164
+ u_dotAlpha: { value: 0.18 },
165
+ u_fadeIn: { value: new THREE.Vector2(4, 12) },
166
+ u_fadeOut: { value: new THREE.Vector2(250, 500) },
167
+ u_dotRadius: { value: new THREE.Vector2(0.5, 1.4) },
168
+ u_levelWeight: { value: new THREE.Vector2(1, 0.4) }
169
+ },
170
+ transparent: true,
171
+ depthTest: false,
172
+ depthWrite: false
173
+ });
174
+ const geometry = new THREE.BufferGeometry();
175
+ const vertices = new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]);
176
+ geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
177
+ this.mesh = new THREE.Mesh(geometry, this.material);
178
+ this.scene.add(this.mesh);
179
+ }
180
+ /** Apply a (partial) grid config. Only provided fields are updated. */
181
+ setConfig(config) {
182
+ const u = this.material.uniforms;
183
+ if (config.spacings) u.u_spacings.value.set(...config.spacings);
184
+ if (config.dotColor) u.u_dotColor.value.set(...config.dotColor);
185
+ if (config.dotAlpha !== void 0) u.u_dotAlpha.value = config.dotAlpha;
186
+ if (config.fadeIn) u.u_fadeIn.value.set(...config.fadeIn);
187
+ if (config.fadeOut) u.u_fadeOut.value.set(...config.fadeOut);
188
+ if (config.dotRadius) u.u_dotRadius.value.set(...config.dotRadius);
189
+ if (config.levelWeight) u.u_levelWeight.value.set(...config.levelWeight);
190
+ }
191
+ setSize(width, height, dpr = 1) {
192
+ this.renderer.setSize(width, height, false);
193
+ this.renderer.setPixelRatio(dpr);
194
+ const u = this.material.uniforms;
195
+ u.u_resolution.value.set(width * dpr, height * dpr);
196
+ u.u_dpr.value = dpr;
197
+ }
198
+ render(cameraX, cameraY, zoom) {
199
+ const u = this.material.uniforms;
200
+ u.u_camera.value.set(cameraX, cameraY);
201
+ u.u_zoom.value = zoom;
202
+ this.renderer.render(this.scene, this.camera);
203
+ }
204
+ dispose() {
205
+ this.mesh.geometry.dispose();
206
+ this.material.dispose();
207
+ this.renderer.dispose();
208
+ }
209
+ /** Expose for future WebGL widget rendering */
210
+ getWebGLRenderer() {
211
+ return this.renderer;
212
+ }
213
+ };
214
+
215
+ // src/react/webgl/SelectionRenderer.ts
216
+ var THREE2 = __toESM(require("three"), 1);
217
+ var DEFAULT_SELECTION_CONFIG = {
218
+ outlineColor: [0.051, 0.6, 1],
219
+ // #0d99ff (Figma blue)
220
+ outlineWidth: 1.5,
221
+ hoverColor: [0.051, 0.6, 1],
222
+ hoverWidth: 1,
223
+ handleSize: 8,
224
+ handleFill: [1, 1, 1],
225
+ handleBorder: [0.051, 0.6, 1],
226
+ handleBorderWidth: 1.5,
227
+ groupDash: 4
228
+ };
229
+ var MAX_ENTITIES = 32;
230
+ var vertexShader2 = (
231
+ /* glsl */
232
+ `
233
+ void main() {
234
+ gl_Position = vec4(position.xy, 0.0, 1.0);
235
+ }
236
+ `
237
+ );
238
+ var fragmentShader2 = (
239
+ /* glsl */
240
+ `
241
+ precision highp float;
242
+
243
+ uniform vec2 u_resolution;
244
+ uniform vec2 u_camera;
245
+ uniform float u_zoom;
246
+ uniform float u_dpr;
247
+
248
+ // Selection data
249
+ uniform int u_count;
250
+ uniform vec4 u_bounds[${MAX_ENTITIES}]; // (worldX, worldY, width, height)
251
+ uniform int u_hoverIdx; // -1 = none
252
+ uniform vec4 u_groupBounds; // group bbox (0 if count <= 1)
253
+ uniform int u_hasGroup;
254
+
255
+ // Snap guides
256
+ uniform int u_guideCount;
257
+ uniform vec4 u_guides[16]; // (axis: 0=x/1=y, position, 0, 0)
258
+ uniform int u_spacingCount;
259
+ uniform vec4 u_spacings[8]; // equal-spacing segments: (axis, from, to, perpPos)
260
+ uniform vec3 u_guideColor;
261
+
262
+ // Style
263
+ uniform vec3 u_outlineColor;
264
+ uniform float u_outlineWidth;
265
+ uniform vec3 u_hoverColor;
266
+ uniform float u_hoverWidth;
267
+ uniform float u_handleSize;
268
+ uniform vec3 u_handleFill;
269
+ uniform vec3 u_handleBorder;
270
+ uniform float u_handleBorderWidth;
271
+ uniform float u_groupDash;
272
+
273
+ // SDF for axis-aligned rectangle outline (returns distance to edge)
274
+ float sdRectOutline(vec2 p, vec2 center, vec2 halfSize) {
275
+ vec2 d = abs(p - center) - halfSize;
276
+ float outside = length(max(d, 0.0));
277
+ float inside = min(max(d.x, d.y), 0.0);
278
+ return abs(outside + inside);
279
+ }
280
+
281
+ // SDF for filled square
282
+ float sdSquare(vec2 p, vec2 center, float halfSize) {
283
+ vec2 d = abs(p - center) - vec2(halfSize);
284
+ return max(d.x, d.y);
285
+ }
286
+
287
+ void main() {
288
+ if (u_count == 0 && u_hoverIdx < 0) discard;
289
+
290
+ vec2 devicePos = gl_FragCoord.xy;
291
+ devicePos.y = u_resolution.y - devicePos.y;
292
+
293
+ float effectiveZoom = u_zoom * u_dpr;
294
+ vec2 worldPos = devicePos / effectiveZoom + u_camera;
295
+
296
+ // Screen-space conversion factor
297
+ float pxToWorld = 1.0 / effectiveZoom;
298
+
299
+ vec4 color = vec4(0.0);
300
+
301
+ // --- Hover outline ---
302
+ if (u_hoverIdx >= 0 && u_hoverIdx < ${MAX_ENTITIES}) {
303
+ vec4 b = u_bounds[u_hoverIdx];
304
+ vec2 center = vec2(b.x + b.z * 0.5, b.y + b.w * 0.5);
305
+ vec2 halfSize = vec2(b.z, b.w) * 0.5;
306
+ float dist = sdRectOutline(worldPos, center, halfSize);
307
+ float width = u_hoverWidth * pxToWorld;
308
+ float alpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
309
+ color = max(color, vec4(u_hoverColor, alpha * 0.6));
310
+ }
311
+
312
+ // --- Selection outlines ---
313
+ for (int i = 0; i < ${MAX_ENTITIES}; i++) {
314
+ if (i >= u_count) break;
315
+ vec4 b = u_bounds[i];
316
+ vec2 center = vec2(b.x + b.z * 0.5, b.y + b.w * 0.5);
317
+ vec2 halfSize = vec2(b.z, b.w) * 0.5;
318
+
319
+ // Outline
320
+ float dist = sdRectOutline(worldPos, center, halfSize);
321
+ float width = u_outlineWidth * pxToWorld;
322
+ float outlineAlpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
323
+ color = max(color, vec4(u_outlineColor, outlineAlpha));
324
+
325
+ // 8 resize handles
326
+ float hs = u_handleSize * 0.5 * pxToWorld;
327
+ float bw = u_handleBorderWidth * pxToWorld;
328
+ vec2 corners[8];
329
+ corners[0] = vec2(b.x, b.y); // nw
330
+ corners[1] = vec2(b.x + b.z * 0.5, b.y); // n
331
+ corners[2] = vec2(b.x + b.z, b.y); // ne
332
+ corners[3] = vec2(b.x + b.z, b.y + b.w * 0.5); // e
333
+ corners[4] = vec2(b.x + b.z, b.y + b.w); // se
334
+ corners[5] = vec2(b.x + b.z * 0.5, b.y + b.w); // s
335
+ corners[6] = vec2(b.x, b.y + b.w); // sw
336
+ corners[7] = vec2(b.x, b.y + b.w * 0.5); // w
337
+
338
+ for (int h = 0; h < 8; h++) {
339
+ float d = sdSquare(worldPos, corners[h], hs);
340
+ // Fill (white)
341
+ float fillAlpha = 1.0 - smoothstep(-pxToWorld * 0.5, pxToWorld * 0.5, d);
342
+ // Border
343
+ float borderDist = abs(d + bw * 0.5) - bw * 0.5;
344
+ float borderAlpha = 1.0 - smoothstep(-pxToWorld * 0.5, pxToWorld * 0.5, borderDist);
345
+
346
+ if (fillAlpha > 0.01) {
347
+ // Composite: border color on top of fill
348
+ vec3 handleColor = mix(u_handleFill, u_handleBorder, borderAlpha);
349
+ color = vec4(handleColor, max(fillAlpha, color.a));
350
+ }
351
+ }
352
+ }
353
+
354
+ // --- Group bounding box (dashed) ---
355
+ if (u_hasGroup == 1 && u_count > 1) {
356
+ vec4 gb = u_groupBounds;
357
+ vec2 center = vec2(gb.x + gb.z * 0.5, gb.y + gb.w * 0.5);
358
+ vec2 halfSize = vec2(gb.z, gb.w) * 0.5;
359
+ float dist = sdRectOutline(worldPos, center, halfSize);
360
+ float width = u_outlineWidth * 0.75 * pxToWorld;
361
+ float lineAlpha = 1.0 - smoothstep(width - pxToWorld * 0.5, width + pxToWorld * 0.5, dist);
362
+
363
+ // Dash pattern along the rectangle perimeter
364
+ if (u_groupDash > 0.0 && lineAlpha > 0.01) {
365
+ vec2 rel = worldPos - vec2(gb.x, gb.y);
366
+ float perim;
367
+ // Approximate perimeter position for dash
368
+ if (abs(rel.y) < width || abs(rel.y - gb.w) < width) {
369
+ perim = rel.x;
370
+ } else {
371
+ perim = rel.y;
372
+ }
373
+ float dashWorld = u_groupDash * pxToWorld;
374
+ float dashPattern = step(0.5, fract(perim / (dashWorld * 2.0)));
375
+ lineAlpha *= dashPattern;
376
+ }
377
+
378
+ color = max(color, vec4(u_outlineColor, lineAlpha * 0.5));
379
+ }
380
+
381
+ // --- Snap guide lines ---
382
+ for (int i = 0; i < 16; i++) {
383
+ if (i >= u_guideCount) break;
384
+ vec4 g = u_guides[i];
385
+ float guideWidth = 0.5 * pxToWorld;
386
+ float dist;
387
+ if (g.x < 0.5) {
388
+ // Vertical line (x-axis alignment)
389
+ dist = abs(worldPos.x - g.y);
390
+ } else {
391
+ // Horizontal line (y-axis alignment)
392
+ dist = abs(worldPos.y - g.y);
393
+ }
394
+ float guideAlpha = 1.0 - smoothstep(guideWidth - pxToWorld * 0.3, guideWidth + pxToWorld * 0.3, dist);
395
+ color = max(color, vec4(u_guideColor, guideAlpha * 0.8));
396
+ }
397
+
398
+ // --- Equal spacing indicators ---
399
+ for (int i = 0; i < 8; i++) {
400
+ if (i >= u_spacingCount) break;
401
+ vec4 s = u_spacings[i];
402
+ float lineWidth = 0.5 * pxToWorld;
403
+ float segAlpha = 0.0;
404
+ if (s.x < 0.5) {
405
+ // Horizontal segment (x-axis gap)
406
+ float yDist = abs(worldPos.y - s.w);
407
+ float xInRange = step(s.y, worldPos.x) * step(worldPos.x, s.z);
408
+ // Center line
409
+ segAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, yDist)) * xInRange;
410
+ // End bars (perpendicular marks at from and to)
411
+ float barHeight = 4.0 * pxToWorld;
412
+ float barFromDist = abs(worldPos.x - s.y);
413
+ float barFromAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, barFromDist))
414
+ * (1.0 - smoothstep(barHeight, barHeight + pxToWorld, abs(worldPos.y - s.w)));
415
+ float barToDist = abs(worldPos.x - s.z);
416
+ float barToAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, barToDist))
417
+ * (1.0 - smoothstep(barHeight, barHeight + pxToWorld, abs(worldPos.y - s.w)));
418
+ segAlpha = max(segAlpha, max(barFromAlpha, barToAlpha));
419
+ } else {
420
+ // Vertical segment (y-axis gap)
421
+ float xDist = abs(worldPos.x - s.w);
422
+ float yInRange = step(s.y, worldPos.y) * step(worldPos.y, s.z);
423
+ segAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, xDist)) * yInRange;
424
+ float barWidth = 4.0 * pxToWorld;
425
+ float barFromAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, abs(worldPos.y - s.y)))
426
+ * (1.0 - smoothstep(barWidth, barWidth + pxToWorld, abs(worldPos.x - s.w)));
427
+ float barToAlpha = (1.0 - smoothstep(lineWidth, lineWidth + pxToWorld, abs(worldPos.y - s.z)))
428
+ * (1.0 - smoothstep(barWidth, barWidth + pxToWorld, abs(worldPos.x - s.w)));
429
+ segAlpha = max(segAlpha, max(barFromAlpha, barToAlpha));
430
+ }
431
+ color = max(color, vec4(u_guideColor, segAlpha * 0.7));
432
+ }
433
+
434
+ if (color.a < 0.01) discard;
435
+ gl_FragColor = color;
436
+ }
437
+ `
438
+ );
439
+ var SelectionRenderer = class {
440
+ material;
441
+ mesh;
442
+ scene;
443
+ camera;
444
+ constructor() {
445
+ this.scene = new THREE2.Scene();
446
+ this.camera = new THREE2.OrthographicCamera(-1, 1, 1, -1, 0, 1);
447
+ const boundsDefault = [];
448
+ for (let i = 0; i < MAX_ENTITIES; i++) {
449
+ boundsDefault.push(new THREE2.Vector4(0, 0, 0, 0));
450
+ }
451
+ this.material = new THREE2.ShaderMaterial({
452
+ vertexShader: vertexShader2,
453
+ fragmentShader: fragmentShader2,
454
+ uniforms: {
455
+ u_resolution: { value: new THREE2.Vector2(1, 1) },
456
+ u_camera: { value: new THREE2.Vector2(0, 0) },
457
+ u_zoom: { value: 1 },
458
+ u_dpr: { value: 1 },
459
+ u_count: { value: 0 },
460
+ u_bounds: { value: boundsDefault },
461
+ u_hoverIdx: { value: -1 },
462
+ u_groupBounds: { value: new THREE2.Vector4(0, 0, 0, 0) },
463
+ u_hasGroup: { value: 0 },
464
+ // Style (Figma defaults)
465
+ u_outlineColor: { value: new THREE2.Vector3(...DEFAULT_SELECTION_CONFIG.outlineColor) },
466
+ u_outlineWidth: { value: DEFAULT_SELECTION_CONFIG.outlineWidth },
467
+ u_hoverColor: { value: new THREE2.Vector3(...DEFAULT_SELECTION_CONFIG.hoverColor) },
468
+ u_hoverWidth: { value: DEFAULT_SELECTION_CONFIG.hoverWidth },
469
+ u_handleSize: { value: DEFAULT_SELECTION_CONFIG.handleSize },
470
+ u_handleFill: { value: new THREE2.Vector3(...DEFAULT_SELECTION_CONFIG.handleFill) },
471
+ u_handleBorder: { value: new THREE2.Vector3(...DEFAULT_SELECTION_CONFIG.handleBorder) },
472
+ u_handleBorderWidth: { value: DEFAULT_SELECTION_CONFIG.handleBorderWidth },
473
+ u_groupDash: { value: DEFAULT_SELECTION_CONFIG.groupDash },
474
+ // Snap guides
475
+ u_guideCount: { value: 0 },
476
+ u_guides: { value: Array.from({ length: 16 }, () => new THREE2.Vector4(0, 0, 0, 0)) },
477
+ u_spacingCount: { value: 0 },
478
+ u_spacings: { value: Array.from({ length: 8 }, () => new THREE2.Vector4(0, 0, 0, 0)) },
479
+ u_guideColor: { value: new THREE2.Vector3(1, 0, 0.55) }
480
+ // magenta/pink
481
+ },
482
+ transparent: true,
483
+ depthTest: false,
484
+ depthWrite: false
485
+ });
486
+ const geometry = new THREE2.BufferGeometry();
487
+ const vertices = new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]);
488
+ geometry.setAttribute("position", new THREE2.BufferAttribute(vertices, 3));
489
+ this.mesh = new THREE2.Mesh(geometry, this.material);
490
+ this.scene.add(this.mesh);
491
+ }
492
+ setConfig(config) {
493
+ const u = this.material.uniforms;
494
+ if (config.outlineColor) u.u_outlineColor.value.set(...config.outlineColor);
495
+ if (config.outlineWidth !== void 0) u.u_outlineWidth.value = config.outlineWidth;
496
+ if (config.hoverColor) u.u_hoverColor.value.set(...config.hoverColor);
497
+ if (config.hoverWidth !== void 0) u.u_hoverWidth.value = config.hoverWidth;
498
+ if (config.handleSize !== void 0) u.u_handleSize.value = config.handleSize;
499
+ if (config.handleFill) u.u_handleFill.value.set(...config.handleFill);
500
+ if (config.handleBorder) u.u_handleBorder.value.set(...config.handleBorder);
501
+ if (config.handleBorderWidth !== void 0)
502
+ u.u_handleBorderWidth.value = config.handleBorderWidth;
503
+ if (config.groupDash !== void 0) u.u_groupDash.value = config.groupDash;
504
+ }
505
+ setSize(resolution, dpr) {
506
+ this.material.uniforms.u_resolution.value.copy(resolution);
507
+ this.material.uniforms.u_dpr.value = dpr;
508
+ }
509
+ render(renderer, cameraX, cameraY, zoom, selected, hovered, guides = [], spacings = []) {
510
+ const u = this.material.uniforms;
511
+ u.u_camera.value.set(cameraX, cameraY);
512
+ u.u_zoom.value = zoom;
513
+ const count = Math.min(selected.length, MAX_ENTITIES);
514
+ u.u_count.value = count;
515
+ for (let i = 0; i < count; i++) {
516
+ const b = selected[i];
517
+ u.u_bounds.value[i].set(b.x, b.y, b.width, b.height);
518
+ }
519
+ if (hovered && count < MAX_ENTITIES) {
520
+ let hoverIdx = -1;
521
+ for (let i = 0; i < count; i++) {
522
+ const b = selected[i];
523
+ if (b.x === hovered.x && b.y === hovered.y) {
524
+ hoverIdx = i;
525
+ break;
526
+ }
527
+ }
528
+ if (hoverIdx < 0) {
529
+ u.u_bounds.value[count].set(hovered.x, hovered.y, hovered.width, hovered.height);
530
+ u.u_hoverIdx.value = count;
531
+ } else {
532
+ u.u_hoverIdx.value = -1;
533
+ }
534
+ } else {
535
+ u.u_hoverIdx.value = -1;
536
+ }
537
+ if (count > 1) {
538
+ let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY;
539
+ for (let i = 0; i < count; i++) {
540
+ const b = selected[i];
541
+ minX = Math.min(minX, b.x);
542
+ minY = Math.min(minY, b.y);
543
+ maxX = Math.max(maxX, b.x + b.width);
544
+ maxY = Math.max(maxY, b.y + b.height);
545
+ }
546
+ u.u_groupBounds.value.set(minX, minY, maxX - minX, maxY - minY);
547
+ u.u_hasGroup.value = 1;
548
+ } else {
549
+ u.u_hasGroup.value = 0;
550
+ }
551
+ const gCount = Math.min(guides.length, 16);
552
+ u.u_guideCount.value = gCount;
553
+ for (let i = 0; i < gCount; i++) {
554
+ const g = guides[i];
555
+ u.u_guides.value[i].set(g.axis === "x" ? 0 : 1, g.position, 0, 0);
556
+ }
557
+ let sIdx = 0;
558
+ for (const sp of spacings) {
559
+ for (const seg of sp.segments) {
560
+ if (sIdx >= 8) break;
561
+ u.u_spacings.value[sIdx].set(sp.axis === "x" ? 0 : 1, seg.from, seg.to, sp.perpPosition);
562
+ sIdx++;
563
+ }
564
+ }
565
+ u.u_spacingCount.value = sIdx;
566
+ const prevAutoClear = renderer.autoClear;
567
+ renderer.autoClear = false;
568
+ renderer.render(this.scene, this.camera);
569
+ renderer.autoClear = prevAutoClear;
570
+ }
571
+ dispose() {
572
+ this.mesh.geometry.dispose();
573
+ this.material.dispose();
574
+ }
575
+ };
576
+
577
+ // src/react/webgl/WebGLWidgetLayer.tsx
578
+ var import_fiber2 = require("@react-three/fiber");
579
+ var import_react4 = require("react");
580
+ var THREE3 = __toESM(require("three"), 1);
581
+
582
+ // src/react/context.ts
583
+ var import_react = require("react");
584
+ var EngineContext = (0, import_react.createContext)(null);
585
+ var EngineProvider = EngineContext.Provider;
586
+ var ContainerRefContext = (0, import_react.createContext)(null);
587
+ var ContainerRefProvider = ContainerRefContext.Provider;
588
+ function useContainerRef() {
589
+ return (0, import_react.useContext)(ContainerRefContext);
590
+ }
591
+ function useLayoutEngine() {
592
+ const engine = (0, import_react.useContext)(EngineContext);
593
+ if (!engine) {
594
+ throw new Error("useLayoutEngine must be used within an <InfiniteCanvas>");
595
+ }
596
+ return engine;
597
+ }
598
+ var WidgetResolverContext = (0, import_react.createContext)(null);
599
+ var WidgetResolverProvider = WidgetResolverContext.Provider;
600
+ function useWidgetResolver() {
601
+ return (0, import_react.useContext)(WidgetResolverContext);
602
+ }
603
+
604
+ // src/react/webgl/WebGLWidgetSlot.tsx
605
+ var import_fiber = require("@react-three/fiber");
606
+ var import_react3 = require("react");
607
+
608
+ // src/ecs/define.ts
609
+ function defineComponent(name, defaults) {
610
+ return Object.freeze({ name, defaults, __kind: "component" });
611
+ }
612
+ function defineTag(name) {
613
+ return Object.freeze({ name, __kind: "tag" });
614
+ }
615
+
616
+ // src/components.ts
617
+ var Transform2D = defineComponent("Transform2D", {
618
+ x: 0,
619
+ y: 0,
620
+ width: 100,
621
+ height: 100,
622
+ rotation: 0
623
+ });
624
+ var WorldBounds = defineComponent("WorldBounds", {
625
+ worldX: 0,
626
+ worldY: 0,
627
+ worldWidth: 0,
628
+ worldHeight: 0
629
+ });
630
+ var ZIndex = defineComponent("ZIndex", { value: 0 });
631
+ var Parent = defineComponent("Parent", { id: 0 });
632
+ var Children = defineComponent("Children", { ids: [] });
633
+ var Widget = defineComponent("Widget", {
634
+ surface: "dom",
635
+ type: ""
636
+ });
637
+ var WidgetData = defineComponent("WidgetData", {
638
+ data: {}
639
+ });
640
+ var WidgetBreakpoint = defineComponent("WidgetBreakpoint", {
641
+ current: "normal",
642
+ screenWidth: 0,
643
+ screenHeight: 0
644
+ });
645
+ var Container = defineComponent("Container", { enterable: true });
646
+ var Selectable = defineTag("Selectable");
647
+ var Draggable = defineTag("Draggable");
648
+ var Resizable = defineTag("Resizable");
649
+ var Locked = defineTag("Locked");
650
+ var Selected = defineTag("Selected");
651
+ var Active = defineTag("Active");
652
+ var Visible = defineTag("Visible");
653
+
654
+ // src/react/hooks.ts
655
+ var import_react2 = require("react");
656
+ function useComponent(entity, type) {
657
+ const engine = useLayoutEngine();
658
+ const [value, setValue] = (0, import_react2.useState)(() => engine.get(entity, type));
659
+ (0, import_react2.useEffect)(() => {
660
+ setValue(engine.get(entity, type));
661
+ const unsub = engine.world.onComponentChanged(
662
+ type,
663
+ (_id, _prev, next) => {
664
+ setValue({ ...next });
665
+ },
666
+ entity
667
+ );
668
+ return unsub;
669
+ }, [engine, entity, type]);
670
+ return value;
671
+ }
672
+
673
+ // src/react/webgl/WebGLWidgetSlot.tsx
674
+ var import_jsx_runtime = require("react/jsx-runtime");
675
+ function WebGLWidgetSlot({ entityId, component: WidgetComponent }) {
676
+ const groupRef = (0, import_react3.useRef)(null);
677
+ const engine = useLayoutEngine();
678
+ const wb = useComponent(entityId, WorldBounds);
679
+ (0, import_fiber.useFrame)(() => {
680
+ if (!groupRef.current) return;
681
+ const bounds = engine.get(entityId, WorldBounds);
682
+ if (!bounds) return;
683
+ groupRef.current.position.set(
684
+ bounds.worldX + bounds.worldWidth / 2,
685
+ -(bounds.worldY + bounds.worldHeight / 2),
686
+ 0
687
+ );
688
+ });
689
+ if (!wb) return null;
690
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("group", { ref: groupRef, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WidgetComponent, { entityId, width: wb.worldWidth, height: wb.worldHeight }) });
691
+ }
692
+
693
+ // src/react/webgl/WebGLWidgetLayer.tsx
694
+ var import_jsx_runtime2 = require("react/jsx-runtime");
695
+ function CameraSync({ engine }) {
696
+ const { camera, size } = (0, import_fiber2.useThree)();
697
+ (0, import_fiber2.useFrame)(() => {
698
+ const cam = engine.getCamera();
699
+ const ortho = camera;
700
+ ortho.left = 0;
701
+ ortho.right = size.width / cam.zoom;
702
+ ortho.top = 0;
703
+ ortho.bottom = -(size.height / cam.zoom);
704
+ ortho.near = 0.1;
705
+ ortho.far = 1e4;
706
+ ortho.position.set(cam.x, -cam.y, 1e3);
707
+ ortho.updateProjectionMatrix();
708
+ });
709
+ return null;
710
+ }
711
+ function WebGLWidgetLayer({ engine, entities, resolve }) {
712
+ const canvasRef = (0, import_react4.useRef)(null);
713
+ const initialCamera = (0, import_react4.useMemo)(() => {
714
+ const cam = new THREE3.OrthographicCamera(0, 1, 0, -1, 0.1, 1e4);
715
+ cam.position.set(0, 0, 1e3);
716
+ return cam;
717
+ }, []);
718
+ const widgetEntries = (0, import_react4.useMemo)(() => {
719
+ const result = [];
720
+ for (const id of entities) {
721
+ const resolved = resolve(id);
722
+ if (resolved && resolved.surface === "webgl") {
723
+ result.push({
724
+ entityId: id,
725
+ component: resolved.component
726
+ });
727
+ }
728
+ }
729
+ return result;
730
+ }, [entities, resolve]);
731
+ if (widgetEntries.length === 0) return null;
732
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
733
+ import_fiber2.Canvas,
734
+ {
735
+ ref: canvasRef,
736
+ camera: initialCamera,
737
+ frameloop: "always",
738
+ gl: { alpha: true, antialias: true },
739
+ style: {
740
+ position: "absolute",
741
+ inset: 0,
742
+ pointerEvents: "none",
743
+ zIndex: 1
744
+ },
745
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(EngineProvider, { value: engine, children: [
746
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CameraSync, { engine }),
747
+ widgetEntries.map(({ entityId, component }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WebGLWidgetSlot, { entityId, component }, entityId))
748
+ ] })
749
+ }
750
+ );
751
+ }
752
+
753
+ // src/react/WidgetSlot.tsx
754
+ var import_react5 = require("react");
755
+ var import_jsx_runtime3 = require("react/jsx-runtime");
756
+ function getMods(e) {
757
+ return { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey };
758
+ }
759
+ var WidgetSlot = (0, import_react5.memo)(function WidgetSlot2({ entityId, slotRef }) {
760
+ const wrapperRef = (0, import_react5.useRef)(null);
761
+ const engine = useLayoutEngine();
762
+ const containerRefObj = useContainerRef();
763
+ const resolve = useWidgetResolver();
764
+ const widgetComp = useComponent(entityId, Widget);
765
+ const resolved = resolve?.(entityId, widgetComp?.type ?? "");
766
+ const WidgetComponent = resolved?.component ?? null;
767
+ (0, import_react5.useEffect)(() => {
768
+ slotRef(entityId, wrapperRef.current);
769
+ return () => slotRef(entityId, null);
770
+ }, [entityId, slotRef]);
771
+ const toLocal = (0, import_react5.useCallback)(
772
+ (e) => {
773
+ const rect = containerRefObj?.current?.getBoundingClientRect();
774
+ if (!rect) return { x: e.clientX, y: e.clientY };
775
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
776
+ },
777
+ [containerRefObj]
778
+ );
779
+ const onPointerDown = (0, import_react5.useCallback)(
780
+ (e) => {
781
+ const target = e.target;
782
+ if (target.closest("button, input, textarea, select, [contenteditable]")) {
783
+ e.stopPropagation();
784
+ return;
785
+ }
786
+ const { x, y } = toLocal(e);
787
+ const directive = engine.handlePointerDown(x, y, e.button, getMods(e));
788
+ e.stopPropagation();
789
+ if (directive.action === "capture-resize" || directive.action === "passthrough-track-drag") {
790
+ wrapperRef.current?.setPointerCapture(e.pointerId);
791
+ }
792
+ if (directive.action === "capture-resize") {
793
+ e.preventDefault();
794
+ }
795
+ },
796
+ [engine, toLocal]
797
+ );
798
+ const capturedRef = (0, import_react5.useRef)(false);
799
+ const onPointerMove = (0, import_react5.useCallback)(
800
+ (e) => {
801
+ const { x, y } = toLocal(e);
802
+ const directive = engine.handlePointerMove(x, y, getMods(e));
803
+ if (directive.action === "capture-drag" && !capturedRef.current) {
804
+ capturedRef.current = true;
805
+ e.stopPropagation();
806
+ }
807
+ },
808
+ [engine, toLocal]
809
+ );
810
+ const onPointerUp = (0, import_react5.useCallback)(
811
+ (e) => {
812
+ e.stopPropagation();
813
+ capturedRef.current = false;
814
+ if (wrapperRef.current?.hasPointerCapture(e.pointerId)) {
815
+ wrapperRef.current.releasePointerCapture(e.pointerId);
816
+ }
817
+ engine.handlePointerUp();
818
+ },
819
+ [engine]
820
+ );
821
+ const onDoubleClick = (0, import_react5.useCallback)(
822
+ (e) => {
823
+ e.stopPropagation();
824
+ engine.enterContainer(entityId);
825
+ },
826
+ [engine, entityId]
827
+ );
828
+ const wb = engine.get(entityId, WorldBounds);
829
+ const initialStyle = wb ? {
830
+ transform: `translate(${wb.worldX}px, ${wb.worldY}px)`,
831
+ width: `${wb.worldWidth}px`,
832
+ height: `${wb.worldHeight}px`
833
+ } : {};
834
+ const content = WidgetComponent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(WidgetComponent, { entityId }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "h-full w-full rounded border border-dashed border-gray-300 bg-gray-50" });
835
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
836
+ "div",
837
+ {
838
+ ref: wrapperRef,
839
+ "data-widget-slot": "",
840
+ className: "absolute left-0 top-0 origin-top-left will-change-transform",
841
+ style: initialStyle,
842
+ onPointerDown,
843
+ onPointerMove,
844
+ onPointerUp,
845
+ onDoubleClick,
846
+ children: content
847
+ }
848
+ );
849
+ });
850
+
851
+ // src/react/SelectionFrame.tsx
852
+ var import_react6 = require("react");
853
+ var import_jsx_runtime4 = require("react/jsx-runtime");
854
+ var SelectionFrame = (0, import_react6.memo)(function SelectionFrame2(_props) {
855
+ const handleClass = "absolute h-2.5 w-2.5 rounded-sm border-2 border-blue-500 bg-white";
856
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "pointer-events-none absolute inset-0", children: [
857
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 rounded border-2 border-blue-500" }),
858
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
859
+ "div",
860
+ {
861
+ className: `${handleClass} pointer-events-auto -left-1.5 -top-1.5 cursor-nw-resize`,
862
+ "data-handle": "nw"
863
+ }
864
+ ),
865
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
866
+ "div",
867
+ {
868
+ className: `${handleClass} pointer-events-auto -right-1.5 -top-1.5 cursor-ne-resize`,
869
+ "data-handle": "ne"
870
+ }
871
+ ),
872
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
873
+ "div",
874
+ {
875
+ className: `${handleClass} pointer-events-auto -bottom-1.5 -left-1.5 cursor-sw-resize`,
876
+ "data-handle": "sw"
877
+ }
878
+ ),
879
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
880
+ "div",
881
+ {
882
+ className: `${handleClass} pointer-events-auto -bottom-1.5 -right-1.5 cursor-se-resize`,
883
+ "data-handle": "se"
884
+ }
885
+ ),
886
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
887
+ "div",
888
+ {
889
+ className: `${handleClass} pointer-events-auto -top-1.5 left-1/2 -translate-x-1/2 cursor-n-resize`,
890
+ "data-handle": "n"
891
+ }
892
+ ),
893
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
894
+ "div",
895
+ {
896
+ className: `${handleClass} pointer-events-auto -bottom-1.5 left-1/2 -translate-x-1/2 cursor-s-resize`,
897
+ "data-handle": "s"
898
+ }
899
+ ),
900
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
901
+ "div",
902
+ {
903
+ className: `${handleClass} pointer-events-auto -left-1.5 top-1/2 -translate-y-1/2 cursor-w-resize`,
904
+ "data-handle": "w"
905
+ }
906
+ ),
907
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
908
+ "div",
909
+ {
910
+ className: `${handleClass} pointer-events-auto -right-1.5 top-1/2 -translate-y-1/2 cursor-e-resize`,
911
+ "data-handle": "e"
912
+ }
913
+ )
914
+ ] });
915
+ });
916
+
917
+ // src/react/SelectionOverlaySlot.tsx
918
+ var import_react7 = require("react");
919
+ var import_jsx_runtime5 = require("react/jsx-runtime");
920
+ function getMods2(e) {
921
+ return { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey };
922
+ }
923
+ var SelectionOverlaySlot = (0, import_react7.memo)(function SelectionOverlaySlot2({
924
+ entityId,
925
+ slotRef
926
+ }) {
927
+ const wrapperRef = (0, import_react7.useRef)(null);
928
+ const engine = useLayoutEngine();
929
+ const containerRefObj = useContainerRef();
930
+ (0, import_react7.useEffect)(() => {
931
+ slotRef(entityId, wrapperRef.current);
932
+ return () => slotRef(entityId, null);
933
+ }, [entityId, slotRef]);
934
+ const toLocal = (0, import_react7.useCallback)(
935
+ (e) => {
936
+ const rect = containerRefObj?.current?.getBoundingClientRect();
937
+ if (!rect) return { x: e.clientX, y: e.clientY };
938
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
939
+ },
940
+ [containerRefObj]
941
+ );
942
+ const capturedRef = (0, import_react7.useRef)(false);
943
+ const onPointerDown = (0, import_react7.useCallback)(
944
+ (e) => {
945
+ e.stopPropagation();
946
+ const { x, y } = toLocal(e);
947
+ const directive = engine.handlePointerDown(x, y, e.button, getMods2(e));
948
+ if (directive.action === "capture-resize" || directive.action === "passthrough-track-drag") {
949
+ wrapperRef.current?.setPointerCapture(e.pointerId);
950
+ }
951
+ if (directive.action === "capture-resize") {
952
+ e.preventDefault();
953
+ }
954
+ },
955
+ [engine, toLocal]
956
+ );
957
+ const onPointerMove = (0, import_react7.useCallback)(
958
+ (e) => {
959
+ const { x, y } = toLocal(e);
960
+ const directive = engine.handlePointerMove(x, y, getMods2(e));
961
+ if (directive.action === "capture-drag" && !capturedRef.current) {
962
+ capturedRef.current = true;
963
+ e.stopPropagation();
964
+ }
965
+ },
966
+ [engine, toLocal]
967
+ );
968
+ const onPointerUp = (0, import_react7.useCallback)(
969
+ (e) => {
970
+ e.stopPropagation();
971
+ capturedRef.current = false;
972
+ if (wrapperRef.current?.hasPointerCapture(e.pointerId)) {
973
+ wrapperRef.current.releasePointerCapture(e.pointerId);
974
+ }
975
+ engine.handlePointerUp();
976
+ },
977
+ [engine]
978
+ );
979
+ const onDoubleClick = (0, import_react7.useCallback)(
980
+ (e) => {
981
+ e.stopPropagation();
982
+ engine.enterContainer(entityId);
983
+ },
984
+ [engine, entityId]
985
+ );
986
+ const wb = engine.get(entityId, WorldBounds);
987
+ const initialStyle = wb ? {
988
+ transform: `translate(${wb.worldX}px, ${wb.worldY}px)`,
989
+ width: `${wb.worldWidth}px`,
990
+ height: `${wb.worldHeight}px`
991
+ } : {};
992
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
993
+ "div",
994
+ {
995
+ ref: wrapperRef,
996
+ className: "absolute left-0 top-0 origin-top-left will-change-transform",
997
+ style: initialStyle,
998
+ onPointerDown,
999
+ onPointerMove,
1000
+ onPointerUp,
1001
+ onDoubleClick
1002
+ }
1003
+ );
1004
+ });
1005
+
1006
+ // src/serialization.ts
1007
+ function serializeWorld(world, componentTypes, tagTypes, camera, navigationFrames) {
1008
+ const entities = [];
1009
+ const allEntities = world.query();
1010
+ for (const entityId of allEntities) {
1011
+ const components = {};
1012
+ const tags = [];
1013
+ for (const type of componentTypes) {
1014
+ const data = world.getComponent(entityId, type);
1015
+ if (data !== void 0) {
1016
+ components[type.name] = structuredClone(data);
1017
+ }
1018
+ }
1019
+ for (const type of tagTypes) {
1020
+ if (world.hasTag(entityId, type)) {
1021
+ if (type.name !== "Active" && type.name !== "Visible") {
1022
+ tags.push(type.name);
1023
+ }
1024
+ }
1025
+ }
1026
+ if (Object.keys(components).length > 0 || tags.length > 0) {
1027
+ entities.push({ id: entityId, components, tags });
1028
+ }
1029
+ }
1030
+ return {
1031
+ version: 1,
1032
+ entities,
1033
+ resources: {
1034
+ camera: { ...camera },
1035
+ navigationStack: structuredClone(navigationFrames)
1036
+ }
1037
+ };
1038
+ }
1039
+ function deserializeWorld(world, doc, componentTypes, tagTypes) {
1040
+ const compByName = /* @__PURE__ */ new Map();
1041
+ for (const t of componentTypes) compByName.set(t.name, t);
1042
+ const tagByName = /* @__PURE__ */ new Map();
1043
+ for (const t of tagTypes) tagByName.set(t.name, t);
1044
+ for (const entityId of world.query()) {
1045
+ world.destroyEntity(entityId);
1046
+ }
1047
+ for (const entry of doc.entities) {
1048
+ const entity = world.createEntity();
1049
+ for (const [compName, data] of Object.entries(entry.components)) {
1050
+ const type = compByName.get(compName);
1051
+ if (type) {
1052
+ world.addComponent(entity, type, data);
1053
+ }
1054
+ }
1055
+ for (const tagName of entry.tags) {
1056
+ const type = tagByName.get(tagName);
1057
+ if (type) {
1058
+ world.addTag(entity, type);
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+ function serializeEntities(world, entityIds, componentTypes, tagTypes) {
1064
+ const result = [];
1065
+ const visited = /* @__PURE__ */ new Set();
1066
+ function visit(entityId) {
1067
+ if (visited.has(entityId)) return;
1068
+ visited.add(entityId);
1069
+ const components = {};
1070
+ const tags = [];
1071
+ for (const type of componentTypes) {
1072
+ const data = world.getComponent(entityId, type);
1073
+ if (data !== void 0) {
1074
+ components[type.name] = structuredClone(data);
1075
+ }
1076
+ }
1077
+ for (const type of tagTypes) {
1078
+ if (world.hasTag(entityId, type)) {
1079
+ if (type.name !== "Active" && type.name !== "Visible") {
1080
+ tags.push(type.name);
1081
+ }
1082
+ }
1083
+ }
1084
+ result.push({ id: entityId, components, tags });
1085
+ const children = components["Children"];
1086
+ if (children?.ids) {
1087
+ for (const childId of children.ids) {
1088
+ visit(childId);
1089
+ }
1090
+ }
1091
+ }
1092
+ for (const id of entityIds) {
1093
+ visit(id);
1094
+ }
1095
+ return result;
1096
+ }
1097
+
1098
+ // src/snap.ts
1099
+ function computeSnapGuides(dragged, references, threshold) {
1100
+ const guides = [];
1101
+ const spacings = [];
1102
+ let snapDx = 0;
1103
+ let snapDy = 0;
1104
+ const dLeft = dragged.x;
1105
+ const dRight = dragged.x + dragged.width;
1106
+ const dCenterX = dragged.x + dragged.width / 2;
1107
+ const dTop = dragged.y;
1108
+ const dBottom = dragged.y + dragged.height;
1109
+ const dCenterY = dragged.y + dragged.height / 2;
1110
+ let bestSnapX = Number.POSITIVE_INFINITY;
1111
+ let bestSnapY = Number.POSITIVE_INFINITY;
1112
+ let bestDx = 0;
1113
+ let bestDy = 0;
1114
+ const xGuides = [];
1115
+ const yGuides = [];
1116
+ for (const ref of references) {
1117
+ const rLeft = ref.x;
1118
+ const rRight = ref.x + ref.width;
1119
+ const rCenterX = ref.x + ref.width / 2;
1120
+ const rTop = ref.y;
1121
+ const rBottom = ref.y + ref.height;
1122
+ const rCenterY = ref.y + ref.height / 2;
1123
+ const xPairs = [
1124
+ [dLeft, rLeft, "edge"],
1125
+ [dLeft, rRight, "edge"],
1126
+ [dRight, rLeft, "edge"],
1127
+ [dRight, rRight, "edge"],
1128
+ [dCenterX, rCenterX, "center"],
1129
+ [dLeft, rCenterX, "edge"],
1130
+ [dRight, rCenterX, "edge"]
1131
+ ];
1132
+ for (const [dVal, rVal, type] of xPairs) {
1133
+ const dist = Math.abs(dVal - rVal);
1134
+ if (dist <= threshold) {
1135
+ const dx = rVal - dVal;
1136
+ if (dist < bestSnapX) {
1137
+ bestSnapX = dist;
1138
+ bestDx = dx;
1139
+ xGuides.length = 0;
1140
+ }
1141
+ if (dist <= bestSnapX + 0.01) {
1142
+ xGuides.push({ axis: "x", position: rVal, type });
1143
+ }
1144
+ }
1145
+ }
1146
+ const yPairs = [
1147
+ [dTop, rTop, "edge"],
1148
+ [dTop, rBottom, "edge"],
1149
+ [dBottom, rTop, "edge"],
1150
+ [dBottom, rBottom, "edge"],
1151
+ [dCenterY, rCenterY, "center"],
1152
+ [dTop, rCenterY, "edge"],
1153
+ [dBottom, rCenterY, "edge"]
1154
+ ];
1155
+ for (const [dVal, rVal, type] of yPairs) {
1156
+ const dist = Math.abs(dVal - rVal);
1157
+ if (dist <= threshold) {
1158
+ const dy = rVal - dVal;
1159
+ if (dist < bestSnapY) {
1160
+ bestSnapY = dist;
1161
+ bestDy = dy;
1162
+ yGuides.length = 0;
1163
+ }
1164
+ if (dist <= bestSnapY + 0.01) {
1165
+ yGuides.push({ axis: "y", position: rVal, type });
1166
+ }
1167
+ }
1168
+ }
1169
+ }
1170
+ const eqResult = computeEqualSpacing(dragged, references, threshold);
1171
+ if (bestSnapX <= threshold) {
1172
+ snapDx = bestDx;
1173
+ } else if (eqResult.snapDx !== void 0) {
1174
+ snapDx = eqResult.snapDx;
1175
+ }
1176
+ if (bestSnapY <= threshold) {
1177
+ snapDy = bestDy;
1178
+ } else if (eqResult.snapDy !== void 0) {
1179
+ snapDy = eqResult.snapDy;
1180
+ }
1181
+ if (bestSnapX <= threshold) {
1182
+ const seen = /* @__PURE__ */ new Set();
1183
+ for (const g of xGuides) {
1184
+ if (!seen.has(g.position)) {
1185
+ seen.add(g.position);
1186
+ guides.push(g);
1187
+ }
1188
+ }
1189
+ }
1190
+ if (bestSnapY <= threshold) {
1191
+ const seen = /* @__PURE__ */ new Set();
1192
+ for (const g of yGuides) {
1193
+ if (!seen.has(g.position)) {
1194
+ seen.add(g.position);
1195
+ guides.push(g);
1196
+ }
1197
+ }
1198
+ }
1199
+ const snappedBounds = {
1200
+ x: dragged.x + snapDx,
1201
+ y: dragged.y + snapDy,
1202
+ width: dragged.width,
1203
+ height: dragged.height
1204
+ };
1205
+ const eqFinal = computeEqualSpacing(snappedBounds, references, threshold * 0.5);
1206
+ spacings.push(...eqFinal.indicators);
1207
+ return { snapDx, snapDy, guides, spacings };
1208
+ }
1209
+ function computeEqualSpacing(dragged, references, threshold) {
1210
+ const indicators = [];
1211
+ let snapDx;
1212
+ let snapDy;
1213
+ const xResult = checkAxisSpacing(dragged, references, threshold, "x");
1214
+ if (xResult) {
1215
+ snapDx = xResult.snap;
1216
+ indicators.push(...xResult.indicators);
1217
+ }
1218
+ const yResult = checkAxisSpacing(dragged, references, threshold, "y");
1219
+ if (yResult) {
1220
+ snapDy = yResult.snap;
1221
+ indicators.push(...yResult.indicators);
1222
+ }
1223
+ return { snapDx, snapDy, indicators };
1224
+ }
1225
+ function checkAxisSpacing(dragged, references, threshold, axis) {
1226
+ const isX = axis === "x";
1227
+ const pos = (b) => isX ? b.x : b.y;
1228
+ const size = (b) => isX ? b.width : b.height;
1229
+ const perpPos = (b) => isX ? b.y : b.x;
1230
+ const perpSize = (b) => isX ? b.height : b.width;
1231
+ const end = (b) => pos(b) + size(b);
1232
+ const neighbors = references.filter(
1233
+ (ref) => perpPos(ref) < perpPos(dragged) + perpSize(dragged) && perpPos(ref) + perpSize(ref) > perpPos(dragged)
1234
+ );
1235
+ if (neighbors.length < 1) return null;
1236
+ const sorted = [...neighbors].sort((a, b) => pos(a) - pos(b));
1237
+ const refGaps = [];
1238
+ for (let i = 0; i < sorted.length - 1; i++) {
1239
+ const gap = pos(sorted[i + 1]) - end(sorted[i]);
1240
+ if (gap > 0.1) {
1241
+ refGaps.push({ from: sorted[i], to: sorted[i + 1], gap });
1242
+ }
1243
+ }
1244
+ let bestSnap = null;
1245
+ let bestIndicators = [];
1246
+ let bestDiff = Number.POSITIVE_INFINITY;
1247
+ let leftN = null;
1248
+ let rightN = null;
1249
+ for (const ref of sorted) {
1250
+ if (end(ref) <= pos(dragged) + threshold) {
1251
+ if (!leftN || end(ref) > end(leftN)) leftN = ref;
1252
+ }
1253
+ if (pos(ref) >= end(dragged) - threshold) {
1254
+ if (!rightN || pos(ref) < pos(rightN)) rightN = ref;
1255
+ }
1256
+ }
1257
+ if (leftN && rightN) {
1258
+ const lGap = pos(dragged) - end(leftN);
1259
+ const rGap = pos(rightN) - end(dragged);
1260
+ const diff = Math.abs(lGap - rGap);
1261
+ if (diff <= threshold && diff < bestDiff) {
1262
+ const idealPos = (end(leftN) + pos(rightN) - size(dragged)) / 2;
1263
+ const snap = idealPos - pos(dragged);
1264
+ const equalGap = (pos(rightN) - end(leftN) - size(dragged)) / 2;
1265
+ if (equalGap > 0.1) {
1266
+ const perpY = computePerpCenter(dragged, [leftN, rightN], isX);
1267
+ bestSnap = snap;
1268
+ bestDiff = diff;
1269
+ bestIndicators = [
1270
+ {
1271
+ axis,
1272
+ gap: equalGap,
1273
+ segments: [
1274
+ { from: end(leftN), to: idealPos },
1275
+ { from: idealPos + size(dragged), to: pos(rightN) }
1276
+ ],
1277
+ perpPosition: perpY
1278
+ }
1279
+ ];
1280
+ }
1281
+ }
1282
+ }
1283
+ for (const refGap of refGaps) {
1284
+ const patternGap = refGap.gap;
1285
+ if (rightN === null || pos(refGap.to) >= end(dragged) - threshold * 2) {
1286
+ const chainEnd = refGap.to;
1287
+ const dragGap = pos(dragged) - end(chainEnd);
1288
+ const diff = Math.abs(dragGap - patternGap);
1289
+ if (diff <= threshold && diff < bestDiff) {
1290
+ const idealPos = end(chainEnd) + patternGap;
1291
+ const snap = idealPos - pos(dragged);
1292
+ const perpY = computePerpCenter(dragged, [refGap.from, refGap.to], isX);
1293
+ bestSnap = snap;
1294
+ bestDiff = diff;
1295
+ bestIndicators = [
1296
+ {
1297
+ axis,
1298
+ gap: patternGap,
1299
+ segments: [
1300
+ { from: end(refGap.from), to: pos(refGap.to) },
1301
+ { from: end(chainEnd), to: idealPos }
1302
+ ],
1303
+ perpPosition: perpY
1304
+ }
1305
+ ];
1306
+ }
1307
+ }
1308
+ if (leftN === null || end(refGap.from) <= pos(dragged) + threshold * 2) {
1309
+ const chainStart = refGap.from;
1310
+ const dragGap = pos(chainStart) - end(dragged);
1311
+ const diff = Math.abs(dragGap - patternGap);
1312
+ if (diff <= threshold && diff < bestDiff) {
1313
+ const idealPos = pos(chainStart) - patternGap - size(dragged);
1314
+ const snap = idealPos - pos(dragged);
1315
+ const perpY = computePerpCenter(dragged, [refGap.from, refGap.to], isX);
1316
+ bestSnap = snap;
1317
+ bestDiff = diff;
1318
+ bestIndicators = [
1319
+ {
1320
+ axis,
1321
+ gap: patternGap,
1322
+ segments: [
1323
+ { from: idealPos + size(dragged), to: pos(chainStart) },
1324
+ { from: end(refGap.from), to: pos(refGap.to) }
1325
+ ],
1326
+ perpPosition: perpY
1327
+ }
1328
+ ];
1329
+ }
1330
+ }
1331
+ }
1332
+ if (bestSnap !== null) {
1333
+ return { snap: bestSnap, indicators: bestIndicators };
1334
+ }
1335
+ return null;
1336
+ }
1337
+ function computePerpCenter(dragged, refs, isX) {
1338
+ const perpPos = (b) => isX ? b.y : b.x;
1339
+ const perpSize = (b) => isX ? b.height : b.width;
1340
+ const allBounds = [dragged, ...refs];
1341
+ const maxStart = Math.max(...allBounds.map(perpPos));
1342
+ const minEnd = Math.min(...allBounds.map((b) => perpPos(b) + perpSize(b)));
1343
+ return maxStart + (minEnd - maxStart) / 2;
1344
+ }
1345
+
1346
+ // src/profiler.ts
1347
+ var RING_SIZE = 300;
1348
+ var Profiler = class {
1349
+ enabled = false;
1350
+ ring = [];
1351
+ writeIdx = 0;
1352
+ filled = false;
1353
+ // Scratch state for current frame
1354
+ frameStart = 0;
1355
+ currentSystems = {};
1356
+ visibilityMs = 0;
1357
+ currentTick = 0;
1358
+ /** Enable/disable profiling. When disabled, all methods are no-ops. */
1359
+ setEnabled(on) {
1360
+ this.enabled = on;
1361
+ if (!on) {
1362
+ this.ring = [];
1363
+ this.writeIdx = 0;
1364
+ this.filled = false;
1365
+ }
1366
+ }
1367
+ isEnabled() {
1368
+ return this.enabled;
1369
+ }
1370
+ /** Call at the start of engine.tick() */
1371
+ beginFrame(tick) {
1372
+ if (!this.enabled) return;
1373
+ this.currentTick = tick;
1374
+ this.currentSystems = {};
1375
+ this.visibilityMs = 0;
1376
+ this.frameStart = performance.now();
1377
+ performance.mark("ic-frame-start");
1378
+ }
1379
+ /** Call around each system execution */
1380
+ beginSystem(name) {
1381
+ if (!this.enabled) return;
1382
+ performance.mark(`ic-sys-${name}-start`);
1383
+ }
1384
+ endSystem(name) {
1385
+ if (!this.enabled) return;
1386
+ performance.mark(`ic-sys-${name}-end`);
1387
+ try {
1388
+ const measure = performance.measure(
1389
+ `ic:${name}`,
1390
+ `ic-sys-${name}-start`,
1391
+ `ic-sys-${name}-end`
1392
+ );
1393
+ this.currentSystems[name] = measure.duration;
1394
+ } catch {
1395
+ }
1396
+ performance.clearMarks(`ic-sys-${name}-start`);
1397
+ performance.clearMarks(`ic-sys-${name}-end`);
1398
+ }
1399
+ /** Call around the visibility computation phase */
1400
+ beginVisibility() {
1401
+ if (!this.enabled) return;
1402
+ performance.mark("ic-vis-start");
1403
+ }
1404
+ endVisibility() {
1405
+ if (!this.enabled) return;
1406
+ performance.mark("ic-vis-end");
1407
+ try {
1408
+ const measure = performance.measure("ic:visibility", "ic-vis-start", "ic-vis-end");
1409
+ this.visibilityMs = measure.duration;
1410
+ } catch {
1411
+ }
1412
+ performance.clearMarks("ic-vis-start");
1413
+ performance.clearMarks("ic-vis-end");
1414
+ }
1415
+ /** Call at the end of engine.tick() */
1416
+ endFrame(entityCount, visibleCount) {
1417
+ if (!this.enabled) return;
1418
+ performance.mark("ic-frame-end");
1419
+ let totalMs = 0;
1420
+ try {
1421
+ const measure = performance.measure("ic:frame", "ic-frame-start", "ic-frame-end");
1422
+ totalMs = measure.duration;
1423
+ } catch {
1424
+ totalMs = performance.now() - this.frameStart;
1425
+ }
1426
+ performance.clearMarks("ic-frame-start");
1427
+ performance.clearMarks("ic-frame-end");
1428
+ const sample = {
1429
+ tick: this.currentTick,
1430
+ timestamp: performance.now(),
1431
+ totalMs,
1432
+ systems: { ...this.currentSystems },
1433
+ visibilityMs: this.visibilityMs,
1434
+ entityCount,
1435
+ visibleCount
1436
+ };
1437
+ if (this.ring.length < RING_SIZE) {
1438
+ this.ring.push(sample);
1439
+ } else {
1440
+ this.ring[this.writeIdx] = sample;
1441
+ }
1442
+ this.writeIdx = (this.writeIdx + 1) % RING_SIZE;
1443
+ if (this.ring.length >= RING_SIZE) this.filled = true;
1444
+ }
1445
+ /** Get the last N frame samples (newest first) */
1446
+ getSamples(count) {
1447
+ const n = Math.min(count ?? this.ring.length, this.ring.length);
1448
+ const result = [];
1449
+ for (let i = 0; i < n; i++) {
1450
+ const idx = (this.writeIdx - 1 - i + this.ring.length) % this.ring.length;
1451
+ if (idx >= 0 && idx < this.ring.length) {
1452
+ result.push(this.ring[idx]);
1453
+ }
1454
+ }
1455
+ return result;
1456
+ }
1457
+ /** Compute rolling statistics from the ring buffer */
1458
+ getStats() {
1459
+ const samples = this.ring;
1460
+ const n = samples.length;
1461
+ if (n === 0) {
1462
+ return {
1463
+ fps: 0,
1464
+ frameTime: { avg: 0, p50: 0, p95: 0, p99: 0, max: 0 },
1465
+ systemAvg: {},
1466
+ systemP95: {},
1467
+ budgetUsed: 0,
1468
+ sampleCount: 0
1469
+ };
1470
+ }
1471
+ const frameTimes = samples.map((s) => s.totalMs).sort((a, b) => a - b);
1472
+ const newest = samples[this.filled ? (this.writeIdx - 1 + RING_SIZE) % RING_SIZE : n - 1];
1473
+ const oldest = samples[this.filled ? this.writeIdx : 0];
1474
+ const spanMs = newest.timestamp - oldest.timestamp;
1475
+ const fps = spanMs > 0 ? Math.round((n - 1) / spanMs * 1e3) : 0;
1476
+ const percentile = (sorted, p) => {
1477
+ const idx = Math.floor(p / 100 * (sorted.length - 1));
1478
+ return sorted[idx] ?? 0;
1479
+ };
1480
+ const avg = frameTimes.reduce((a, b) => a + b, 0) / n;
1481
+ const systemNames = /* @__PURE__ */ new Set();
1482
+ for (const s of samples) {
1483
+ for (const name of Object.keys(s.systems)) systemNames.add(name);
1484
+ }
1485
+ const systemAvg = {};
1486
+ const systemP95 = {};
1487
+ for (const name of systemNames) {
1488
+ const times = samples.map((s) => s.systems[name] ?? 0).sort((a, b) => a - b);
1489
+ systemAvg[name] = times.reduce((a, b) => a + b, 0) / n;
1490
+ systemP95[name] = percentile(times, 95);
1491
+ }
1492
+ return {
1493
+ fps,
1494
+ frameTime: {
1495
+ avg,
1496
+ p50: percentile(frameTimes, 50),
1497
+ p95: percentile(frameTimes, 95),
1498
+ p99: percentile(frameTimes, 99),
1499
+ max: frameTimes[frameTimes.length - 1]
1500
+ },
1501
+ systemAvg,
1502
+ systemP95,
1503
+ budgetUsed: avg / 16.67 * 100,
1504
+ sampleCount: n
1505
+ };
1506
+ }
1507
+ /** Clear all collected data */
1508
+ clear() {
1509
+ this.ring = [];
1510
+ this.writeIdx = 0;
1511
+ this.filled = false;
1512
+ }
1513
+ };
1514
+
1515
+ // src/spatial.ts
1516
+ var import_rbush = __toESM(require("rbush"), 1);
1517
+ var RBush = typeof import_rbush.default.default === "function" ? import_rbush.default.default : import_rbush.default;
1518
+ var SpatialIndex = class {
1519
+ tree = new RBush();
1520
+ entries = /* @__PURE__ */ new Map();
1521
+ upsert(entityId, bounds) {
1522
+ const existing = this.entries.get(entityId);
1523
+ if (existing) {
1524
+ this.tree.remove(existing);
1525
+ }
1526
+ const entry = { ...bounds, entityId };
1527
+ this.entries.set(entityId, entry);
1528
+ this.tree.insert(entry);
1529
+ }
1530
+ remove(entityId) {
1531
+ const existing = this.entries.get(entityId);
1532
+ if (existing) {
1533
+ this.tree.remove(existing);
1534
+ this.entries.delete(entityId);
1535
+ }
1536
+ }
1537
+ /** Query all entries intersecting the given AABB */
1538
+ search(bounds) {
1539
+ return this.tree.search(bounds);
1540
+ }
1541
+ /** Find the topmost entity at a point (by z-order — caller sorts) */
1542
+ searchPoint(x, y, tolerance = 0) {
1543
+ return this.tree.search({
1544
+ minX: x - tolerance,
1545
+ minY: y - tolerance,
1546
+ maxX: x + tolerance,
1547
+ maxY: y + tolerance
1548
+ });
1549
+ }
1550
+ clear() {
1551
+ this.tree.clear();
1552
+ this.entries.clear();
1553
+ }
1554
+ get size() {
1555
+ return this.entries.size;
1556
+ }
1557
+ };
1558
+ // Annotate the CommonJS export names for ESM import in node:
1559
+ 0 && (module.exports = {
1560
+ ContainerRefProvider,
1561
+ DEFAULT_GRID_CONFIG,
1562
+ DEFAULT_SELECTION_CONFIG,
1563
+ EngineProvider,
1564
+ GridRenderer,
1565
+ Profiler,
1566
+ SelectionFrame,
1567
+ SelectionOverlaySlot,
1568
+ SelectionRenderer,
1569
+ SpatialIndex,
1570
+ WebGLWidgetLayer,
1571
+ WebGLWidgetSlot,
1572
+ WidgetSlot,
1573
+ computeSnapGuides,
1574
+ deserializeWorld,
1575
+ serializeEntities,
1576
+ serializeWorld
1577
+ });
1578
+ //# sourceMappingURL=advanced.cjs.map