@jamesyong42/infinite-canvas 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +186 -44
  2. package/dist/SelectionRenderer-DRtwHWJ0.d.cts +102 -0
  3. package/dist/SelectionRenderer-rGYPadnn.d.ts +102 -0
  4. package/dist/advanced.cjs +26 -26
  5. package/dist/advanced.cjs.map +1 -1
  6. package/dist/advanced.d.cts +23 -27
  7. package/dist/advanced.d.ts +23 -27
  8. package/dist/advanced.js +3 -3
  9. package/dist/advanced.js.map +1 -1
  10. package/dist/chunk-2KBYGER3.cjs +336 -0
  11. package/dist/chunk-2KBYGER3.cjs.map +1 -0
  12. package/dist/{chunk-OFQ75B4X.cjs → chunk-FUPKRQB2.cjs} +1653 -1799
  13. package/dist/chunk-FUPKRQB2.cjs.map +1 -0
  14. package/dist/chunk-NILAZG6O.js +292 -0
  15. package/dist/chunk-NILAZG6O.js.map +1 -0
  16. package/dist/{chunk-JT3KDQYX.js → chunk-W2ZNA7HP.js} +1411 -1522
  17. package/dist/chunk-W2ZNA7HP.js.map +1 -0
  18. package/dist/devtools.cjs +628 -0
  19. package/dist/devtools.cjs.map +1 -0
  20. package/dist/devtools.d.cts +19 -0
  21. package/dist/devtools.d.ts +19 -0
  22. package/dist/devtools.js +626 -0
  23. package/dist/devtools.js.map +1 -0
  24. package/dist/{context-BI_YakHi.d.ts → engine-DqgJ82tq.d.cts} +335 -221
  25. package/dist/{context-C6VM7KNh.d.cts → engine-DqgJ82tq.d.ts} +335 -221
  26. package/dist/index.cjs +149 -140
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +77 -50
  29. package/dist/index.d.ts +77 -50
  30. package/dist/index.js +14 -35
  31. package/dist/index.js.map +1 -1
  32. package/package.json +13 -8
  33. package/dist/chunk-CH3TR4LF.js +0 -475
  34. package/dist/chunk-CH3TR4LF.js.map +0 -1
  35. package/dist/chunk-JT3KDQYX.js.map +0 -1
  36. package/dist/chunk-OFQ75B4X.cjs.map +0 -1
  37. package/dist/chunk-TX3ZABAK.cjs +0 -482
  38. package/dist/chunk-TX3ZABAK.cjs.map +0 -1
  39. package/dist/ecs.cjs +0 -32
  40. package/dist/ecs.cjs.map +0 -1
  41. package/dist/ecs.d.cts +0 -49
  42. package/dist/ecs.d.ts +0 -49
  43. package/dist/ecs.js +0 -3
  44. package/dist/ecs.js.map +0 -1
  45. package/dist/profiler-DnHBllDV.d.cts +0 -165
  46. package/dist/profiler-DnHBllDV.d.ts +0 -165
package/README.md CHANGED
@@ -18,6 +18,7 @@ Build Figma-style infinite canvases in React -- drag, resize, snap, zoom, nested
18
18
  - **Hierarchical navigation** -- Enter and exit nested containers with camera state preservation
19
19
  - **ECS architecture** -- Extensible via custom components, tags, and systems with topologically-sorted scheduling
20
20
  - **Performance** -- SDF shaders for grid and selection chrome, RBush spatial indexing, viewport culling, per-system profiling
21
+ - **Live ECS editor** -- Drop-in `<EcsDevtools>` panel for spawning, inspecting, and editing components and tags at runtime
21
22
  - **Dark mode** -- Full dark mode support across canvas, widgets, and UI chrome
22
23
 
23
24
  ## Quick Start
@@ -30,39 +31,42 @@ npm install three @react-three/fiber
30
31
 
