@jamesyong42/infinite-canvas 0.1.0 → 1.0.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 +123 -45
- package/dist/{context-BI_YakHi.d.ts → SelectionRenderer-CeWSNZT8.d.cts} +407 -207
- package/dist/{context-C6VM7KNh.d.cts → SelectionRenderer-CeWSNZT8.d.ts} +407 -207
- package/dist/advanced.cjs +19 -20
- package/dist/advanced.cjs.map +1 -1
- package/dist/advanced.d.cts +21 -27
- package/dist/advanced.d.ts +21 -27
- package/dist/advanced.js +2 -3
- package/dist/advanced.js.map +1 -1
- package/dist/{chunk-OFQ75B4X.cjs → chunk-VSHXWTJH.cjs} +2510 -2453
- package/dist/chunk-VSHXWTJH.cjs.map +1 -0
- package/dist/{chunk-JT3KDQYX.js → chunk-Z6JQQOWL.js} +2506 -2452
- package/dist/chunk-Z6JQQOWL.js.map +1 -0
- package/dist/index.cjs +94 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -50
- package/dist/index.d.ts +52 -50
- package/dist/index.js +12 -35
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/dist/chunk-CH3TR4LF.js +0 -475
- package/dist/chunk-CH3TR4LF.js.map +0 -1
- package/dist/chunk-JT3KDQYX.js.map +0 -1
- package/dist/chunk-OFQ75B4X.cjs.map +0 -1
- package/dist/chunk-TX3ZABAK.cjs +0 -482
- package/dist/chunk-TX3ZABAK.cjs.map +0 -1
- package/dist/ecs.cjs +0 -32
- package/dist/ecs.cjs.map +0 -1
- package/dist/ecs.d.cts +0 -49
- package/dist/ecs.d.ts +0 -49
- package/dist/ecs.js +0 -3
- package/dist/ecs.js.map +0 -1
- package/dist/profiler-DnHBllDV.d.cts +0 -165
- package/dist/profiler-DnHBllDV.d.ts +0 -165
package/README.md
CHANGED
|
@@ -30,49 +30,53 @@ npm install three @react-three/fiber
|
|
|
30
30
|
|
|
31
31
|
```tsx
|
|
32
32
|
import { useMemo } from 'react';
|
|
33
|
+
import type { DomWidget, EntityId } from '@jamesyong42/infinite-canvas';
|
|
33
34
|
import { createLayoutEngine, InfiniteCanvas, useWidgetData } from '@jamesyong42/infinite-canvas';
|
|
35
|
+
import { z } from 'zod';
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const schema = z.object({ title: z.string().default('Card') });
|
|
38
|
+
type CardData = z.infer<typeof schema>;
|
|
39
|
+
|
|
40
|
+
function MyCardView({ entityId }: { entityId: EntityId }) {
|
|
41
|
+
const data = useWidgetData<CardData>(entityId);
|
|
37
42
|
return <div style={{ padding: 16, background: 'white', borderRadius: 8 }}>{data.title}</div>;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
const MyCard: DomWidget<CardData> = {
|
|
46
|
+
type: 'card',
|
|
47
|
+
schema,
|
|
48
|
+
defaultData: { title: 'Card' },
|
|
49
|
+
defaultSize: { width: 250, height: 180 },
|
|
50
|
+
component: MyCardView,
|
|
51
|
+
};
|
|
43
52
|
|
|
44
53
|
export default function App() {
|
|
45
54
|
const engine = useMemo(() => {
|
|
46
|
-
const e = createLayoutEngine({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
position: { x: 100, y: 100 },
|
|
50
|
-
size: { width: 250, height: 180 },
|
|
51
|
-
data: { title: 'Hello World' },
|
|
55
|
+
const e = createLayoutEngine({
|
|
56
|
+
zoom: { min: 0.05, max: 8 },
|
|
57
|
+
widgets: [MyCard],
|
|
52
58
|
});
|
|
59
|
+
e.spawn('card', { at: { x: 100, y: 100 }, data: { title: 'Hello World' } });
|
|
53
60
|
return e;
|
|
54
61
|
}, []);
|
|
55
62
|
|
|
56
|
-
return
|
|
57
|
-
<InfiniteCanvas
|
|
58
|
-
engine={engine}
|
|
59
|
-
widgets={widgets}
|
|
60
|
-
style={{ width: '100vw', height: '100vh' }}
|
|
61
|
-
/>
|
|
62
|
-
);
|
|
63
|
+
return <InfiniteCanvas engine={engine} style={{ width: '100vw', height: '100vh' }} />;
|
|
63
64
|
}
|
|
64
65
|
```
|
|
65
66
|
|
|
67
|
+
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.
|
|
68
|
+
|
|
66
69
|
## Package
|
|
67
70
|
|
|
68
|
-
Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exposes
|
|
71
|
+
Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exposes two entry points:
|
|
69
72
|
|
|
70
73
|
| Import | Purpose |
|
|
71
74
|
|--------|---------|
|
|
72
75
|
| `@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
76
|
| `@jamesyong42/infinite-canvas/advanced` | WebGL renderers, serialization, profiler, spatial index |
|
|
75
77
|
|
|
78
|
+
The underlying ECS primitives (`defineComponent`, `defineSystem`, `World`, `SystemScheduler`) live in a separate package: [**`@jamesyong42/reactive-ecs`**](https://github.com/jamesyong-42/reactive-ecs).
|
|
79
|
+
|
|
76
80
|
## Why This Library?
|
|
77
81
|
|
|
78
82
|
| | Infinite Canvas | React Flow | Konva | Excalidraw |
|
|
@@ -105,8 +109,7 @@ Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exp
|
|
|
105
109
|
|
|
106
110
|
| Prop | Type | Description |
|
|
107
111
|
|------|------|-------------|
|
|
108
|
-
| `engine` | `LayoutEngine` | Engine instance (required) |
|
|
109
|
-
| `widgets` | `WidgetDef[]` | Widget type definitions |
|
|
112
|
+
| `engine` | `LayoutEngine` | Engine instance (required) -- widgets and archetypes are registered on the engine, not passed as props |
|
|
110
113
|
| `grid` | `Partial<GridConfig> \| false` | Grid configuration, or `false` to disable |
|
|
111
114
|
| `selection` | `Partial<SelectionConfig>` | Selection style overrides |
|
|
112
115
|
| `onSelectionChange` | `(ids: EntityId[]) => void` | Called when selected entities change |
|
|
@@ -118,10 +121,10 @@ Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exp
|
|
|
118
121
|
|
|
119
122
|
## Widget Development
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
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
125
|
|
|
123
126
|
```tsx
|
|
124
|
-
import type {
|
|
127
|
+
import type { DomWidget, EntityId } from '@jamesyong42/infinite-canvas';
|
|
125
128
|
import {
|
|
126
129
|
Transform2D,
|
|
127
130
|
useBreakpoint,
|
|
@@ -130,33 +133,103 @@ import {
|
|
|
130
133
|
useUpdateWidget,
|
|
131
134
|
useWidgetData,
|
|
132
135
|
} from '@jamesyong42/infinite-canvas';
|
|
136
|
+
import { z } from 'zod';
|
|
133
137
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const schema = z.object({
|
|
139
|
+
title: z.string().default('Widget'),
|
|
140
|
+
note: z.string().default(''),
|
|
141
|
+
});
|
|
142
|
+
type MyWidgetData = z.infer<typeof schema>;
|
|
143
|
+
|
|
144
|
+
function MyWidgetView({ entityId }: { entityId: EntityId }) {
|
|
145
|
+
const data = useWidgetData<MyWidgetData>(entityId); // typed custom data
|
|
146
|
+
const breakpoint = useBreakpoint(entityId); // 'micro' | 'compact' | 'normal' | 'expanded' | 'detailed'
|
|
147
|
+
const isSelected = useIsSelected(entityId); // selection state
|
|
148
|
+
const transform = useComponent(entityId, Transform2D); // position/size
|
|
149
|
+
const updateWidget = useUpdateWidget(entityId); // patch widget data
|
|
140
150
|
|
|
141
151
|
if (breakpoint === 'micro') return <div>...</div>; // minimal view
|
|
142
152
|
if (breakpoint === 'compact') return <div>...</div>; // condensed view
|
|
143
153
|
return <div>...</div>; // full view
|
|
144
154
|
}
|
|
155
|
+
|
|
156
|
+
export const MyWidget: DomWidget<MyWidgetData> = {
|
|
157
|
+
type: 'my-widget',
|
|
158
|
+
schema,
|
|
159
|
+
defaultData: { title: 'Widget', note: '' },
|
|
160
|
+
defaultSize: { width: 280, height: 200 },
|
|
161
|
+
component: MyWidgetView,
|
|
162
|
+
};
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
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)`.
|
|
166
|
+
|
|
167
|
+
## Archetypes
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import type { Archetype, DomWidget, EntityId } from '@jamesyong42/infinite-canvas';
|
|
173
|
+
import { Children, Container } from '@jamesyong42/infinite-canvas';
|
|
174
|
+
|
|
175
|
+
export const MyContainer: DomWidget<{ title: string }> = {
|
|
176
|
+
type: 'my-container',
|
|
177
|
+
schema,
|
|
178
|
+
defaultData: { title: 'Container' },
|
|
179
|
+
defaultSize: { width: 500, height: 350 },
|
|
180
|
+
component: MyContainerView,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const MyContainerArchetype: Archetype = {
|
|
184
|
+
id: 'my-container',
|
|
185
|
+
widget: 'my-container',
|
|
186
|
+
components: [
|
|
187
|
+
[Container, { enterable: true }],
|
|
188
|
+
[Children, { ids: [] as EntityId[] }],
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Use it:
|
|
193
|
+
const engine = createLayoutEngine({
|
|
194
|
+
widgets: [MyContainer],
|
|
195
|
+
archetypes: [MyContainerArchetype],
|
|
196
|
+
});
|
|
197
|
+
const id = engine.spawn('my-container', { at: { x: 50, y: 50 }, data: { title: 'Hello' } });
|
|
145
198
|
```
|
|
146
199
|
|
|
147
|
-
|
|
200
|
+
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:
|
|
201
|
+
|
|
202
|
+
| Option | Description |
|
|
203
|
+
|--------|-------------|
|
|
204
|
+
| `at` | World-space position. Defaults to `{ x: 0, y: 0 }`. |
|
|
205
|
+
| `size` | Overrides the widget's `defaultSize`. |
|
|
206
|
+
| `data` | Patch merged into the widget's `defaultData`. |
|
|
207
|
+
| `zIndex` | Rendering + hit-test order. |
|
|
208
|
+
| `parent` | Parent entity id for nesting. |
|
|
209
|
+
| `rotation` | Initial rotation in radians. |
|
|
148
210
|
|
|
149
211
|
## WebGL Widgets (R3F)
|
|
150
212
|
|
|
151
|
-
|
|
213
|
+
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
214
|
|
|
153
215
|
```tsx
|
|
216
|
+
import type { EntityId, R3FWidget } from '@jamesyong42/infinite-canvas';
|
|
154
217
|
import { useFrame } from '@react-three/fiber';
|
|
155
218
|
import { useRef } from 'react';
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
219
|
+
import { z } from 'zod';
|
|
220
|
+
import type { Mesh } from 'three';
|
|
221
|
+
|
|
222
|
+
const schema = z.object({ color: z.string().default('hotpink') });
|
|
223
|
+
|
|
224
|
+
function My3DView({
|
|
225
|
+
entityId,
|
|
226
|
+
width,
|
|
227
|
+
height,
|
|
228
|
+
}: { entityId: EntityId; width: number; height: number }) {
|
|
229
|
+
const meshRef = useRef<Mesh>(null);
|
|
230
|
+
useFrame((_, delta) => {
|
|
231
|
+
if (meshRef.current) meshRef.current.rotation.y += delta;
|
|
232
|
+
});
|
|
160
233
|
return (
|
|
161
234
|
<mesh ref={meshRef}>
|
|
162
235
|
<boxGeometry args={[width * 0.5, height * 0.5, 50]} />
|
|
@@ -165,15 +238,20 @@ function My3DWidget({ entityId, width, height }) {
|
|
|
165
238
|
);
|
|
166
239
|
}
|
|
167
240
|
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
241
|
+
export const My3D: R3FWidget<z.infer<typeof schema>> = {
|
|
242
|
+
type: 'my-3d',
|
|
243
|
+
surface: 'webgl',
|
|
244
|
+
schema,
|
|
245
|
+
defaultData: { color: 'hotpink' },
|
|
246
|
+
defaultSize: { width: 250, height: 250 },
|
|
247
|
+
component: My3DView,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const engine = createLayoutEngine({ widgets: [My3D] });
|
|
251
|
+
engine.spawn('my-3d', { at: { x: 100, y: 100 } });
|
|
174
252
|
```
|
|
175
253
|
|
|
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.
|
|
254
|
+
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
255
|
|
|
178
256
|
## Configuration
|
|
179
257
|
|
|
@@ -282,7 +360,7 @@ Use a ref on `<InfiniteCanvas>` for imperative control from outside:
|
|
|
282
360
|
```tsx
|
|
283
361
|
const canvasRef = useRef<InfiniteCanvasHandle>(null);
|
|
284
362
|
|
|
285
|
-
<InfiniteCanvas ref={canvasRef} engine={engine}
|
|
363
|
+
<InfiniteCanvas ref={canvasRef} engine={engine} />
|
|
286
364
|
|
|
287
365
|
// Later:
|
|
288
366
|
canvasRef.current?.zoomToFit();
|
|
@@ -318,7 +396,7 @@ useEffect(() => {
|
|
|
318
396
|
Define custom components and systems to extend the canvas:
|
|
319
397
|
|
|
320
398
|
```tsx
|
|
321
|
-
import { defineComponent, defineSystem } from '@jamesyong42/
|
|
399
|
+
import { defineComponent, defineSystem } from '@jamesyong42/reactive-ecs';
|
|
322
400
|
import { Visible } from '@jamesyong42/infinite-canvas';
|
|
323
401
|
|
|
324
402
|
const Health = defineComponent('Health', { hp: 100, maxHp: 100 });
|