@jamesyong42/infinite-canvas 1.0.0 → 1.2.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.
Files changed (54) hide show
  1. package/README.md +170 -9
  2. package/dist/SelectionRenderer-CR2PBQwx.d.cts +105 -0
  3. package/dist/SelectionRenderer-CR2PBQwx.d.cts.map +1 -0
  4. package/dist/SelectionRenderer-DlsBstAq.d.mts +105 -0
  5. package/dist/SelectionRenderer-DlsBstAq.d.mts.map +1 -0
  6. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs +3560 -0
  7. package/dist/WebGLWidgetLayer-BBMuwzHq.cjs.map +1 -0
  8. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs +3375 -0
  9. package/dist/WebGLWidgetLayer-C3p1tnpm.mjs.map +1 -0
  10. package/dist/advanced.cjs +110 -165
  11. package/dist/advanced.cjs.map +1 -1
  12. package/dist/advanced.d.cts +58 -40
  13. package/dist/advanced.d.cts.map +1 -0
  14. package/dist/advanced.d.mts +99 -0
  15. package/dist/advanced.d.mts.map +1 -0
  16. package/dist/advanced.mjs +105 -0
  17. package/dist/advanced.mjs.map +1 -0
  18. package/dist/devtools.cjs +654 -0
  19. package/dist/devtools.cjs.map +1 -0
  20. package/dist/devtools.d.cts +23 -0
  21. package/dist/devtools.d.cts.map +1 -0
  22. package/dist/devtools.d.mts +23 -0
  23. package/dist/devtools.d.mts.map +1 -0
  24. package/dist/devtools.mjs +652 -0
  25. package/dist/devtools.mjs.map +1 -0
  26. package/dist/engine-BfbvWXSk.d.mts +982 -0
  27. package/dist/engine-BfbvWXSk.d.mts.map +1 -0
  28. package/dist/engine-CCjuFMC-.d.cts +982 -0
  29. package/dist/engine-CCjuFMC-.d.cts.map +1 -0
  30. package/dist/hooks-BwY7rRHg.mjs +425 -0
  31. package/dist/hooks-BwY7rRHg.mjs.map +1 -0
  32. package/dist/hooks-DHShH86C.cjs +707 -0
  33. package/dist/hooks-DHShH86C.cjs.map +1 -0
  34. package/dist/index.cjs +909 -803
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +199 -67
  37. package/dist/index.d.cts.map +1 -0
  38. package/dist/index.d.mts +258 -0
  39. package/dist/index.d.mts.map +1 -0
  40. package/dist/index.mjs +855 -0
  41. package/dist/index.mjs.map +1 -0
  42. package/package.json +47 -15
  43. package/dist/SelectionRenderer-CeWSNZT8.d.cts +0 -891
  44. package/dist/SelectionRenderer-CeWSNZT8.d.ts +0 -891
  45. package/dist/advanced.d.ts +0 -81
  46. package/dist/advanced.js +0 -124
  47. package/dist/advanced.js.map +0 -1
  48. package/dist/chunk-VSHXWTJH.cjs +0 -3228
  49. package/dist/chunk-VSHXWTJH.cjs.map +0 -1
  50. package/dist/chunk-Z6JQQOWL.js +0 -3142
  51. package/dist/chunk-Z6JQQOWL.js.map +0 -1
  52. package/dist/index.d.ts +0 -126
  53. package/dist/index.js +0 -602
  54. package/dist/index.js.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,855 @@