31
32
  ```tsx
32
33
  import { useMemo } from 'react';
34
+ import type { DomWidget, EntityId } from '@jamesyong42/infinite-canvas';
33
35
  import { createLayoutEngine, InfiniteCanvas, useWidgetData } from '@jamesyong42/infinite-canvas';
36
+ import { z } from 'zod';
34
37
 
35
- function MyCard({ entityId }) {
36
- const data = useWidgetData(entityId);
38
+ const schema = z.object({ title: z.string().default('Card') });
39
+ type CardData = z.infer<typeof schema>;
40
+
41
+ function MyCardView({ entityId }: { entityId: EntityId }) {
42
+ const data = useWidgetData<CardData>(entityId);
37
43
  return <div style={{ padding: 16, background: 'white', borderRadius: 8 }}>{data.title}</div>;
38
44
  }
39
45
 
40
- const widgets = [
41
- { type: 'card', component: MyCard, defaultSize: { width: 250, height: 180 } },
42
- ];
46
+ const MyCard: DomWidget<CardData> = {
47
+ type: 'card',
48
+ schema,
49
+ defaultData: { title: 'Card' },
50
+ defaultSize: { width: 250, height: 180 },
51
+ component: MyCardView,
52
+ };
43
53
 
44
54
  export default function App() {
45
55
  const engine = useMemo(() => {
46
- const e = createLayoutEngine({ zoom: { min: 0.05, max: 8 } });
47
- e.addWidget({
48
- type: 'card',
49
- position: { x: 100, y: 100 },
50
- size: { width: 250, height: 180 },
51
- data: { title: 'Hello World' },
56
+ const e = createLayoutEngine({
57
+ zoom: { min: 0.05, max: 8 },
58
+ widgets: [MyCard],
52
59
  });
60
+ e.spawn('card', { at: { x: 100, y: 100 }, data: { title: 'Hello World' } });
53
61
  return e;
54
62
  }, []);
55
63
 
56
- return (
57
- <InfiniteCanvas
58
- engine={engine}
59
- widgets={widgets}
60
- style={{ width: '100vw', height: '100vh' }}
61
- />
62
- );
64
+ return <InfiniteCanvas engine={engine} style={{ width: '100vw', height: '100vh' }} />;
63
65
  }
64
66
  ```
65
67
 
