@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.
- package/README.md +170 -9
- package/dist/SelectionRenderer-CR2PBQwx.d.cts +105 -0
- package/dist/SelectionRenderer-CR2PBQwx.d.cts.map +1 -0
- package/dist/SelectionRenderer-DlsBstAq.d.mts +105 -0
- package/dist/SelectionRenderer-DlsBstAq.d.mts.map +1 -0
- package/dist/WebGLWidgetLayer-BBMuwzHq.cjs +3560 -0
- package/dist/WebGLWidgetLayer-BBMuwzHq.cjs.map +1 -0
- package/dist/WebGLWidgetLayer-C3p1tnpm.mjs +3375 -0
- package/dist/WebGLWidgetLayer-C3p1tnpm.mjs.map +1 -0
- package/dist/advanced.cjs +110 -165
- package/dist/advanced.cjs.map +1 -1
- package/dist/advanced.d.cts +58 -40
- package/dist/advanced.d.cts.map +1 -0
- package/dist/advanced.d.mts +99 -0
- package/dist/advanced.d.mts.map +1 -0
- package/dist/advanced.mjs +105 -0
- package/dist/advanced.mjs.map +1 -0
- package/dist/devtools.cjs +654 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.cts +23 -0
- package/dist/devtools.d.cts.map +1 -0
- package/dist/devtools.d.mts +23 -0
- package/dist/devtools.d.mts.map +1 -0
- package/dist/devtools.mjs +652 -0
- package/dist/devtools.mjs.map +1 -0
- package/dist/engine-BfbvWXSk.d.mts +982 -0
- package/dist/engine-BfbvWXSk.d.mts.map +1 -0
- package/dist/engine-CCjuFMC-.d.cts +982 -0
- package/dist/engine-CCjuFMC-.d.cts.map +1 -0
- package/dist/hooks-BwY7rRHg.mjs +425 -0
- package/dist/hooks-BwY7rRHg.mjs.map +1 -0
- package/dist/hooks-DHShH86C.cjs +707 -0
- package/dist/hooks-DHShH86C.cjs.map +1 -0
- package/dist/index.cjs +909 -803
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +199 -67
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +258 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +855 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -15
- package/dist/SelectionRenderer-CeWSNZT8.d.cts +0 -891
- package/dist/SelectionRenderer-CeWSNZT8.d.ts +0 -891
- package/dist/advanced.d.ts +0 -81
- package/dist/advanced.js +0 -124
- package/dist/advanced.js.map +0 -1
- package/dist/chunk-VSHXWTJH.cjs +0 -3228
- package/dist/chunk-VSHXWTJH.cjs.map +0 -1
- package/dist/chunk-Z6JQQOWL.js +0 -3142
- package/dist/chunk-Z6JQQOWL.js.map +0 -1
- package/dist/index.d.ts +0 -126
- package/dist/index.js +0 -602
- 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
|