@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
@@ -1,81 +0,0 @@
1
- import { L as LayoutEngine, R as ResolvedWidget, a as R3FWidgetProps, N as NavigationFrame } from './SelectionRenderer-CeWSNZT8.js';
2
- export { C as ContainerRefProvider, E as EngineProvider, F as FrameSample, G as GridRenderer, P as Profiler, b as ProfilerStats, S as SelectionRenderer, c as SpatialIndex, d as SpatialIndexResource, e as computeSnapGuides } from './SelectionRenderer-CeWSNZT8.js';
3
- import * as react from 'react';
4
- import * as react_jsx_runtime from 'react/jsx-runtime';
5
- import { EntityId, World, ComponentType, TagType } from '@jamesyong42/reactive-ecs';
6
- import 'three';
7
-
8
- interface SelectionOverlaySlotProps {
9
- entityId: EntityId;
10
- slotRef: (entityId: EntityId, el: HTMLDivElement | null) => void;
11
- }
12
- /**
13
- * DOM overlay for WebGL widgets — provides selection frame and pointer
14
- * interaction (select, drag, resize) without rendering widget content.
15
- */
16
- declare const SelectionOverlaySlot: react.MemoExoticComponent<({ entityId, slotRef, }: SelectionOverlaySlotProps) => react_jsx_runtime.JSX.Element>;
17
-
18
- interface WidgetSlotProps {
19
- entityId: EntityId;
20
- slotRef: (entityId: EntityId, el: HTMLDivElement | null) => void;
21
- }
22
- declare const WidgetSlot: react.MemoExoticComponent<({ entityId, slotRef }: WidgetSlotProps) => react_jsx_runtime.JSX.Element>;
23
-
24
- interface WebGLWidgetLayerProps {
25
- engine: LayoutEngine;
26
- entities: EntityId[];
27
- resolve: (entityId: EntityId) => ResolvedWidget | null;
28
- }
29
- declare function WebGLWidgetLayer({ engine, entities, resolve }: WebGLWidgetLayerProps): react_jsx_runtime.JSX.Element;
30
-
31
- interface WebGLWidgetSlotProps {
32
- entityId: EntityId;
33
- component: React.ComponentType<R3FWidgetProps>;
34
- }
35
- /**
36
- * Positions a Three.js Group at the entity's world-space center.
37
- * The widget component renders in local space: origin at center,
38
- * X right, Y up, dimensions = (width, height) in world units.
39
- */
40
- declare function WebGLWidgetSlot({ entityId, component: WidgetComponent }: WebGLWidgetSlotProps): react_jsx_runtime.JSX.Element | null;
41
-
42
- /** JSON-serializable snapshot of the canvas state, including all entities and camera. */
43
- interface CanvasDocument {
44
- version: number;
45
- entities: SerializedEntity[];
46
- resources: {
47
- camera: {
48
- x: number;
49
- y: number;
50
- zoom: number;
51
- };
52
- navigationStack: NavigationFrame[];
53
- };
54
- }
55
- /** A single serialized entity with its components and tags. */
56
- interface SerializedEntity {
57
- id: EntityId;
58
- components: Record<string, unknown>;
59
- tags: string[];
60
- }
61
- /**
62
- * Serializes all entities, components, and tags to a JSON-compatible document.
63
- * Requires registries of known component and tag types for enumeration.
64
- */
65
- declare function serializeWorld(world: World, componentTypes: ComponentType[], tagTypes: TagType[], camera: {
66
- x: number;
67
- y: number;
68
- zoom: number;
69
- }, navigationFrames: NavigationFrame[]): CanvasDocument;
70
- /**
71
- * Restores entities from a serialized document into the world.
72
- * Clears existing state first and remaps entity IDs automatically.
73
- */
74
- declare function deserializeWorld(world: World, doc: CanvasDocument, componentTypes: ComponentType[], tagTypes: TagType[]): void;
75
- /**
76
- * Serializes a subset of entities (e.g., for copy/paste).
77
- * Recursively includes children of the specified entities.
78
- */
79
- declare function serializeEntities(world: World, entityIds: EntityId[], componentTypes: ComponentType[], tagTypes: TagType[]): SerializedEntity[];
80
-
81
- export { type CanvasDocument, SelectionOverlaySlot, type SerializedEntity, WebGLWidgetLayer, WebGLWidgetSlot, WidgetSlot, deserializeWorld, serializeEntities, serializeWorld };
package/dist/advanced.js DELETED
@@ -1,124 +0,0 @@
1
- import { Parent, Children, HandleSet } from './chunk-Z6JQQOWL.js';
2
- export { ContainerRefProvider, EngineProvider, GridRenderer, Profiler, SelectionOverlaySlot, SelectionRenderer, SpatialIndex, SpatialIndexResource, WebGLWidgetLayer, WebGLWidgetSlot, WidgetSlot, computeSnapGuides } from './chunk-Z6JQQOWL.js';
3
-
4
- // src/serialization.ts
5
- function serializeWorld(world, componentTypes, tagTypes, camera, navigationFrames) {
6
- const entities = [];
7
- const allEntities = world.query();
8
- for (const entityId of allEntities) {
9
- const components = {};
10
- const tags = [];
11
- for (const type of componentTypes) {
12
- const data = world.getComponent(entityId, type);
13
- if (data !== void 0) {
14
- components[type.name] = structuredClone(data);
15
- }
16
- }
17
- for (const type of tagTypes) {
18
- if (world.hasTag(entityId, type)) {
19
- if (type.name !== "Active" && type.name !== "Visible") {
20
- tags.push(type.name);
21
- }
22
- }
23
- }
24
- if (Object.keys(components).length > 0 || tags.length > 0) {
25
- entities.push({ id: entityId, components, tags });
26
- }
27
- }
28
- return {
29
- version: 1,
30
- entities,
31
- resources: {
32
- camera: { ...camera },
33
- navigationStack: structuredClone(navigationFrames)
34
- }
35
- };
36
- }
37
- function deserializeWorld(world, doc, componentTypes, tagTypes) {
38
- if (doc.version !== 1) {
39
- throw new Error(`Unsupported canvas document version: ${doc.version}. Expected version 1.`);
40
- }
41
- const compByName = /* @__PURE__ */ new Map();
42
- for (const t of componentTypes) compByName.set(t.name, t);
43
- const tagByName = /* @__PURE__ */ new Map();
44
- for (const t of tagTypes) tagByName.set(t.name, t);
45
- for (const entityId of world.query()) {
46
- world.destroyEntity(entityId);
47
- }
48
- const idMap = /* @__PURE__ */ new Map();
49
- for (const entry of doc.entities) {
50
- const newId = world.createEntity();
51
- idMap.set(entry.id, newId);
52
- for (const [compName, data] of Object.entries(entry.components)) {
53
- const type = compByName.get(compName);
54
- if (type) {
55
- world.addComponent(newId, type, data);
56
- }
57
- }
58
- for (const tagName of entry.tags) {
59
- const type = tagByName.get(tagName);
60
- if (type) {
61
- world.addTag(newId, type);
62
- }
63
- }
64
- }
65
- for (const [_oldId, newId] of idMap) {
66
- const parent = world.getComponent(newId, Parent);
67
- if (parent && idMap.has(parent.id)) {
68
- const mappedId = idMap.get(parent.id);
69
- if (mappedId !== void 0) {
70
- world.setComponent(newId, Parent, { id: mappedId });
71
- }
72
- }
73
- const children = world.getComponent(newId, Children);
74
- if (children) {
75
- world.setComponent(newId, Children, {
76
- ids: children.ids.map((id) => idMap.get(id) ?? id)
77
- });
78
- }
79
- const handleSet = world.getComponent(newId, HandleSet);
80
- if (handleSet) {
81
- world.setComponent(newId, HandleSet, {
82
- ids: handleSet.ids.map((id) => idMap.get(id) ?? id)
83
- });
84
- }
85
- }
86
- }
87
- function serializeEntities(world, entityIds, componentTypes, tagTypes) {
88
- const result = [];
89
- const visited = /* @__PURE__ */ new Set();
90
- function visit(entityId) {
91
- if (visited.has(entityId)) return;
92
- visited.add(entityId);
93
- const components = {};
94
- const tags = [];
95
- for (const type of componentTypes) {
96
- const data = world.getComponent(entityId, type);
97
- if (data !== void 0) {
98
- components[type.name] = structuredClone(data);
99
- }
100
- }
101
- for (const type of tagTypes) {
102
- if (world.hasTag(entityId, type)) {
103
- if (type.name !== "Active" && type.name !== "Visible") {
104
- tags.push(type.name);
105
- }
106
- }
107
- }
108
- result.push({ id: entityId, components, tags });
109
- const children = components.Children;
110
- if (children?.ids) {
111
- for (const childId of children.ids) {
112
- visit(childId);
113
- }
114
- }
115
- }
116
- for (const id of entityIds) {
117
- visit(id);
118
- }
119
- return result;
120
- }
121
-
122
- export { deserializeWorld, serializeEntities, serializeWorld };
123
- //# sourceMappingURL=advanced.js.map
124
- //# sourceMappingURL=advanced.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/serialization.ts"],"names":[],"mappings":"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","file":"advanced.js","sourcesContent":["import type { ComponentType, EntityId, TagType, World } from '@jamesyong42/reactive-ecs';\nimport { Children, HandleSet, Parent } from './components.js';\nimport type { NavigationFrame } from './resources.js';\n\n// === Serialization Types ===\n\n/** JSON-serializable snapshot of the canvas state, including all entities and camera. */\nexport interface CanvasDocument {\n\tversion: number;\n\tentities: SerializedEntity[];\n\tresources: {\n\t\tcamera: { x: number; y: number; zoom: number };\n\t\tnavigationStack: NavigationFrame[];\n\t};\n}\n\n/** A single serialized entity with its components and tags. */\nexport interface SerializedEntity {\n\tid: EntityId;\n\tcomponents: Record<string, unknown>;\n\ttags: string[];\n}\n\n// === Serialize/Deserialize ===\n\n/**\n * Serializes all entities, components, and tags to a JSON-compatible document.\n * Requires registries of known component and tag types for enumeration.\n */\nexport function serializeWorld(\n\tworld: World,\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n\tcamera: { x: number; y: number; zoom: number },\n\tnavigationFrames: NavigationFrame[],\n): CanvasDocument {\n\tconst entities: SerializedEntity[] = [];\n\n\t// Get all entity IDs (use a broad query)\n\tconst allEntities = world.query();\n\n\tfor (const entityId of allEntities) {\n\t\tconst components: Record<string, unknown> = {};\n\t\tconst tags: string[] = [];\n\n\t\tfor (const type of componentTypes) {\n\t\t\tconst data = world.getComponent(entityId, type);\n\t\t\tif (data !== undefined) {\n\t\t\t\tcomponents[type.name] = structuredClone(data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const type of tagTypes) {\n\t\t\tif (world.hasTag(entityId, type)) {\n\t\t\t\t// Skip runtime-only tags (Active, Visible — they're recomputed)\n\t\t\t\tif (type.name !== 'Active' && type.name !== 'Visible') {\n\t\t\t\t\ttags.push(type.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (Object.keys(components).length > 0 || tags.length > 0) {\n\t\t\tentities.push({ id: entityId, components, tags });\n\t\t}\n\t}\n\n\treturn {\n\t\tversion: 1,\n\t\tentities,\n\t\tresources: {\n\t\t\tcamera: { ...camera },\n\t\t\tnavigationStack: structuredClone(navigationFrames),\n\t\t},\n\t};\n}\n\n/**\n * Restores entities from a serialized document into the world.\n * Clears existing state first and remaps entity IDs automatically.\n */\nexport function deserializeWorld(\n\tworld: World,\n\tdoc: CanvasDocument,\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n): void {\n\tif (doc.version !== 1) {\n\t\tthrow new Error(`Unsupported canvas document version: ${doc.version}. Expected version 1.`);\n\t}\n\n\t// Build lookup maps\n\tconst compByName = new Map<string, ComponentType>();\n\tfor (const t of componentTypes) compByName.set(t.name, t);\n\n\tconst tagByName = new Map<string, TagType>();\n\tfor (const t of tagTypes) tagByName.set(t.name, t);\n\n\t// Destroy all existing entities\n\tfor (const entityId of world.query()) {\n\t\tworld.destroyEntity(entityId);\n\t}\n\n\t// First pass: create entities and build old-to-new ID mapping\n\tconst idMap = new Map<EntityId, EntityId>();\n\n\tfor (const entry of doc.entities) {\n\t\tconst newId = world.createEntity();\n\t\tidMap.set(entry.id as EntityId, newId);\n\n\t\tfor (const [compName, data] of Object.entries(entry.components)) {\n\t\t\tconst type = compByName.get(compName);\n\t\t\tif (type) {\n\t\t\t\tworld.addComponent(newId, type, data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const tagName of entry.tags) {\n\t\t\tconst type = tagByName.get(tagName);\n\t\t\tif (type) {\n\t\t\t\tworld.addTag(newId, type);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Second pass: remap cross-reference components (Parent, Children, HandleSet)\n\tfor (const [_oldId, newId] of idMap) {\n\t\tconst parent = world.getComponent(newId, Parent);\n\t\tif (parent && idMap.has(parent.id)) {\n\t\t\tconst mappedId = idMap.get(parent.id);\n\t\t\tif (mappedId !== undefined) {\n\t\t\t\tworld.setComponent(newId, Parent, { id: mappedId });\n\t\t\t}\n\t\t}\n\n\t\tconst children = world.getComponent(newId, Children);\n\t\tif (children) {\n\t\t\tworld.setComponent(newId, Children, {\n\t\t\t\tids: children.ids.map((id: EntityId) => idMap.get(id) ?? id),\n\t\t\t});\n\t\t}\n\n\t\tconst handleSet = world.getComponent(newId, HandleSet);\n\t\tif (handleSet) {\n\t\t\tworld.setComponent(newId, HandleSet, {\n\t\t\t\tids: handleSet.ids.map((id: EntityId) => idMap.get(id) ?? id),\n\t\t\t});\n\t\t}\n\t}\n}\n\n/**\n * Serializes a subset of entities (e.g., for copy/paste).\n * Recursively includes children of the specified entities.\n */\nexport function serializeEntities(\n\tworld: World,\n\tentityIds: EntityId[],\n\tcomponentTypes: ComponentType[],\n\ttagTypes: TagType[],\n): SerializedEntity[] {\n\tconst result: SerializedEntity[] = [];\n\tconst visited = new Set<EntityId>();\n\n\tfunction visit(entityId: EntityId) {\n\t\tif (visited.has(entityId)) return;\n\t\tvisited.add(entityId);\n\n\t\tconst components: Record<string, unknown> = {};\n\t\tconst tags: string[] = [];\n\n\t\tfor (const type of componentTypes) {\n\t\t\tconst data = world.getComponent(entityId, type);\n\t\t\tif (data !== undefined) {\n\t\t\t\tcomponents[type.name] = structuredClone(data);\n\t\t\t}\n\t\t}\n\n\t\tfor (const type of tagTypes) {\n\t\t\tif (world.hasTag(entityId, type)) {\n\t\t\t\tif (type.name !== 'Active' && type.name !== 'Visible') {\n\t\t\t\t\ttags.push(type.name);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresult.push({ id: entityId, components, tags });\n\n\t\t// Recurse into children. components.Children is typed as unknown via\n\t\t// the Record<string, unknown> shape, so narrow through a cast.\n\t\tconst children = components.Children as { ids?: EntityId[] } | undefined;\n\t\tif (children?.ids) {\n\t\t\tfor (const childId of children.ids) {\n\t\t\t\tvisit(childId);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const id of entityIds) {\n\t\tvisit(id);\n\t}\n\n\treturn result;\n}\n"]}