68
+ Widgets declare a **schema** (any [Standard Schema v1](https://standardschema.dev)-compatible validator — Zod 3.24+, Valibot, ArkType) and **default data**. Entities are spawned by `engine.spawn(archetypeOrWidgetId, options)`. For widgets without a custom archetype, the engine synthesizes a default one that makes the entity selectable, draggable, and resizable.
69
+
66
70
  ## Package
67
71
 
68
72
  Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exposes three entry points:
@@ -70,8 +74,10 @@ Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exp
70
74
  | Import | Purpose |
71
75
  |--------|---------|
72
76
  | `@jamesyong42/infinite-canvas` | Main API -- `<InfiniteCanvas>`, `createLayoutEngine`, hooks, built-in components |
73
- | `@jamesyong42/infinite-canvas/ecs` | ECS primitives for advanced users (`defineComponent`, `defineSystem`, `World`) |
74
77
  | `@jamesyong42/infinite-canvas/advanced` | WebGL renderers, serialization, profiler, spatial index |
78
+ | `@jamesyong42/infinite-canvas/devtools` | `<EcsDevtools>` live ECS editor (see [Devtools](#devtools)) |
79
+
80
+ The underlying ECS primitives (`defineComponent`, `defineSystem`, `World`, `SystemScheduler`) live in a separate package: [**`@jamesyong42/reactive-ecs`**](https://github.com/jamesyong-42/reactive-ecs).
75
81
 
76
82
  ## Why This Library?
77
83
 
@@ -99,14 +105,18 @@ Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exp
99
105
  | `useQuery(...types)` | Entity IDs matching component/tag types |
100
106
  | `useTaggedEntities(type)` | All entity IDs with a specific tag |
101
107
  | `useResource<T>(type)` | Read an ECS resource reactively |
108
+ | `useAllEntities()` | Every live entity ID (reactive on create/destroy) |
109
+ | `useEntityComponents(entityId)` | `ComponentType[]` currently on an entity |
110
+ | `useEntityTags(entityId)` | `TagType[]` currently on an entity |
111
+ | `useRegisteredComponents()` | Every `ComponentType` the world has observed |
112
+ | `useRegisteredTags()` | Every `TagType` the world has observed |
102
113
  | `useLayoutEngine()` | Access the `LayoutEngine` instance from context |
103
114
 
104
115
  ### InfiniteCanvas Props
105
116
 
106
117
  | Prop | Type | Description |
107
118
  |------|------|-------------|
108
- | `engine` | `LayoutEngine` | Engine instance (required) |
109
- | `widgets` | `WidgetDef[]` | Widget type definitions |
119
+ | `engine` | `LayoutEngine` | Engine instance (required) -- widgets and archetypes are registered on the engine, not passed as props |
110
120
  | `grid` | `Partial<GridConfig> \| false` | Grid configuration, or `false` to disable |
111
121
  | `selection` | `Partial<SelectionConfig>` | Selection style overrides |
112
122
  | `onSelectionChange` | `(ids: EntityId[]) => void` | Called when selected entities change |
@@ -118,10 +128,10 @@ Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exp
118
128
 
119
129
  ## Widget Development
120
130
 
121
- Widgets are React components that receive an `entityId` prop and use hooks to read/write ECS data:
131
+ A widget is a self-contained plugin: a schema describing its data, a default data object, and a React view. Export it as a `DomWidget<T>` (or `R3FWidget<T>` see below) and register it on the engine.
122
132
 
123
133
  ```tsx
124
- import type { WidgetProps } from '@jamesyong42/infinite-canvas';
134
+ import type { DomWidget, EntityId } from '@jamesyong42/infinite-canvas';
125
135
  import {
126
136
  Transform2D,
127
137
  useBreakpoint,
@@ -130,33 +140,103 @@ import {
130
140
  useUpdateWidget,
131
141
  useWidgetData,
132
142
  } from '@jamesyong42/infinite-canvas';
143
+ import { z } from 'zod';
133
144
 
134
- function MyWidget({ entityId }: WidgetProps) {
135
- const data = useWidgetData(entityId); // custom widget data
136
- const breakpoint = useBreakpoint(entityId); // 'micro' | 'compact' | 'normal' | 'expanded' | 'detailed'
137
- const isSelected = useIsSelected(entityId); // selection state
138
- const transform = useComponent(entityId, Transform2D); // position/size
139
- const updateWidget = useUpdateWidget(entityId); // update widget data
145
+ const schema = z.object({
146
+ title: z.string().default('Widget'),
147
+ note: z.string().default(''),
148
+ });
149
+ type MyWidgetData = z.infer<typeof schema>;
150
+
151
+ function MyWidgetView({ entityId }: { entityId: EntityId }) {
152
+ const data = useWidgetData<MyWidgetData>(entityId); // typed custom data
153
+ const breakpoint = useBreakpoint(entityId); // 'micro' | 'compact' | 'normal' | 'expanded' | 'detailed'
154
+ const isSelected = useIsSelected(entityId); // selection state
155
+ const transform = useComponent(entityId, Transform2D); // position/size
156
+ const updateWidget = useUpdateWidget(entityId); // patch widget data
140
157
 
141
158
  if (breakpoint === 'micro') return <div>...</div>; // minimal view
142
159
  if (breakpoint === 'compact') return <div>...</div>; // condensed view
143
160
  return <div>...</div>; // full view
144
161
  }
162
+
163
+ export const MyWidget: DomWidget<MyWidgetData> = {
164
+ type: 'my-widget',
165
+ schema,
166
+ defaultData: { title: 'Widget', note: '' },
167
+ defaultSize: { width: 280, height: 200 },
168
+ component: MyWidgetView,
169
+ };
170
+ ```
171
+
172
+ DOM widget components receive only `{ entityId }`. The outer slot div is sized by CSS, so read `Transform2D` via `useComponent` if you need width/height. Register the widget via `createLayoutEngine({ widgets: [MyWidget] })` or imperatively with `engine.registerWidget(MyWidget)`.
173
+
174
+ ## Archetypes
175
+
176
+ An **archetype** is a recipe for spawning an entity — a bundle of components and tags, optionally referencing a widget type. Every widget you register gets a default archetype automatically (with `Selectable`, `Draggable`, `Resizable`). Write a custom archetype when you need to attach extra behaviour like `Container` + `Children` for an enterable container, or skip the interactive defaults for a locked decoration.
177
+
178
+ ```tsx
179
+ import type { Archetype, DomWidget, EntityId } from '@jamesyong42/infinite-canvas';
180
+ import { Children, Container } from '@jamesyong42/infinite-canvas';
181
+
182
+ export const MyContainer: DomWidget<{ title: string }> = {
183
+ type: 'my-container',
184
+ schema,
185
+ defaultData: { title: 'Container' },
186
+ defaultSize: { width: 500, height: 350 },
187
+ component: MyContainerView,
188
+ };
189
+
190
+ export const MyContainerArchetype: Archetype = {
191
+ id: 'my-container',
192
+ widget: 'my-container',
193
+ components: [
194
+ [Container, { enterable: true }],
195
+ [Children, { ids: [] as EntityId[] }],
196
+ ],
197
+ };
198
+
199
+ // Use it:
200
+ const engine = createLayoutEngine({
201
+ widgets: [MyContainer],
202
+ archetypes: [MyContainerArchetype],
203
+ });
204
+ const id = engine.spawn('my-container', { at: { x: 50, y: 50 }, data: { title: 'Hello' } });
145
205
  ```
146
206
 
147
- The `WidgetProps` interface provides `entityId`, `width`, `height`, and `zoom`.
207
+ Spawning is uniform: `engine.spawn(id, options)`. If `id` matches an archetype, that archetype is used; otherwise the engine synthesizes a default from the widget. Options:
208
+
209
+ | Option | Description |
210
+ |--------|-------------|
211
+ | `at` | World-space position. Defaults to `{ x: 0, y: 0 }`. |
212
+ | `size` | Overrides the widget's `defaultSize`. |
213
+ | `data` | Patch merged into the widget's `defaultData`. |
214
+ | `zIndex` | Rendering + hit-test order. |
215
+ | `parent` | Parent entity id for nesting. |
216
+ | `rotation` | Initial rotation in radians. |
148
217
 
149
218
  ## WebGL Widgets (R3F)
150
219
 
151
- Register widgets with `surface: 'webgl'` to render 3D content via React Three Fiber:
220
+ Define an `R3FWidget<T>` with `surface: 'webgl'` to render 3D content via React Three Fiber. R3F widget views receive `{ entityId, width, height }` and render in local coordinates (origin at widget centre):
152
221
 
153
222
  ```tsx
223
+ import type { EntityId, R3FWidget } from '@jamesyong42/infinite-canvas';
154
224
  import { useFrame } from '@react-three/fiber';
155
225
  import { useRef } from 'react';
156
-
157
- function My3DWidget({ entityId, width, height }) {
158
- const meshRef = useRef();
159
- useFrame((_, delta) => { meshRef.current.rotation.y += delta; });
226
+ import { z } from 'zod';
227
+ import type { Mesh } from 'three';
228
+
229
+ const schema = z.object({ color: z.string().default('hotpink') });
230
+
231
+ function My3DView({
232
+ entityId,
233
+ width,
234
+ height,
235
+ }: { entityId: EntityId; width: number; height: number }) {
236
+ const meshRef = useRef<Mesh>(null);
237
+ useFrame((_, delta) => {
238
+ if (meshRef.current) meshRef.current.rotation.y += delta;
239
+ });
160
240
  return (
161
241
  <mesh ref={meshRef}>
162
242
  <boxGeometry args={[width * 0.5, height * 0.5, 50]} />
@@ -165,15 +245,20 @@ function My3DWidget({ entityId, width, height }) {
165
245
  );
166
246
  }
167
247
 
168
- <InfiniteCanvas
169
- engine={engine}
170
- widgets={[
171
- { type: 'my-3d', surface: 'webgl', component: My3DWidget, defaultSize: { width: 250, height: 250 } },
172
- ]}
173
- />
248
+ export const My3D: R3FWidget<z.infer<typeof schema>> = {
249
+ type: 'my-3d',
250
+ surface: 'webgl',
251
+ schema,
252
+ defaultData: { color: 'hotpink' },
253
+ defaultSize: { width: 250, height: 250 },
254
+ component: My3DView,
255
+ };
256
+
257
+ const engine = createLayoutEngine({ widgets: [My3D] });
258
+ engine.spawn('my-3d', { at: { x: 100, y: 100 } });
174
259
  ```
175
260
 
176
- WebGL widgets get a transparent R3F canvas layered between the grid and DOM layers. The R3F camera is synced with the engine camera every frame. Widget components receive `entityId`, `width`, and `height` props and work in centered local coordinates.
261
+ WebGL widgets get a transparent R3F canvas layered between the grid and DOM layers. The R3F camera is synced with the engine camera every frame.
177
262
 
178
263
  ## Configuration
179
264
 
@@ -255,6 +340,42 @@ deserializeWorld(engine.world, saved, componentTypes, tagTypes);
255
340
  engine.markDirty();
256
341
  ```
257
342
 
343
+ ## Devtools
344
+
345
+ A live ECS editor ships in `@jamesyong42/infinite-canvas/devtools`. Drop it in during development to spawn widgets, inspect entities, edit components, and toggle tags at runtime — FLECS Explorer-style, but driven by the live React tree.
346
+
347
+ ```tsx
348
+ import { InfiniteCanvas } from '@jamesyong42/infinite-canvas';
349
+ import { EcsDevtools } from '@jamesyong42/infinite-canvas/devtools';
350
+
351
+ function App() {
352
+ const engine = useMemo(() => createLayoutEngine({ widgets: [MyWidget] }), []);
353
+ const [showDevtools, setShowDevtools] = useState(false);
354
+
355
+ return (
356
+ <>
357
+ <InfiniteCanvas engine={engine} />
358
+ {showDevtools && <EcsDevtools engine={engine} onClose={() => setShowDevtools(false)} />}
359
+ </>
360
+ );
361
+ }
362
+ ```
363
+
364
+ What the panel does:
365
+
366
+ - **Spawn** any registered widget at the current viewport centre.
367
+ - **List** all widget entities (or all entities with `show all`).
368
+ - **Inspect** the canvas-selected entity: its components and tags.
369
+ - **Edit** component fields inline — primitive types get typed inputs, everything else falls back to a JSON input. `WidgetData.data` is edited field-by-field.
370
+ - **Add / remove** components and toggle tags without leaving the canvas.
371
+ - **Destroy** entities.
372
+
373
+ Pass `engine` as a prop when the devtools render outside the `<InfiniteCanvas>` subtree (the usual case, since the panel is typically absolute-positioned above the canvas). If rendered inside, the prop is optional — it reads from context.
374
+
375
+ Styling is self-contained (a single scoped `<style>` injected once, classnames prefixed `ic-ecs-`). Dark mode is auto via `prefers-color-scheme` or an ancestor `.dark` class. No stylesheet import required.
376
+
377
+ The devtools consume the same introspection primitives (`useAllEntities`, `useEntityComponents`, `useRegisteredComponents`, etc.) that are exported from the main entry point, so you can build your own inspector UI on top of them if you need something bespoke.
378
+
258
379
  ## Programmatic Control
259
380
 
260
381
  ### Camera
@@ -275,6 +396,27 @@ engine.redo();
275
396
  engine.markDirty();
276
397
  ```
277
398
 
399
+ ### Spawning & ECS mutation
400
+
401
+ Runtime spawning and component edits go through the engine so it can cascade handles and mark dirty in one step:
402
+
403
+ ```tsx
404
+ // Spawn at the viewport centre (sized from the widget/archetype default)
405
+ const id = engine.spawnAtCameraCenter('my-widget');
406
+
407
+ // Component mutation
408
+ engine.addComponent(id, Container, { enterable: true });
409
+ engine.removeComponent(id, Container);
410
+ engine.set(id, Transform2D, { x: 200 }); // partial merge
411
+ engine.addTag(id, Selected);
412
+ engine.removeTag(id, Draggable);
413
+
414
+ // Widget-aware introspection
415
+ engine.getSchemaFor(id); // Standard Schema for the widget's data, if declared
416
+ ```
417
+
418
+ All of these mark the engine dirty internally — no separate `markDirty` call needed.
419
+
278
420
  ### Imperative Handle
279
421
 
280
422
  Use a ref on `<InfiniteCanvas>` for imperative control from outside:
@@ -282,7 +424,7 @@ Use a ref on `<InfiniteCanvas>` for imperative control from outside:
282
424
  ```tsx
283
425
  const canvasRef = useRef<InfiniteCanvasHandle>(null);
284
426
 
285
- <InfiniteCanvas ref={canvasRef} engine={engine} widgets={widgets} />
427
+ <InfiniteCanvas ref={canvasRef} engine={engine} />
286
428
 
287
429
  // Later:
288
430
  canvasRef.current?.zoomToFit();
@@ -318,7 +460,7 @@ useEffect(() => {
318
460
  Define custom components and systems to extend the canvas:
319
461
 
320
462
  ```tsx
321
- import { defineComponent, defineSystem } from '@jamesyong42/infinite-canvas/ecs';
463
+ import { defineComponent, defineSystem } from '@jamesyong42/reactive-ecs';
322
464
  import { Visible } from '@jamesyong42/infinite-canvas';
323
465
 
324
466
  const Health = defineComponent('Health', { hp: 100, maxHp: 100 });
@@ -0,0 +1,102 @@
1
+ import * as react from 'react';
2
+ import { EntityId } from '@jamesyong42/reactive-ecs';
3
+ import { L as LayoutEngine, q as DomWidgetProps, R as R3FWidgetProps, _ as SnapGuide, s as EqualSpacingIndicator } from './engine-DqgJ82tq.cjs';
4
+ import * as THREE from 'three';
5
+
6
+ declare const EngineProvider: react.Provider<LayoutEngine | null>;
7
+ declare const ContainerRefProvider: react.Provider<react.RefObject<HTMLDivElement | null> | null>;
8
+ declare function useContainerRef(): React.RefObject<HTMLDivElement | null> | null;
9
+ /**
10
+ * Returns the LayoutEngine instance from the nearest InfiniteCanvas context.
11
+ * Throws if used outside an InfiniteCanvas provider.
12
+ */
13
+ declare function useLayoutEngine(): LayoutEngine;
14
+
15
+ /**
16
+ * Discriminated resolution of a widget by type. The surface determines which
17
+ * layer renders the component and with what prop shape.
18
+ */
19
+ type ResolvedWidget = {
20
+ surface: 'dom';
21
+ component: React.ComponentType<DomWidgetProps>;
22
+ } | {
23
+ surface: 'webgl';
24
+ component: React.ComponentType<R3FWidgetProps>;
25
+ };
26
+ type WidgetResolver = (entityId: EntityId, widgetType: string) => ResolvedWidget | null;
27
+ declare const WidgetResolverProvider: react.Provider<WidgetResolver | null>;
28
+ declare function useWidgetResolver(): WidgetResolver | null;
29
+
30
+ interface GridConfig {
31
+ /** World-unit spacings for up to 3 grid levels [fine, medium, coarse]. */
32
+ spacings: [number, number, number];
33
+ /** Dot RGB color as [r, g, b] in 0–1 range. */
34
+ dotColor: [number, number, number];
35
+ /** Base dot opacity multiplier (0–1). */
36
+ dotAlpha: number;
37
+ /** CSS-pixel range where a grid level fades in: [start, end]. */
38
+ fadeIn: [number, number];
39
+ /** CSS-pixel range where a grid level fades out: [start, end]. */
40
+ fadeOut: [number, number];
41
+ /** Dot radius range in CSS pixels [min, max]. Scaled by DPR internally. */
42
+ dotRadius: [number, number];
43
+ /** Per-level opacity weight: level i gets (base + i * step). */
44
+ levelWeight: [number, number];
45
+ }
46
+ declare const DEFAULT_GRID_CONFIG: GridConfig;
47
+ declare class GridRenderer {
48
+ private renderer;
49
+ private scene;
50
+ private camera;
51
+ private material;
52
+ private mesh;
53
+ constructor(canvas: HTMLCanvasElement);
54
+ /** Apply a (partial) grid config. Only provided fields are updated. */
55
+ setConfig(config: Partial<GridConfig>): void;
56
+ setSize(width: number, height: number, dpr?: number): void;
57
+ render(cameraX: number, cameraY: number, zoom: number): void;
58
+ dispose(): void;
59
+ /** Expose for future WebGL widget rendering */
60
+ getWebGLRenderer(): THREE.WebGLRenderer;
61
+ }
62
+
63
+ interface SelectionConfig {
64
+ /** Selection outline color [r,g,b] 0-1. Default: Figma blue. */
65
+ outlineColor: [number, number, number];
66
+ /** Selection outline width in screen px. */
67
+ outlineWidth: number;
68
+ /** Hover outline color [r,g,b] 0-1. */
69
+ hoverColor: [number, number, number];
70
+ /** Hover outline width in screen px. */
71
+ hoverWidth: number;
72
+ /** Handle size in screen px. */
73
+ handleSize: number;
74
+ /** Handle fill color [r,g,b] 0-1 (white). */
75
+ handleFill: [number, number, number];
76
+ /** Handle border color [r,g,b] 0-1 (same as outline). */
77
+ handleBorder: [number, number, number];
78
+ /** Handle border width in screen px. */
79
+ handleBorderWidth: number;
80
+ /** Group bbox dash length in screen px (0 = solid). */
81
+ groupDash: number;
82
+ }
83
+ declare const DEFAULT_SELECTION_CONFIG: SelectionConfig;
84
+ interface SelectionBounds {
85
+ x: number;
86
+ y: number;
87
+ width: number;
88
+ height: number;
89
+ }
90
+ declare class SelectionRenderer {
91
+ private material;
92
+ private mesh;
93
+ private scene;
94
+ private camera;
95
+ constructor();
96
+ setConfig(config: Partial<SelectionConfig>): void;
97
+ setSize(resolution: THREE.Vector2, dpr: number): void;
98
+ render(renderer: THREE.WebGLRenderer, cameraX: number, cameraY: number, zoom: number, selected: SelectionBounds[], hovered: SelectionBounds | null, guides?: SnapGuide[], spacings?: EqualSpacingIndicator[]): void;
99
+ dispose(): void;
100
+ }
101
+
102
+ export { ContainerRefProvider as C, DEFAULT_GRID_CONFIG as D, EngineProvider as E, GridRenderer as G, type ResolvedWidget as R, SelectionRenderer as S, WidgetResolverProvider as W, type GridConfig as a, type SelectionConfig as b, DEFAULT_SELECTION_CONFIG as c, type SelectionBounds as d, useLayoutEngine as e, useWidgetResolver as f, useContainerRef as u };
@@ -0,0 +1,102 @@
1
+ import * as react from 'react';
2
+ import { EntityId } from '@jamesyong42/reactive-ecs';
3
+ import { L as LayoutEngine, q as DomWidgetProps, R as R3FWidgetProps, _ as SnapGuide, s as EqualSpacingIndicator } from './engine-DqgJ82tq.js';
4
+ import * as THREE from 'three';
5
+
6
+ declare const EngineProvider: react.Provider<LayoutEngine | null>;
7
+ declare const ContainerRefProvider: react.Provider<react.RefObject<HTMLDivElement | null> | null>;
8
+ declare function useContainerRef(): React.RefObject<HTMLDivElement | null> | null;
9
+ /**
10
+ * Returns the LayoutEngine instance from the nearest InfiniteCanvas context.
11
+ * Throws if used outside an InfiniteCanvas provider.
12
+ */
13
+ declare function useLayoutEngine(): LayoutEngine;
14
+
15
+ /**
16
+ * Discriminated resolution of a widget by type. The surface determines which
17
+ * layer renders the component and with what prop shape.
18
+ */
19
+ type ResolvedWidget = {
20
+ surface: 'dom';
21
+ component: React.ComponentType<DomWidgetProps>;
22
+ } | {
23
+ surface: 'webgl';
24
+ component: React.ComponentType<R3FWidgetProps>;
25
+ };
26
+ type WidgetResolver = (entityId: EntityId, widgetType: string) => ResolvedWidget | null;
27
+ declare const WidgetResolverProvider: react.Provider<WidgetResolver | null>;
28
+ declare function useWidgetResolver(): WidgetResolver | null;
29
+
30
+ interface GridConfig {
31
+ /** World-unit spacings for up to 3 grid levels [fine, medium, coarse]. */
32
+ spacings: [number, number, number];
33
+ /** Dot RGB color as [r, g, b] in 0–1 range. */
34
+ dotColor: [number, number, number];
35
+ /** Base dot opacity multiplier (0–1). */
36
+ dotAlpha: number;
37
+ /** CSS-pixel range where a grid level fades in: [start, end]. */
38
+ fadeIn: [number, number];
39
+ /** CSS-pixel range where a grid level fades out: [start, end]. */
40
+ fadeOut: [number, number];
41
+ /** Dot radius range in CSS pixels [min, max]. Scaled by DPR internally. */
42
+ dotRadius: [number, number];
43
+ /** Per-level opacity weight: level i gets (base + i * step). */
44
+ levelWeight: [number, number];
45
+ }
46
+ declare const DEFAULT_GRID_CONFIG: GridConfig;
47
+ declare class GridRenderer {
48
+ private renderer;
49
+ private scene;
50
+ private camera;
51
+ private material;
52
+ private mesh;
53
+ constructor(canvas: HTMLCanvasElement);
54
+ /** Apply a (partial) grid config. Only provided fields are updated. */
55
+ setConfig(config: Partial<GridConfig>): void;
56
+ setSize(width: number, height: number, dpr?: number): void;
57
+ render(cameraX: number, cameraY: number, zoom: number): void;
58
+ dispose(): void;
59
+ /** Expose for future WebGL widget rendering */
60
+ getWebGLRenderer(): THREE.WebGLRenderer;
61
+ }
62
+
63
+ interface SelectionConfig {
64
+ /** Selection outline color [r,g,b] 0-1. Default: Figma blue. */
65
+ outlineColor: [number, number, number];
66
+ /** Selection outline width in screen px. */
67
+ outlineWidth: number;
68
+ /** Hover outline color [r,g,b] 0-1. */
69
+ hoverColor: [number, number, number];
70
+ /** Hover outline width in screen px. */
71
+ hoverWidth: number;
72
+ /** Handle size in screen px. */
73
+ handleSize: number;
74
+ /** Handle fill color [r,g,b] 0-1 (white). */
75
+ handleFill: [number, number, number];
76
+ /** Handle border color [r,g,b] 0-1 (same as outline). */
77
+ handleBorder: [number, number, number];
78
+ /** Handle border width in screen px. */
79
+ handleBorderWidth: number;
80
+ /** Group bbox dash length in screen px (0 = solid). */
81
+ groupDash: number;
82
+ }
83
+ declare const DEFAULT_SELECTION_CONFIG: SelectionConfig;
84
+ interface SelectionBounds {
85
+ x: number;
86
+ y: number;
87
+ width: number;
88
+ height: number;
89
+ }
90
+ declare class SelectionRenderer {
91
+ private material;
92
+ private mesh;
93
+ private scene;
94
+ private camera;
95
+ constructor();
96
+ setConfig(config: Partial<SelectionConfig>): void;
97
+ setSize(resolution: THREE.Vector2, dpr: number): void;
98
+ render(renderer: THREE.WebGLRenderer, cameraX: number, cameraY: number, zoom: number, selected: SelectionBounds[], hovered: SelectionBounds | null, guides?: SnapGuide[], spacings?: EqualSpacingIndicator[]): void;
99
+ dispose(): void;
100
+ }
101
+
102
+ export { ContainerRefProvider as C, DEFAULT_GRID_CONFIG as D, EngineProvider as E, GridRenderer as G, type ResolvedWidget as R, SelectionRenderer as S, WidgetResolverProvider as W, type GridConfig as a, type SelectionConfig as b, DEFAULT_SELECTION_CONFIG as c, type SelectionBounds as d, useLayoutEngine as e, useWidgetResolver as f, useContainerRef as u };
package/dist/advanced.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunkOFQ75B4X_cjs = require('./chunk-OFQ75B4X.cjs');
4
- require('./chunk-TX3ZABAK.cjs');
3
+ var chunkFUPKRQB2_cjs = require('./chunk-FUPKRQB2.cjs');
4
+ var chunk2KBYGER3_cjs = require('./chunk-2KBYGER3.cjs');
5
5
 
6
6
  // src/serialization.ts
7
7
  function serializeWorld(world, componentTypes, tagTypes, camera, navigationFrames) {
@@ -65,22 +65,22 @@ function deserializeWorld(world, doc, componentTypes, tagTypes) {
65
65
  }
66
66
  }
67
67
  for (const [_oldId, newId] of idMap) {
68
- const parent = world.getComponent(newId, chunkOFQ75B4X_cjs.Parent);
68
+ const parent = world.getComponent(newId, chunk2KBYGER3_cjs.Parent);
69
69
  if (parent && idMap.has(parent.id)) {
70
70
  const mappedId = idMap.get(parent.id);
71
71
  if (mappedId !== void 0) {
72
- world.setComponent(newId, chunkOFQ75B4X_cjs.Parent, { id: mappedId });
72
+ world.setComponent(newId, chunk2KBYGER3_cjs.Parent, { id: mappedId });
73
73
  }
74
74
  }
75
- const children = world.getComponent(newId, chunkOFQ75B4X_cjs.Children);
75
+ const children = world.getComponent(newId, chunk2KBYGER3_cjs.Children);
76
76
  if (children) {
77
- world.setComponent(newId, chunkOFQ75B4X_cjs.Children, {
77
+ world.setComponent(newId, chunk2KBYGER3_cjs.Children, {
78
78
  ids: children.ids.map((id) => idMap.get(id) ?? id)
79
79
  });
80
80
  }
81
- const handleSet = world.getComponent(newId, chunkOFQ75B4X_cjs.HandleSet);
81
+ const handleSet = world.getComponent(newId, chunk2KBYGER3_cjs.HandleSet);
82
82
  if (handleSet) {
83
- world.setComponent(newId, chunkOFQ75B4X_cjs.HandleSet, {
83
+ world.setComponent(newId, chunk2KBYGER3_cjs.HandleSet, {
84
84
  ids: handleSet.ids.map((id) => idMap.get(id) ?? id)
85
85
  });
86
86
  }
@@ -121,53 +121,53 @@ function serializeEntities(world, entityIds, componentTypes, tagTypes) {
121
121
  return result;
122
122
  }
123
123
 
124
- Object.defineProperty(exports, "ContainerRefProvider", {
125
- enumerable: true,
126
- get: function () { return chunkOFQ75B4X_cjs.ContainerRefProvider; }
127
- });
128
- Object.defineProperty(exports, "EngineProvider", {
129
- enumerable: true,
130
- get: function () { return chunkOFQ75B4X_cjs.EngineProvider; }
131
- });
132
124
  Object.defineProperty(exports, "GridRenderer", {
133
125
  enumerable: true,
134
- get: function () { return chunkOFQ75B4X_cjs.GridRenderer; }
126
+ get: function () { return chunkFUPKRQB2_cjs.GridRenderer; }
135
127
  });
136
128
  Object.defineProperty(exports, "Profiler", {
137
129
  enumerable: true,
138
- get: function () { return chunkOFQ75B4X_cjs.Profiler; }
130
+ get: function () { return chunkFUPKRQB2_cjs.Profiler; }
139
131
  });
140
132
  Object.defineProperty(exports, "SelectionOverlaySlot", {
141
133
  enumerable: true,
142
- get: function () { return chunkOFQ75B4X_cjs.SelectionOverlaySlot; }
134
+ get: function () { return chunkFUPKRQB2_cjs.SelectionOverlaySlot; }
143
135
  });
144
136
  Object.defineProperty(exports, "SelectionRenderer", {
145
137
  enumerable: true,
146
- get: function () { return chunkOFQ75B4X_cjs.SelectionRenderer; }
138
+ get: function () { return chunkFUPKRQB2_cjs.SelectionRenderer; }
147
139
  });
148
140
  Object.defineProperty(exports, "SpatialIndex", {
149
141
  enumerable: true,
150
- get: function () { return chunkOFQ75B4X_cjs.SpatialIndex; }
142
+ get: function () { return chunkFUPKRQB2_cjs.SpatialIndex; }
151
143
  });
152
144
  Object.defineProperty(exports, "SpatialIndexResource", {
153
145
  enumerable: true,
154
- get: function () { return chunkOFQ75B4X_cjs.SpatialIndexResource; }
146
+ get: function () { return chunkFUPKRQB2_cjs.SpatialIndexResource; }
155
147
  });
156
148
  Object.defineProperty(exports, "WebGLWidgetLayer", {
157
149
  enumerable: true,
158
- get: function () { return chunkOFQ75B4X_cjs.WebGLWidgetLayer; }
150
+ get: function () { return chunkFUPKRQB2_cjs.WebGLWidgetLayer; }
159
151
  });
160
152
  Object.defineProperty(exports, "WebGLWidgetSlot", {
161
153
  enumerable: true,
162
- get: function () { return chunkOFQ75B4X_cjs.WebGLWidgetSlot; }
154
+ get: function () { return chunkFUPKRQB2_cjs.WebGLWidgetSlot; }
163
155
  });
164
156
  Object.defineProperty(exports, "WidgetSlot", {
165
157
  enumerable: true,
166
- get: function () { return chunkOFQ75B4X_cjs.WidgetSlot; }
158
+ get: function () { return chunkFUPKRQB2_cjs.WidgetSlot; }
167
159
  });
168
160
  Object.defineProperty(exports, "computeSnapGuides", {
169
161
  enumerable: true,
170
- get: function () { return chunkOFQ75B4X_cjs.computeSnapGuides; }
162
+ get: function () { return chunkFUPKRQB2_cjs.computeSnapGuides; }
163
+ });
164
+ Object.defineProperty(exports, "ContainerRefProvider", {
165
+ enumerable: true,
166
+ get: function () { return chunk2KBYGER3_cjs.ContainerRefProvider; }
167
+ });
168
+ Object.defineProperty(exports, "EngineProvider", {
169
+ enumerable: true,
170
+ get: function () { return chunk2KBYGER3_cjs.EngineProvider; }
171
171
  });
172
172
  exports.deserializeWorld = deserializeWorld;
173
173
  exports.serializeEntities = serializeEntities;