1
+ import { C as MoveCommand, E as createArchetypeRegistry, S as CommandBuffer, T as SetComponentCommand, _ as intersectsAABB, a as DEFAULT_GRID_CONFIG, b as worldBoundsToAABB, c as SelectionOverlaySlot, g as clamp, i as SelectionRenderer, m as isR3FWidget, o as GridRenderer, p as createWidgetRegistry, r as DEFAULT_SELECTION_CONFIG, s as WidgetSlot, t as WebGLWidgetLayer, u as createLayoutEngine, v as pointInAABB, w as ResizeCommand, x as worldToScreen, y as screenToWorld } from "./WebGLWidgetLayer-C3p1tnpm.mjs";
2
+ import { A as Draggable, B as SelectionFrame, C as ViewportResource, D as Children, E as Card, F as Locked, G as WidgetData, H as Visible, I as Parent, K as WorldBounds, L as Resizable, M as HandleSet, N as Hitbox, O as Container, P as InteractionRole, R as Selectable, S as NavigationStackResource, T as Active, U as Widget, V as Transform2D, W as WidgetBreakpoint, _ as useWidgetResolver, a as useEntityTags, b as CardPresetsResource, c as useRegisteredTags, d as useTaggedEntities, f as ContainerRefProvider, g as useLayoutEngine, h as useContainerRef, i as useEntityComponents, j as Dragging, k as CursorHint, l as useResource, m as WidgetResolverProvider, n as useCamera, o as useQuery, p as EngineProvider, q as ZIndex, r as useComponent, s as useRegisteredComponents, t as useAllEntities, u as useTag, v as BreakpointConfigResource, w as ZoomConfigResource, x as CursorResource, y as CameraResource, z as Selected } from "./hooks-BwY7rRHg.mjs";
3
+ import React, { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { useFrame } from "@react-three/fiber";
6
+ import { ExtrudeGeometry, Shape, Vector2 } from "three";
7
+ //#region src/react/widget-hooks.ts
8
+ /**
9
+ * Returns the custom data attached to a widget entity.
10
+ * Use the generic parameter for type safety: `useWidgetData<MyData>(entityId)`. Re-renders when data changes.
11
+ */
12
+ function useWidgetData(entityId) {
13
+ return useComponent(entityId, WidgetData)?.data ?? {};
14
+ }
15
+ /**
16
+ * Returns the current responsive breakpoint for a widget based on its screen-space size.
17
+ * Re-renders when the breakpoint changes.
18
+ */
19
+ function useBreakpoint(entityId) {
20
+ return useComponent(entityId, WidgetBreakpoint)?.current ?? "normal";
21
+ }
22
+ /**
23
+ * Returns child entity IDs of a container entity.
24
+ * Re-renders when children are added or removed.
25
+ */
26
+ function useChildren(entityId) {
27
+ return useComponent(entityId, Children)?.ids ?? [];
28
+ }
29
+ /**
30
+ * Returns whether the entity is currently selected.
31
+ * Re-renders when the entity's selection state changes.
32
+ */
33
+ function useIsSelected(entityId) {
34
+ return useTag(entityId, Selected);
35
+ }
36
+ /**
37
+ * Returns a function to update the widget's custom data.
38
+ * Merges the patch into existing data via shallow spread.
39
+ */
40
+ function useUpdateWidget(entityId) {
41
+ const engine = useLayoutEngine();
42
+ return useCallback((patch) => {
43
+ const existing = engine.get(entityId, WidgetData);
44
+ if (existing) engine.set(entityId, WidgetData, { data: {
45
+ ...existing.data,
46
+ ...patch
47
+ } });
48
+ }, [engine, entityId]);
49
+ }
50
+ //#endregion
51
+ //#region src/react/card.tsx
52
+ /**
53
+ * Built-in preset sizes, matching `CardPresetsResource` defaults.
54
+ * Used by `createCardWidget` to set `defaultSize` at widget-registration
55
+ * time (before the engine is constructed).
56
+ */
57
+ const DEFAULT_CARD_PRESET_SIZES$1 = {
58
+ small: {
59
+ width: 155,
60
+ height: 155
61
+ },
62
+ medium: {
63
+ width: 329,
64
+ height: 155
65
+ },
66
+ large: {
67
+ width: 329,
68
+ height: 345
69
+ },
70
+ xl: {
71
+ width: 329,
72
+ height: 535
73
+ }
74
+ };
75
+ /**
76
+ * Visual chrome for an iOS-style card: rounded corners, hairline ring,
77
+ * soft drop shadow, and a subtle lift (scale + stronger shadow) while
78
+ * the entity carries the `Dragging` tag.
79
+ *
80
+ * Uses CSS transitions — no animation library dependency.
81
+ */
82
+ function CardFrame({ entityId, children, className, style }) {
83
+ const dragging = useTag(entityId, Dragging);
84
+ return /* @__PURE__ */ jsx("div", {
85
+ className,
86
+ style: {
87
+ width: "100%",
88
+ height: "100%",
89
+ borderRadius: "21.67px",
90
+ overflow: "hidden",
91
+ boxShadow: dragging ? "0 30px 60px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.06)" : "0 20px 40px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.05)",
92
+ transform: dragging ? "scale(1.05)" : "scale(1)",
93
+ transformOrigin: "center center",
94
+ transition: "transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1.2), box-shadow 180ms cubic-bezier(0.2, 0.9, 0.3, 1.2)",
95
+ willChange: dragging ? "transform, box-shadow" : void 0,
96
+ ...style
97
+ },
98
+ children
99
+ });
100
+ }
101
+ /**
102
+ * Returns a paired widget + archetype for an iOS-style card. Register both
103
+ * with `createLayoutEngine({ widgets: [card.widget], archetypes: [card.archetype] })`
104
+ * (or via `engine.registerWidget` / `engine.registerArchetype`) and spawn with
105
+ * `engine.spawn('your-card-type', { at, data })`.
106
+ *
107
+ * The produced widget is non-resizable (Selectable + Draggable only), wrapped
108
+ * in `<CardFrame>`, and spawns with a `Card` component so `cardSystem` enforces
109
+ * the preset size each tick.
110
+ */
111
+ function createCardWidget(opts) {
112
+ const defaultSize = DEFAULT_CARD_PRESET_SIZES$1[opts.size];
113
+ const Render = opts.render;
114
+ const Component = ({ entityId }) => {
115
+ return /* @__PURE__ */ jsx(CardFrame, {
116
+ entityId,
117
+ children: /* @__PURE__ */ jsx(Render, {
118
+ entityId,
119
+ data: useWidgetData(entityId)
120
+ })
121
+ });
122
+ };
123
+ return {
124
+ widget: {
125
+ type: opts.type,
126
+ schema: opts.schema,
127
+ defaultData: opts.defaultData,
128
+ defaultSize,
129
+ component: Component
130
+ },
131
+ archetype: {
132
+ id: opts.type,
133
+ widget: opts.type,
134
+ components: [[Card, { preset: opts.size }]],
135
+ interactive: {
136
+ selectable: true,
137
+ draggable: true,
138
+ resizable: false,
139
+ selectionFrame: false
140
+ },
141
+ defaultSize
142
+ }
143
+ };
144
+ }
145
+ //#endregion
146
+ //#region src/react/geometry-card.tsx
147
+ /** Must match {@link CardPresetsResource} defaults. */
148
+ const DEFAULT_CARD_PRESET_SIZES = {
149
+ small: {
150
+ width: 155,
151
+ height: 155
152
+ },
153
+ medium: {
154
+ width: 329,
155
+ height: 155
156
+ },
157
+ large: {
158
+ width: 329,
159
+ height: 345
160
+ },
161
+ xl: {
162
+ width: 329,
163
+ height: 535
164
+ }
165
+ };
166
+ /**
167
+ * Pure-three rounded-rect extrude geometry — avoids a drei dependency.
168
+ * Rounded corners match the DOM CardFrame radius (21.67 px).
169
+ */
170
+ function makeRoundedCardGeometry(width, height, radius, depth) {
171
+ const shape = new Shape();
172
+ const r = Math.min(radius, Math.min(width, height) / 2);
173
+ const x = -width / 2;
174
+ const y = -height / 2;
175
+ shape.moveTo(x, y + r);
176
+ shape.lineTo(x, y + height - r);
177
+ shape.quadraticCurveTo(x, y + height, x + r, y + height);
178
+ shape.lineTo(x + width - r, y + height);
179
+ shape.quadraticCurveTo(x + width, y + height, x + width, y + height - r);
180
+ shape.lineTo(x + width, y + r);
181
+ shape.quadraticCurveTo(x + width, y, x + width - r, y);
182
+ shape.lineTo(x + r, y);
183
+ shape.quadraticCurveTo(x, y, x, y + r);
184
+ return new ExtrudeGeometry(shape, {
185
+ depth,
186
+ bevelEnabled: true,
187
+ bevelSegments: 3,
188
+ bevelSize: .6,
189
+ bevelThickness: .6
190
+ });
191
+ }
192
+ function CardBack({ width, height, color, roughness, metalness }) {
193
+ return /* @__PURE__ */ jsx("mesh", {
194
+ geometry: useMemo(() => makeRoundedCardGeometry(width, height, 21.67, 3), [width, height]),
195
+ position: [
196
+ 0,
197
+ 0,
198
+ -6
199
+ ],
200
+ receiveShadow: true,
201
+ children: /* @__PURE__ */ jsx("meshStandardMaterial", {
202
+ color,
203
+ roughness,
204
+ metalness
205
+ })
206
+ });
207
+ }
208
+ /**
209
+ * Returns a paired R3F widget + archetype for a card-shaped 3D widget.
210
+ * Behaves like {@link createCardWidget} — fixed preset size, non-resizable,
211
+ * no engine-drawn selection frame, and lifts on drag (scale + z) — but
212
+ * renders a three.js scene instead of DOM content.
213
+ *
214
+ * Lighting: this helper adds no lights. Declare your own in the `geometry`
215
+ * component (typically a local `pointLight` scoped with `distance`).
216
+ */
217
+ function createGeometryCardWidget(opts) {
218
+ const defaultSize = DEFAULT_CARD_PRESET_SIZES[opts.size];
219
+ const Render = opts.geometry;
220
+ const backgroundConfig = opts.background ?? "card";
221
+ const resolvedBack = backgroundConfig === "transparent" ? null : backgroundConfig === "card" ? {
222
+ color: "#1C1C1E",
223
+ roughness: .55,
224
+ metalness: 0
225
+ } : {
226
+ color: backgroundConfig.color,
227
+ roughness: backgroundConfig.roughness ?? .55,
228
+ metalness: backgroundConfig.metalness ?? 0
229
+ };
230
+ const Component = ({ entityId, width, height }) => {
231
+ const data = useWidgetData(entityId);
232
+ const dragging = useTag(entityId, Dragging);
233
+ const groupRef = useRef(null);
234
+ useFrame(() => {
235
+ const g = groupRef.current;
236
+ if (!g) return;
237
+ const targetScale = dragging ? 1.05 : 1;
238
+ const targetZ = dragging ? 8 : 0;
239
+ const s = g.scale.x;
240
+ g.scale.setScalar(s + (targetScale - s) * .2);
241
+ g.position.z += (targetZ - g.position.z) * .2;
242
+ });
243
+ return /* @__PURE__ */ jsxs("group", {
244
+ ref: groupRef,
245
+ children: [resolvedBack && /* @__PURE__ */ jsx(CardBack, {
246
+ width,
247
+ height,
248
+ color: resolvedBack.color,
249
+ roughness: resolvedBack.roughness,
250
+ metalness: resolvedBack.metalness
251
+ }), /* @__PURE__ */ jsx(Render, {
252
+ entityId,
253
+ data,
254
+ width,
255
+ height
256
+ })]
257
+ });
258
+ };
259
+ return {
260
+ widget: {
261
+ type: opts.type,
262
+ surface: "webgl",
263
+ schema: opts.schema,
264
+ defaultData: opts.defaultData,
265
+ defaultSize,
266
+ component: Component
267
+ },
268
+ archetype: {
269
+ id: opts.type,
270
+ widget: opts.type,
271
+ components: [[Card, { preset: opts.size }]],
272
+ interactive: {
273
+ selectable: true,
274
+ draggable: true,
275
+ resizable: false,
276
+ selectionFrame: false
277
+ },
278
+ defaultSize
279
+ }
280
+ };
281
+ }
282
+ //#endregion
283
+ //#region src/react/WidgetProvider.tsx
284
+ /**
285
+ * Bridges the engine's widget registry to React context so WidgetSlot /
286
+ * WebGLWidgetLayer can resolve components by type.
287
+ */
288
+ function WidgetProvider({ engine, children }) {
289
+ return /* @__PURE__ */ jsx(WidgetResolverProvider, {
290
+ value: useCallback((_entityId, widgetType) => {
291
+ const def = engine.getWidget(widgetType);
292
+ if (!def) return null;
293
+ if (isR3FWidget(def)) return {
294
+ surface: "webgl",
295
+ component: def.component
296
+ };
297
+ return {
298
+ surface: "dom",
299
+ component: def.component
300
+ };
301
+ }, [engine]),
302
+ children
303
+ });
304
+ }
305
+ //#endregion
306
+ //#region src/react/InfiniteCanvas.tsx
307
+ const InfiniteCanvas = React.forwardRef(function InfiniteCanvas({ engine, grid, selection, onSelectionChange, onCameraChange, onNavigationChange, className, style, children }, ref) {
308
+ const containerRef = useRef(null);
309
+ const onSelectionChangeRef = useRef(onSelectionChange);
310
+ const onCameraChangeRef = useRef(onCameraChange);
311
+ const onNavigationChangeRef = useRef(onNavigationChange);
312
+ useEffect(() => {
313
+ onSelectionChangeRef.current = onSelectionChange;
314
+ }, [onSelectionChange]);
315
+ useEffect(() => {
316
+ onCameraChangeRef.current = onCameraChange;
317
+ }, [onCameraChange]);
318
+ useEffect(() => {
319
+ onNavigationChangeRef.current = onNavigationChange;
320
+ }, [onNavigationChange]);
321
+ useImperativeHandle(ref, () => ({
322
+ panTo: (x, y) => {
323
+ engine.panTo(x, y);
324
+ engine.markDirty();
325
+ },
326
+ zoomTo: (zoom) => {
327
+ engine.zoomTo(zoom);
328
+ engine.markDirty();
329
+ },
330
+ zoomToFit: (padding) => {
331
+ engine.zoomToFit(void 0, padding);
332
+ engine.markDirty();
333
+ },
334
+ undo: () => {
335
+ engine.undo();
336
+ engine.markDirty();
337
+ },
338
+ redo: () => {
339
+ engine.redo();
340
+ engine.markDirty();
341
+ },
342
+ getEngine: () => engine
343
+ }), [engine]);
344
+ const webglCanvasRef = useRef(null);
345
+ const gridRendererRef = useRef(null);
346
+ const selectionRendererRef = useRef(null);
347
+ const cameraLayerRef = useRef(null);
348
+ const slotRefs = useRef(/* @__PURE__ */ new Map());
349
+ const [visibleEntities, setVisibleEntities] = useState([]);
350
+ const registerSlotRef = useCallback((entityId, el) => {
351
+ if (el) slotRefs.current.set(entityId, el);
352
+ else slotRefs.current.delete(entityId);
353
+ }, []);
354
+ useLayoutEffect(() => {
355
+ const container = containerRef.current;
356
+ const canvas = webglCanvasRef.current;
357
+ if (!container || !canvas) return;
358
+ const gridEnabled = grid !== false;
359
+ let gridInst = null;
360
+ if (gridEnabled) {
361
+ gridInst = new GridRenderer(canvas);
362
+ gridRendererRef.current = gridInst;
363
+ }
364
+ const selInst = new SelectionRenderer();
365
+ selectionRendererRef.current = selInst;
366
+ const updateSize = () => {
367
+ const rect = container.getBoundingClientRect();
368
+ const dpr = window.devicePixelRatio;
369
+ engine.setViewport(rect.width, rect.height, dpr);
370
+ canvas.style.width = `${rect.width}px`;
371
+ canvas.style.height = `${rect.height}px`;
372
+ if (gridInst) gridInst.setSize(rect.width, rect.height, dpr);
373
+ selInst.setSize(new Vector2(rect.width * dpr, rect.height * dpr), dpr);
374
+ };
375
+ updateSize();
376
+ const observer = new ResizeObserver(updateSize);
377
+ observer.observe(container);
378
+ return () => {
379
+ observer.disconnect();
380
+ if (gridInst) {
381
+ gridInst.dispose();
382
+ gridRendererRef.current = null;
383
+ }
384
+ selInst.dispose();
385
+ selectionRendererRef.current = null;
386
+ };
387
+ }, [engine, grid]);
388
+ useEffect(() => {
389
+ const gridR = gridRendererRef.current;
390
+ if (gridR && grid !== false) {
391
+ const isDark = document.documentElement.classList.contains("dark");
392
+ gridR.setConfig({
393
+ dotColor: isDark ? [
394
+ 1,
395
+ 1,
396
+ 1
397
+ ] : [
398
+ 0,
399
+ 0,
400
+ 0
401
+ ],
402
+ dotAlpha: isDark ? .12 : .18,
403
+ ...grid
404
+ });
405
+ }
406
+ const selR = selectionRendererRef.current;
407
+ if (selR && selection) selR.setConfig(selection);
408
+ engine.markDirty();
409
+ }, [
410
+ engine,
411
+ grid,
412
+ selection
413
+ ]);
414
+ useEffect(() => {
415
+ const container = containerRef.current;
416
+ if (!container) return;
417
+ const onWheel = (e) => {
418
+ e.preventDefault();
419
+ if (e.ctrlKey || e.metaKey) {
420
+ const rect = container.getBoundingClientRect();
421
+ engine.zoomAtPoint(e.clientX - rect.left, e.clientY - rect.top, -e.deltaY * .01);
422
+ } else engine.panBy(-e.deltaX, -e.deltaY);
423
+ };
424
+ container.addEventListener("wheel", onWheel, { passive: false });
425
+ return () => container.removeEventListener("wheel", onWheel);
426
+ }, [engine]);
427
+ useEffect(() => {
428
+ const container = containerRef.current;
429
+ if (!container) return;
430
+ let gesture = { type: "idle" };
431
+ let lastTapTime = 0;
432
+ let lastTapX = 0;
433
+ let lastTapY = 0;
434
+ const DOUBLE_TAP_MS = 300;
435
+ const DOUBLE_TAP_DIST = 30;
436
+ function isOnWidget(target) {
437
+ let el = target;
438
+ while (el && el !== container) {
439
+ if (el.hasAttribute("data-widget-slot")) return true;
440
+ el = el.parentElement;
441
+ }
442
+ return false;
443
+ }
444
+ function isInteractive(target) {
445
+ const el = target;
446
+ if (!el) return false;
447
+ const tag = el.tagName;
448
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "BUTTON" || tag === "SELECT" || el.isContentEditable || el.closest("button") !== null;
449
+ }
450
+ function getRect() {
451
+ return container?.getBoundingClientRect() ?? new DOMRect();
452
+ }
453
+ function touchDist(t1, t2) {
454
+ const dx = t1.clientX - t2.clientX;
455
+ const dy = t1.clientY - t2.clientY;
456
+ return Math.sqrt(dx * dx + dy * dy);
457
+ }
458
+ function touchCenter(t1, t2, rect) {
459
+ return {
460
+ x: (t1.clientX + t2.clientX) / 2 - rect.left,
461
+ y: (t1.clientY + t2.clientY) / 2 - rect.top
462
+ };
463
+ }
464
+ function cancelEngineGesture() {
465
+ if (gesture.type === "pending-entity" || gesture.type === "entity-dragging") engine.handlePointerUp();
466
+ }
467
+ const noMods = {
468
+ shift: false,
469
+ ctrl: false,
470
+ alt: false,
471
+ meta: false
472
+ };
473
+ function onTouchStart(e) {
474
+ const rect = getRect();
475
+ const touches = e.touches;
476
+ if (touches.length >= 2) {
477
+ e.preventDefault();
478
+ cancelEngineGesture();
479
+ const dist = touchDist(touches[0], touches[1]);
480
+ const center = touchCenter(touches[0], touches[1], rect);
481
+ gesture = {
482
+ type: "pinching",
483
+ lastDist: dist,
484
+ lastCx: center.x,
485
+ lastCy: center.y
486
+ };
487
+ return;
488
+ }
489
+ const touch = touches[0];
490
+ const x = touch.clientX - rect.left;
491
+ const y = touch.clientY - rect.top;
492
+ if (isInteractive(e.target)) return;
493
+ e.preventDefault();
494
+ const now = Date.now();
495
+ if (now - lastTapTime < DOUBLE_TAP_MS && Math.abs(x - lastTapX) < DOUBLE_TAP_DIST && Math.abs(y - lastTapY) < DOUBLE_TAP_DIST) {
496
+ lastTapTime = 0;
497
+ const directive = engine.handlePointerDown(x, y, 0, noMods);
498
+ try {
499
+ if (directive.action === "passthrough-track-drag") {
500
+ const selected = engine.getSelectedEntities();
501
+ if (selected.length === 1) engine.enterContainer(selected[0]);
502
+ } else {
503
+ const camera = engine.getCamera();
504
+ const target = camera.zoom < .9 ? 1 : camera.zoom < 1.8 ? 2 : 1;
505
+ engine.zoomAtPoint(x, y, (target - camera.zoom) / camera.zoom);
506
+ }
507
+ } finally {
508
+ engine.handlePointerUp();
509
+ engine.markDirty();
510
+ }
511
+ gesture = { type: "idle" };
512
+ return;
513
+ }
514
+ if (isOnWidget(e.target)) {
515
+ engine.handlePointerDown(x, y, 0, noMods);
516
+ gesture = {
517
+ type: "pending-entity",
518
+ x,
519
+ y,
520
+ time: now
521
+ };
522
+ } else gesture = {
523
+ type: "pending-pan",
524
+ x,
525
+ y,
526
+ time: now
527
+ };
528
+ }
529
+ function onTouchMove(e) {
530
+ e.preventDefault();
531
+ const rect = getRect();
532
+ const touches = e.touches;
533
+ if (gesture.type === "pinching" && touches.length >= 2) {
534
+ const dist = touchDist(touches[0], touches[1]);
535
+ const center = touchCenter(touches[0], touches[1], rect);
536
+ const scale = dist / gesture.lastDist;
537
+ engine.zoomAtPoint(center.x, center.y, scale - 1);
538
+ engine.panBy(center.x - gesture.lastCx, center.y - gesture.lastCy);
539
+ gesture.lastDist = dist;
540
+ gesture.lastCx = center.x;
541
+ gesture.lastCy = center.y;
542
+ return;
543
+ }
544
+ if (touches.length >= 2) {
545
+ cancelEngineGesture();
546
+ const dist = touchDist(touches[0], touches[1]);
547
+ const center = touchCenter(touches[0], touches[1], rect);
548
+ gesture = {
549
+ type: "pinching",
550
+ lastDist: dist,
551
+ lastCx: center.x,
552
+ lastCy: center.y
553
+ };
554
+ return;
555
+ }
556
+ if (touches.length < 1) return;
557
+ const touch = touches[0];
558
+ const x = touch.clientX - rect.left;
559
+ const y = touch.clientY - rect.top;
560
+ if (gesture.type === "pending-pan") {
561
+ if (Math.abs(x - gesture.x) > 8 || Math.abs(y - gesture.y) > 8) gesture = {
562
+ type: "panning",
563
+ lastX: x,
564
+ lastY: y
565
+ };
566
+ return;
567
+ }
568
+ if (gesture.type === "panning") {
569
+ engine.panBy(x - gesture.lastX, y - gesture.lastY);
570
+ gesture.lastX = x;
571
+ gesture.lastY = y;
572
+ return;
573
+ }
574
+ if (gesture.type === "pending-entity" || gesture.type === "entity-dragging") {
575
+ engine.handlePointerMove(x, y, noMods);
576
+ if (gesture.type === "pending-entity") {
577
+ if (Math.abs(x - gesture.x) > 8 || Math.abs(y - gesture.y) > 8) gesture = { type: "entity-dragging" };
578
+ }
579
+ }
580
+ }
581
+ function onTouchEnd(e) {
582
+ e.preventDefault();
583
+ const remaining = e.touches.length;
584
+ const rect = getRect();
585
+ if (gesture.type === "pinching") {
586
+ if (remaining === 1) {
587
+ const t = e.touches[0];
588
+ gesture = {
589
+ type: "panning",
590
+ lastX: t.clientX - rect.left,
591
+ lastY: t.clientY - rect.top
592
+ };
593
+ } else if (remaining === 0) gesture = { type: "idle" };
594
+ return;
595
+ }
596
+ if (remaining > 0) return;
597
+ if (gesture.type === "pending-pan") {
598
+ engine.handlePointerDown(gesture.x, gesture.y, 0, noMods);
599
+ engine.handlePointerUp();
600
+ engine.markDirty();
601
+ lastTapTime = Date.now();
602
+ lastTapX = gesture.x;
603
+ lastTapY = gesture.y;
604
+ }
605
+ if (gesture.type === "pending-entity") {
606
+ engine.handlePointerUp();
607
+ engine.markDirty();
608
+ lastTapTime = Date.now();
609
+ lastTapX = gesture.x;
610
+ lastTapY = gesture.y;
611
+ }
612
+ if (gesture.type === "entity-dragging") {
613
+ engine.handlePointerUp();
614
+ engine.markDirty();
615
+ }
616
+ gesture = { type: "idle" };
617
+ }
618
+ function onTouchCancel(_e) {
619
+ gesture = { type: "idle" };
620
+ engine.handlePointerCancel();
621
+ }
622
+ container.addEventListener("touchstart", onTouchStart, { passive: false });
623
+ container.addEventListener("touchmove", onTouchMove, { passive: false });
624
+ container.addEventListener("touchend", onTouchEnd, { passive: false });
625
+ container.addEventListener("touchcancel", onTouchCancel, { passive: true });
626
+ return () => {
627
+ container.removeEventListener("touchstart", onTouchStart);
628
+ container.removeEventListener("touchmove", onTouchMove);
629
+ container.removeEventListener("touchend", onTouchEnd);
630
+ container.removeEventListener("touchcancel", onTouchCancel);
631
+ };
632
+ }, [engine]);
633
+ const onCanvasPointerDown = useCallback((e) => {
634
+ if (e.target?.closest("button, input, textarea, select, [contenteditable]")) return;
635
+ const rect = containerRef.current?.getBoundingClientRect();
636
+ if (!rect) return;
637
+ const directive = engine.handlePointerDown(e.clientX - rect.left, e.clientY - rect.top, e.button, {
638
+ shift: e.shiftKey,
639
+ ctrl: e.ctrlKey,
640
+ alt: e.altKey,
641
+ meta: e.metaKey
642
+ });
643
+ if (directive.action === "capture-resize" || directive.action === "passthrough-track-drag") containerRef.current?.setPointerCapture(e.pointerId);
644
+ if (directive.action === "capture-resize") e.preventDefault();
645
+ }, [engine]);
646
+ const onCanvasPointerMove = useCallback((e) => {
647
+ const target = e.target;
648
+ if (target.closest?.("[data-widget-slot]") && target !== containerRef.current) return;
649
+ const rect = containerRef.current?.getBoundingClientRect();
650
+ if (!rect) return;
651
+ engine.handlePointerMove(e.clientX - rect.left, e.clientY - rect.top, {
652
+ shift: e.shiftKey,
653
+ ctrl: e.ctrlKey,
654
+ alt: e.altKey,
655
+ meta: e.metaKey
656
+ });
657
+ }, [engine]);
658
+ const onCanvasPointerUp = useCallback((e) => {
659
+ if (containerRef.current?.hasPointerCapture(e.pointerId)) containerRef.current.releasePointerCapture(e.pointerId);
660
+ engine.handlePointerUp();
661
+ }, [engine]);
662
+ useEffect(() => {
663
+ let rafId;
664
+ let running = true;
665
+ function loop() {
666
+ if (!running) return;
667
+ if (engine.flushIfDirty()) {
668
+ const camera = engine.getCamera();
669
+ const changes = engine.getFrameChanges();
670
+ if (cameraLayerRef.current) cameraLayerRef.current.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
671
+ const cursor = engine.world.getResource(CursorResource).cursor;
672
+ if (containerRef.current && containerRef.current.style.cursor !== cursor) containerRef.current.style.cursor = cursor;
673
+ const profiler = engine.profiler;
674
+ const profilerOn = profiler.isEnabled();
675
+ let selectionFramesDrawn = 0;
676
+ let snapGuidesDrawn = 0;
677
+ let spacingIndicatorsDrawn = 0;
678
+ if (gridRendererRef.current) gridRendererRef.current.getWebGLRenderer().info.reset();
679
+ if (gridRendererRef.current) {
680
+ profiler.beginWebGL("grid");
681
+ gridRendererRef.current.render(camera.x, camera.y, camera.zoom);
682
+ profiler.endWebGL("grid");
683
+ }
684
+ if (selectionRendererRef.current && gridRendererRef.current) {
685
+ const selected = engine.getSelectedEntities();
686
+ const selBounds = [];
687
+ for (const id of selected) {
688
+ if (!engine.has(id, SelectionFrame)) continue;
689
+ const wb = engine.get(id, WorldBounds);
690
+ if (wb) selBounds.push({
691
+ x: wb.worldX,
692
+ y: wb.worldY,
693
+ width: wb.worldWidth,
694
+ height: wb.worldHeight
695
+ });
696
+ }
697
+ const hovId = engine.getHoveredEntity();
698
+ let hovBounds = null;
699
+ if (hovId !== null && engine.has(hovId, SelectionFrame)) {
700
+ const wb = engine.get(hovId, WorldBounds);
701
+ if (wb) hovBounds = {
702
+ x: wb.worldX,
703
+ y: wb.worldY,
704
+ width: wb.worldWidth,
705
+ height: wb.worldHeight
706
+ };
707
+ }
708
+ const snapGuides = engine.getSnapGuides();
709
+ const equalSpacing = engine.getEqualSpacing();
710
+ selectionFramesDrawn = selBounds.length + (hovBounds ? 1 : 0);
711
+ snapGuidesDrawn = snapGuides.length;
712
+ spacingIndicatorsDrawn = equalSpacing.length;
713
+ profiler.beginWebGL("selection");
714
+ selectionRendererRef.current.render(gridRendererRef.current.getWebGLRenderer(), camera.x, camera.y, camera.zoom, selBounds, hovBounds, snapGuides, equalSpacing);
715
+ profiler.endWebGL("selection");
716
+ }
717
+ if (profilerOn && gridRendererRef.current) {
718
+ const info = gridRendererRef.current.getWebGLRenderer().info;
719
+ profiler.recordWebGLStats({
720
+ drawCalls: info.render.calls,
721
+ triangles: info.render.triangles,
722
+ selectionFrames: selectionFramesDrawn,
723
+ snapGuides: snapGuidesDrawn,
724
+ spacingIndicators: spacingIndicatorsDrawn,
725
+ domPositionsUpdated: changes.positionsChanged.length
726
+ });
727
+ }
728
+ for (const entityId of changes.positionsChanged) {
729
+ const el = slotRefs.current.get(entityId);
730
+ if (!el) continue;
731
+ const wb = engine.get(entityId, WorldBounds);
732
+ if (!wb) continue;
733
+ el.style.transform = `translate(${wb.worldX}px, ${wb.worldY}px)`;
734
+ el.style.width = `${wb.worldWidth}px`;
735
+ el.style.height = `${wb.worldHeight}px`;
736
+ }
737
+ if (changes.entered.length > 0 || changes.exited.length > 0) setVisibleEntities(engine.getVisibleEntities().map((v) => v.entityId));
738
+ if (changes.selectionChanged && onSelectionChangeRef.current) onSelectionChangeRef.current(engine.getSelectedEntities());
739
+ if (changes.cameraChanged && onCameraChangeRef.current) onCameraChangeRef.current({
740
+ x: camera.x,
741
+ y: camera.y,
742
+ zoom: camera.zoom
743
+ });
744
+ if (changes.navigationChanged && onNavigationChangeRef.current) {
745
+ const navStack = engine.world.getResource(NavigationStackResource);
746
+ const depth = navStack.frames.length - 1;
747
+ const containerId = navStack.frames[navStack.frames.length - 1].containerId;
748
+ onNavigationChangeRef.current(depth, containerId);
749
+ }
750
+ }
751
+ rafId = requestAnimationFrame(loop);
752
+ }
753
+ engine.tick();
754
+ const visible = engine.getVisibleEntities();
755
+ setVisibleEntities(visible.map((v) => v.entityId));
756
+ const camera = engine.getCamera();
757
+ if (cameraLayerRef.current) cameraLayerRef.current.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
758
+ if (gridRendererRef.current) gridRendererRef.current.render(camera.x, camera.y, camera.zoom);
759
+ for (const v of visible) {
760
+ const el = slotRefs.current.get(v.entityId);
761
+ if (!el) continue;
762
+ el.style.transform = `translate(${v.worldX}px, ${v.worldY}px)`;
763
+ el.style.width = `${v.worldWidth}px`;
764
+ el.style.height = `${v.worldHeight}px`;
765
+ }
766
+ rafId = requestAnimationFrame(loop);
767
+ return () => {
768
+ running = false;
769
+ cancelAnimationFrame(rafId);
770
+ };
771
+ }, [engine]);
772
+ useLayoutEffect(() => {
773
+ for (const entityId of visibleEntities) {
774
+ const el = slotRefs.current.get(entityId);
775
+ if (!el) continue;
776
+ const wb = engine.get(entityId, WorldBounds);
777
+ if (!wb) continue;
778
+ el.style.transform = `translate(${wb.worldX}px, ${wb.worldY}px)`;
779
+ el.style.width = `${wb.worldWidth}px`;
780
+ el.style.height = `${wb.worldHeight}px`;
781
+ }
782
+ }, [visibleEntities, engine]);
783
+ const { domEntities, webglEntities } = useMemo(() => {
784
+ const dom = [];
785
+ const webgl = [];
786
+ for (const id of visibleEntities) if (engine.get(id, Widget)?.surface === "webgl") webgl.push(id);
787
+ else dom.push(id);
788
+ return {
789
+ domEntities: dom,
790
+ webglEntities: webgl
791
+ };
792
+ }, [visibleEntities, engine]);
793
+ return /* @__PURE__ */ jsx(EngineProvider, {
794
+ value: engine,
795
+ children: /* @__PURE__ */ jsx(ContainerRefProvider, {
796
+ value: containerRef,
797
+ children: /* @__PURE__ */ jsx(WidgetProvider, {
798
+ engine,
799
+ children: /* @__PURE__ */ jsxs("div", {
800
+ ref: containerRef,
801
+ className: `relative overflow-hidden ${className ?? ""}`,
802
+ style: {
803
+ ...style,
804
+ touchAction: "none",
805
+ backgroundColor: "var(--canvas-bg, #fafafa)"
806
+ },
807
+ onPointerDown: onCanvasPointerDown,
808
+ onPointerMove: onCanvasPointerMove,
809
+ onPointerUp: onCanvasPointerUp,
810
+ children: [
811
+ /* @__PURE__ */ jsx("canvas", {
812
+ ref: webglCanvasRef,
813
+ className: "absolute inset-0 pointer-events-none"
814
+ }),
815
+ webglEntities.length > 0 && /* @__PURE__ */ jsx(WebGLWidgetBridge, {
816
+ engine,
817
+ entities: webglEntities
818
+ }),
819
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 pointer-events-none" }),
820
+ /* @__PURE__ */ jsxs("div", {
821
+ ref: cameraLayerRef,
822
+ className: "absolute left-0 top-0 origin-top-left will-change-transform",
823
+ children: [domEntities.map((entityId) => /* @__PURE__ */ jsx(WidgetSlot, {
824
+ entityId,
825
+ slotRef: registerSlotRef
826
+ }, entityId)), webglEntities.map((entityId) => /* @__PURE__ */ jsx(SelectionOverlaySlot, {
827
+ entityId,
828
+ slotRef: registerSlotRef
829
+ }, entityId))]
830
+ }),
831
+ children
832
+ ]
833
+ })
834
+ })
835
+ })
836
+ });
837
+ });
838
+ /** Bridge component — reads widget resolver from context and passes to WebGLWidgetLayer */
839
+ function WebGLWidgetBridge({ engine, entities }) {
840
+ const resolver = useWidgetResolver();
841
+ const resolve = useCallback((entityId) => {
842
+ if (!resolver) return null;
843
+ return resolver(entityId, engine.get(entityId, Widget)?.type ?? "");
844
+ }, [resolver, engine]);
845
+ if (!resolver) return null;
846
+ return /* @__PURE__ */ jsx(WebGLWidgetLayer, {
847
+ engine,
848
+ entities,
849
+ resolve
850
+ });
851
+ }
852
+ //#endregion
853
+ export { Active, BreakpointConfigResource, CameraResource, Card, CardFrame, CardPresetsResource, Children, CommandBuffer, Container, CursorHint, CursorResource, DEFAULT_GRID_CONFIG, DEFAULT_SELECTION_CONFIG, Draggable, Dragging, HandleSet, Hitbox, InfiniteCanvas, InteractionRole, Locked, MoveCommand, NavigationStackResource, Parent, Resizable, ResizeCommand, Selectable, Selected, SelectionFrame, SetComponentCommand, Transform2D, ViewportResource, Visible, Widget, WidgetBreakpoint, WidgetData, WidgetProvider, WidgetResolverProvider, WorldBounds, ZIndex, ZoomConfigResource, clamp, createArchetypeRegistry, createCardWidget, createGeometryCardWidget, createLayoutEngine, createWidgetRegistry, intersectsAABB, isR3FWidget, pointInAABB, screenToWorld, useAllEntities, useBreakpoint, useCamera, useChildren, useComponent, useContainerRef, useEntityComponents, useEntityTags, useIsSelected, useLayoutEngine, useQuery, useRegisteredComponents, useRegisteredTags, useResource, useTag, useTaggedEntities, useUpdateWidget, useWidgetData, useWidgetResolver, worldBoundsToAABB, worldToScreen };
854
+
855
+ //# sourceMappingURL=index.mjs.map