@opsydyn/effect-three 0.0.0-stage → 0.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 (166) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE +21 -0
  3. package/README.md +191 -2
  4. package/dist/index-4045wme0.js +129 -0
  5. package/dist/index-4045wme0.js.map +10 -0
  6. package/dist/index-ef8pbv9g.js +5118 -0
  7. package/dist/index-ef8pbv9g.js.map +40 -0
  8. package/dist/index.d.ts +12 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +1361 -0
  11. package/dist/index.js.map +14 -0
  12. package/dist/kernel/animation.d.ts +14 -0
  13. package/dist/kernel/animation.d.ts.map +1 -0
  14. package/dist/kernel/behaviours.d.ts +16 -0
  15. package/dist/kernel/behaviours.d.ts.map +1 -0
  16. package/dist/kernel/bodies.d.ts +60 -0
  17. package/dist/kernel/bodies.d.ts.map +1 -0
  18. package/dist/kernel/bodySources.d.ts +47 -0
  19. package/dist/kernel/bodySources.d.ts.map +1 -0
  20. package/dist/kernel/build.d.ts +564 -0
  21. package/dist/kernel/build.d.ts.map +1 -0
  22. package/dist/kernel/cameraMotion.d.ts +39 -0
  23. package/dist/kernel/cameraMotion.d.ts.map +1 -0
  24. package/dist/kernel/clock.d.ts +17 -0
  25. package/dist/kernel/clock.d.ts.map +1 -0
  26. package/dist/kernel/contactShadowPasses.d.ts +47 -0
  27. package/dist/kernel/contactShadowPasses.d.ts.map +1 -0
  28. package/dist/kernel/description.d.ts +1120 -0
  29. package/dist/kernel/description.d.ts.map +1 -0
  30. package/dist/kernel/diff.d.ts +90 -0
  31. package/dist/kernel/diff.d.ts.map +1 -0
  32. package/dist/kernel/environmentPresets.d.ts +30 -0
  33. package/dist/kernel/environmentPresets.d.ts.map +1 -0
  34. package/dist/kernel/equality.d.ts +64 -0
  35. package/dist/kernel/equality.d.ts.map +1 -0
  36. package/dist/kernel/frames.d.ts +55 -0
  37. package/dist/kernel/frames.d.ts.map +1 -0
  38. package/dist/kernel/index.d.ts +37 -0
  39. package/dist/kernel/index.d.ts.map +1 -0
  40. package/dist/kernel/index.js +335 -0
  41. package/dist/kernel/index.js.map +9 -0
  42. package/dist/kernel/instances.d.ts +26 -0
  43. package/dist/kernel/instances.d.ts.map +1 -0
  44. package/dist/kernel/labels.d.ts +45 -0
  45. package/dist/kernel/labels.d.ts.map +1 -0
  46. package/dist/kernel/loop.d.ts +49 -0
  47. package/dist/kernel/loop.d.ts.map +1 -0
  48. package/dist/kernel/materials.d.ts +24 -0
  49. package/dist/kernel/materials.d.ts.map +1 -0
  50. package/dist/kernel/motion.d.ts +36 -0
  51. package/dist/kernel/motion.d.ts.map +1 -0
  52. package/dist/kernel/orbit.d.ts +127 -0
  53. package/dist/kernel/orbit.d.ts.map +1 -0
  54. package/dist/kernel/overlays.d.ts +16 -0
  55. package/dist/kernel/overlays.d.ts.map +1 -0
  56. package/dist/kernel/performance.d.ts +41 -0
  57. package/dist/kernel/performance.d.ts.map +1 -0
  58. package/dist/kernel/physics.d.ts +166 -0
  59. package/dist/kernel/physics.d.ts.map +1 -0
  60. package/dist/kernel/physicsDebug.d.ts +18 -0
  61. package/dist/kernel/physicsDebug.d.ts.map +1 -0
  62. package/dist/kernel/pointer.d.ts +357 -0
  63. package/dist/kernel/pointer.d.ts.map +1 -0
  64. package/dist/kernel/renderKit.d.ts +48 -0
  65. package/dist/kernel/renderKit.d.ts.map +1 -0
  66. package/dist/kernel/renderer.d.ts +83 -0
  67. package/dist/kernel/renderer.d.ts.map +1 -0
  68. package/dist/kernel/resources.d.ts +112 -0
  69. package/dist/kernel/resources.d.ts.map +1 -0
  70. package/dist/kernel/sceneGraph.d.ts +155 -0
  71. package/dist/kernel/sceneGraph.d.ts.map +1 -0
  72. package/dist/kernel/sceneIndex.d.ts +90 -0
  73. package/dist/kernel/sceneIndex.d.ts.map +1 -0
  74. package/dist/kernel/stage.d.ts +87 -0
  75. package/dist/kernel/stage.d.ts.map +1 -0
  76. package/dist/kernel/textLayout.d.ts +29 -0
  77. package/dist/kernel/textLayout.d.ts.map +1 -0
  78. package/dist/kernel/tween.d.ts +35 -0
  79. package/dist/kernel/tween.d.ts.map +1 -0
  80. package/dist/kernel/xrInput.d.ts +107 -0
  81. package/dist/kernel/xrInput.d.ts.map +1 -0
  82. package/dist/kernel/xrLocomotion.d.ts +38 -0
  83. package/dist/kernel/xrLocomotion.d.ts.map +1 -0
  84. package/dist/runtime/FrameSource.d.ts +29 -0
  85. package/dist/runtime/FrameSource.d.ts.map +1 -0
  86. package/dist/runtime/ResourceRegistry.d.ts +18 -0
  87. package/dist/runtime/ResourceRegistry.d.ts.map +1 -0
  88. package/dist/runtime/SceneRuntime.d.ts +68 -0
  89. package/dist/runtime/SceneRuntime.d.ts.map +1 -0
  90. package/dist/runtime/SceneSchema.d.ts +2536 -0
  91. package/dist/runtime/SceneSchema.d.ts.map +1 -0
  92. package/dist/runtime/errors.d.ts +19 -0
  93. package/dist/runtime/errors.d.ts.map +1 -0
  94. package/dist/runtime/index.d.ts +11 -0
  95. package/dist/runtime/index.d.ts.map +1 -0
  96. package/dist/webgl/contactShadows.d.ts +5 -0
  97. package/dist/webgl/contactShadows.d.ts.map +1 -0
  98. package/dist/webgl/index.d.ts +11 -0
  99. package/dist/webgl/index.d.ts.map +1 -0
  100. package/dist/webgl/index.js +239 -0
  101. package/dist/webgl/index.js.map +13 -0
  102. package/dist/webgl/shaderGrid.d.ts +27 -0
  103. package/dist/webgl/shaderGrid.d.ts.map +1 -0
  104. package/dist/webgl/sky.d.ts +4 -0
  105. package/dist/webgl/sky.d.ts.map +1 -0
  106. package/dist/webgpu/contactShadows.d.ts +5 -0
  107. package/dist/webgpu/contactShadows.d.ts.map +1 -0
  108. package/dist/webgpu/index.d.ts +10 -0
  109. package/dist/webgpu/index.d.ts.map +1 -0
  110. package/dist/webgpu/index.js +216 -0
  111. package/dist/webgpu/index.js.map +13 -0
  112. package/dist/webgpu/shaderGrid.d.ts +5 -0
  113. package/dist/webgpu/shaderGrid.d.ts.map +1 -0
  114. package/dist/webgpu/sky.d.ts +4 -0
  115. package/dist/webgpu/sky.d.ts.map +1 -0
  116. package/package.json +82 -4
  117. package/src/index.ts +11 -0
  118. package/src/kernel/animation.ts +24 -0
  119. package/src/kernel/behaviours.ts +36 -0
  120. package/src/kernel/bodies.ts +338 -0
  121. package/src/kernel/bodySources.ts +203 -0
  122. package/src/kernel/build.ts +1325 -0
  123. package/src/kernel/cameraMotion.ts +202 -0
  124. package/src/kernel/clock.ts +35 -0
  125. package/src/kernel/contactShadowPasses.ts +207 -0
  126. package/src/kernel/description.ts +1197 -0
  127. package/src/kernel/diff.ts +383 -0
  128. package/src/kernel/environmentPresets.ts +38 -0
  129. package/src/kernel/equality.ts +604 -0
  130. package/src/kernel/frames.ts +124 -0
  131. package/src/kernel/index.ts +274 -0
  132. package/src/kernel/instances.ts +97 -0
  133. package/src/kernel/labels.ts +48 -0
  134. package/src/kernel/loop.ts +248 -0
  135. package/src/kernel/materials.ts +258 -0
  136. package/src/kernel/motion.ts +75 -0
  137. package/src/kernel/orbit.ts +554 -0
  138. package/src/kernel/overlays.ts +130 -0
  139. package/src/kernel/performance.ts +101 -0
  140. package/src/kernel/physics.ts +361 -0
  141. package/src/kernel/physicsDebug.ts +68 -0
  142. package/src/kernel/pointer.ts +785 -0
  143. package/src/kernel/renderKit.ts +53 -0
  144. package/src/kernel/renderer.ts +136 -0
  145. package/src/kernel/resources.ts +504 -0
  146. package/src/kernel/sceneGraph.ts +1440 -0
  147. package/src/kernel/sceneIndex.ts +219 -0
  148. package/src/kernel/stage.ts +157 -0
  149. package/src/kernel/textLayout.ts +102 -0
  150. package/src/kernel/tween.ts +80 -0
  151. package/src/kernel/xrInput.ts +230 -0
  152. package/src/kernel/xrLocomotion.ts +72 -0
  153. package/src/runtime/FrameSource.ts +41 -0
  154. package/src/runtime/ResourceRegistry.ts +24 -0
  155. package/src/runtime/SceneRuntime.ts +229 -0
  156. package/src/runtime/SceneSchema.ts +983 -0
  157. package/src/runtime/errors.ts +18 -0
  158. package/src/runtime/index.ts +10 -0
  159. package/src/webgl/contactShadows.ts +67 -0
  160. package/src/webgl/index.ts +19 -0
  161. package/src/webgl/shaderGrid.ts +196 -0
  162. package/src/webgl/sky.ts +42 -0
  163. package/src/webgpu/contactShadows.ts +83 -0
  164. package/src/webgpu/index.ts +19 -0
  165. package/src/webgpu/shaderGrid.ts +158 -0
  166. package/src/webgpu/sky.ts +35 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * What a renderer brings to a scene graph besides drawing: the parts of a scene written
3
+ * in its shading language. The kernel stays free of shader code (ADR-003): it describes a
4
+ * sky, a shader grid or contact shadows, and the renderer's kit makes them. The WebGL kit
5
+ * is `webglKit` from `@opsydyn/effect-three/webgl`, which `webglRendererFactory` carries;
6
+ * without a kit these parts are not drawn.
7
+ *
8
+ * @module
9
+ */
10
+ import type { Object3D, Scene } from 'three';
11
+
12
+ import type { ContactShadows, ShaderGridNode, Sky } from './description.ts';
13
+ import type { SceneRenderer } from './renderer.ts';
14
+
15
+ /** A sky as a kit draws it: the object the graph adds to its scene, never picked. */
16
+ export type HostedSky = {
17
+ readonly object: Object3D;
18
+ readonly update: (sky: Sky) => void;
19
+ readonly dispose: () => void;
20
+ };
21
+
22
+ /** A shader grid as a kit draws it, placed by the kit from its description. */
23
+ export type HostedShaderGrid = {
24
+ readonly object: Object3D;
25
+ readonly update: (previous: ShaderGridNode, next: ShaderGridNode) => void;
26
+ readonly dispose: () => void;
27
+ };
28
+
29
+ /** The contact shadow passes for one scene, drawn by the frame loop before each frame. */
30
+ export type ContactShadowPasses = {
31
+ /** Draws the scene's contact shadows, if it has any, before the frame is drawn. */
32
+ readonly draw: () => void;
33
+ readonly dispose: () => void;
34
+ };
35
+
36
+ /** What contact shadows need from a scene graph: its scene and their description. */
37
+ export type ShadowedScene = {
38
+ readonly scene: Scene;
39
+ readonly contactShadows: () => ContactShadows | null;
40
+ };
41
+
42
+ /** A renderer's shaded parts. */
43
+ export type RenderKit = {
44
+ /**
45
+ * Whether the renderer can draw a label engine's labels. troika's are WebGL materials:
46
+ * the WebGPU renderer cannot (on real WebGPU they break the frame), so its kit says no
47
+ * and labels stay empty.
48
+ */
49
+ readonly drawsLabels: boolean;
50
+ readonly sky: () => HostedSky;
51
+ readonly shaderGrid: (node: ShaderGridNode) => HostedShaderGrid;
52
+ readonly contactShadows: (graph: ShadowedScene, renderer: SceneRenderer) => ContactShadowPasses;
53
+ };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The renderer boundary (requirement 7). Synchronous and backend-neutral: a WebGL or
3
+ * WebGPU implementation lives outside the DOM-free core and is handed in as a factory.
4
+ *
5
+ * @module
6
+ */
7
+ import { type Camera, type Scene, Texture } from 'three';
8
+
9
+ import type { RenderSettings } from './description.ts';
10
+ import type { RenderKit } from './renderKit.ts';
11
+
12
+ /** Size of the drawing surface in CSS pixels, plus the device pixel ratio. */
13
+ export type Viewport = {
14
+ readonly width: number;
15
+ readonly height: number;
16
+ readonly pixelRatio: number;
17
+ };
18
+
19
+ /** GPU memory counters, as reported by three.js `renderer.info.memory`. */
20
+ export type RendererMemory = {
21
+ readonly geometries: number;
22
+ readonly textures: number;
23
+ };
24
+
25
+ /** A live renderer bound to one drawing surface. */
26
+ /** A rectangle of the drawing surface in CSS pixels, from the top left. */
27
+ export type RenderArea = {
28
+ readonly x: number;
29
+ readonly y: number;
30
+ readonly width: number;
31
+ readonly height: number;
32
+ };
33
+
34
+ /** An offscreen surface: render into it, then use its texture. Dispose exactly once. */
35
+ export type RenderTarget = {
36
+ readonly texture: Texture;
37
+ readonly dispose: () => void;
38
+ };
39
+
40
+ export type SceneRenderer = {
41
+ /**
42
+ * Draws the scene through `camera`, into `area` when given (clearing only that area),
43
+ * otherwise across the whole surface.
44
+ */
45
+ readonly render: (scene: Scene, camera: Camera, area?: RenderArea, target?: RenderTarget) => void;
46
+ /** Creates an offscreen target of `width` × `height` pixels to render into. */
47
+ readonly createTarget: (width: number, height: number) => RenderTarget;
48
+ /**
49
+ * Creates the environment map of three.js's `RoomEnvironment`, prefiltered for this
50
+ * renderer (WebGL: `PMREMGenerator.fromScene(new RoomEnvironment(), 0.04)`).
51
+ */
52
+ readonly createEnvironment: () => RenderTarget;
53
+ /** Applies scene-wide render settings. Called before the first frame and on each change. */
54
+ readonly configure: (settings: RenderSettings) => void;
55
+ readonly setViewport: (viewport: Viewport) => void;
56
+ readonly memory: () => RendererMemory;
57
+ /** Releases the renderer and its drawing context. Called exactly once. */
58
+ readonly dispose: () => void;
59
+ /**
60
+ * Settles when the renderer can draw, for renderers that start asynchronously (three's
61
+ * `WebGPURenderer.init()`); the runtime waits for it before the first frame. Absent for
62
+ * renderers ready at once.
63
+ */
64
+ readonly ready?: (() => Promise<void>) | undefined;
65
+ };
66
+
67
+ /** Creates a renderer. `create` may throw when the backend is unavailable. */
68
+ export type RendererFactory = {
69
+ readonly create: () => SceneRenderer;
70
+ /**
71
+ * The renderer's shaded parts, given to the scene graphs it draws (the sky, shader
72
+ * grids, contact shadows). A factory wrapping another keeps its kit (`...factory`).
73
+ */
74
+ readonly kit?: RenderKit | undefined;
75
+ };
76
+
77
+ /** A renderer that records calls instead of drawing. For headless tests. */
78
+ export type RecordingRenderer = SceneRenderer & {
79
+ readonly renders: () => number;
80
+ /** Every `configure` call, in order. */
81
+ readonly configured: () => ReadonlyArray<RenderSettings>;
82
+ readonly viewport: () => Viewport | undefined;
83
+ readonly disposed: () => boolean;
84
+ };
85
+
86
+ /** A factory of recording renderers that remembers every renderer it created. */
87
+ export type RecordingRendererFactory = RendererFactory & {
88
+ readonly created: () => ReadonlyArray<RecordingRenderer>;
89
+ };
90
+
91
+ const createRecordingRenderer = (): RecordingRenderer => {
92
+ let renders = 0;
93
+ const configured: Array<RenderSettings> = [];
94
+ let viewport: Viewport | undefined;
95
+ let disposed = false;
96
+ return {
97
+ render: () => {
98
+ renders++;
99
+ },
100
+ configure: (settings) => {
101
+ configured.push(settings);
102
+ },
103
+ createTarget: () => {
104
+ const texture = new Texture();
105
+ return { texture, dispose: () => texture.dispose() };
106
+ },
107
+ createEnvironment: () => {
108
+ const texture = new Texture();
109
+ return { texture, dispose: () => texture.dispose() };
110
+ },
111
+ setViewport: (next) => {
112
+ viewport = next;
113
+ },
114
+ memory: () => ({ geometries: 0, textures: 0 }),
115
+ dispose: () => {
116
+ disposed = true;
117
+ },
118
+ renders: () => renders,
119
+ configured: () => configured,
120
+ viewport: () => viewport,
121
+ disposed: () => disposed,
122
+ };
123
+ };
124
+
125
+ /** Creates a factory of recording renderers. */
126
+ export const recordingRendererFactory = (): RecordingRendererFactory => {
127
+ const created: Array<RecordingRenderer> = [];
128
+ return {
129
+ create: () => {
130
+ const renderer = createRecordingRenderer();
131
+ created.push(renderer);
132
+ return renderer;
133
+ },
134
+ created: () => created,
135
+ };
136
+ };
@@ -0,0 +1,504 @@
1
+ /**
2
+ * GPU resource ownership (PLAN D5).
3
+ *
4
+ * - Owned: created for one mesh, disposed when that mesh releases it.
5
+ * - Shared: defined by id, created on first use, reference-counted by the meshes using
6
+ * it, disposed when the last user releases it. Several scene graphs may share one store.
7
+ *
8
+ * Definitions are upserts. Changing a live shared material within its kind updates it in
9
+ * place. Changing a live shared geometry, or a shared material's kind, creates a
10
+ * replacement, notifies subscribers (each scene graph swaps it onto its meshes) and
11
+ * disposes the old one.
12
+ *
13
+ * The store is synchronous so the applier stays synchronous. The Effect runtime owns the
14
+ * store's lifetime and calls `dispose` from a scope finalizer.
15
+ *
16
+ * @module
17
+ */
18
+ import {
19
+ BoxGeometry,
20
+ BufferGeometry,
21
+ CapsuleGeometry,
22
+ CircleGeometry,
23
+ ConeGeometry,
24
+ CylinderGeometry,
25
+ ExtrudeGeometry,
26
+ Material,
27
+ type Object3D,
28
+ type Texture,
29
+ PlaneGeometry,
30
+ RingGeometry,
31
+ ShapeGeometry as ShapeGeometry3,
32
+ SphereGeometry,
33
+ TorusGeometry,
34
+ } from 'three';
35
+ import type { Font } from 'three/addons/loaders/FontLoader.js';
36
+
37
+ import type {
38
+ GeometryRef,
39
+ MaterialRef,
40
+ ResourceDefinitions,
41
+ ShapeGeometry,
42
+ SurfaceMaterial,
43
+ TextGeometry,
44
+ } from './description.ts';
45
+ import { geometryRefEquals, materialRefEquals } from './equality.ts';
46
+ import { buildMaterial, type HostMaterial, updateMaterial } from './materials.ts';
47
+ import { layoutText } from './textLayout.ts';
48
+
49
+ /** Lifetime counters for one resource type. `live` is `created - disposed`. */
50
+ export type ResourceCounts = {
51
+ readonly created: number;
52
+ readonly disposed: number;
53
+ readonly live: number;
54
+ };
55
+
56
+ export type ResourceStats = {
57
+ readonly geometries: ResourceCounts;
58
+ readonly materials: ResourceCounts;
59
+ /** Acquisitions per live shared resource id. */
60
+ readonly sharedGeometryUsers: ReadonlyMap<string, number>;
61
+ readonly sharedMaterialUsers: ReadonlyMap<string, number>;
62
+ };
63
+
64
+ /** A loan that changed: an object for `borrowed` nodes, or a texture for materials. */
65
+ export type Loan = {
66
+ readonly kind: 'object' | 'texture' | 'font' | 'geometry';
67
+ readonly id: string;
68
+ };
69
+
70
+ /** Raised when a live shared geometry is replaced because its definition changed. */
71
+ export type GeometryReplaced = {
72
+ readonly previous: BufferGeometry;
73
+ readonly next: BufferGeometry;
74
+ };
75
+
76
+ /** Raised when a live shared material is replaced because its kind changed. */
77
+ export type MaterialReplaced = {
78
+ readonly previous: HostMaterial;
79
+ readonly next: HostMaterial;
80
+ };
81
+
82
+ export type ResourceStore = {
83
+ readonly define: (definitions: ResourceDefinitions) => void;
84
+ readonly acquireGeometry: (ref: GeometryRef) => BufferGeometry;
85
+ /** Releases one acquisition. `geometry` is the value currently held for `ref`. */
86
+ readonly releaseGeometry: (ref: GeometryRef, geometry: BufferGeometry) => void;
87
+ readonly acquireMaterial: (ref: MaterialRef) => HostMaterial;
88
+ /** Releases one acquisition. `material` is the value currently held for `ref`. */
89
+ readonly releaseMaterial: (ref: MaterialRef, material: HostMaterial) => void;
90
+ /** Subscribes to shared geometry replacement. Returns an unsubscribe function. */
91
+ readonly onGeometryReplaced: (listener: (event: GeometryReplaced) => void) => () => void;
92
+ /**
93
+ * Subscribes to any change to a live shared resource (updated in place or replaced),
94
+ * so scenes using it know to draw again. Returns an unsubscribe function.
95
+ */
96
+ readonly onSharedChanged: (listener: () => void) => () => void;
97
+ /**
98
+ * Lends an application-owned three.js object under `id`, replacing any earlier loan.
99
+ * Scenes show it wherever a `borrowed` node names `id`. The store never disposes it.
100
+ * Returns a function that withdraws this loan (and nothing lent since).
101
+ */
102
+ readonly lend: (id: string, object: Object3D) => () => void;
103
+ /** The object lent under `id`, if any. */
104
+ readonly lent: (id: string) => Object3D | undefined;
105
+ /**
106
+ * Lends an application-owned texture under `id`, for materials' `map`. Materials using
107
+ * it update at once, owned and shared alike. The store never disposes it. Returns a
108
+ * function that withdraws this loan.
109
+ */
110
+ readonly lendTexture: (id: string, texture: Texture) => () => void;
111
+ /** The texture lent under `id`, if any. */
112
+ readonly texture: (id: string) => Texture | undefined;
113
+ /**
114
+ * Lends a typeface font under `id` for text geometries naming it. Text already shown
115
+ * is rebuilt. Returns a function that withdraws the loan.
116
+ */
117
+ readonly lendFont: (id: string, font: Font) => () => void;
118
+ /** The font lent under `id`, if any. */
119
+ readonly font: (id: string) => Font | undefined;
120
+ /**
121
+ * Lends an application-owned geometry under `id`, for meshes whose geometry is
122
+ * `lentGeometry(id)`; they show it at once, and nothing once it is withdrawn. The store
123
+ * never disposes it. Returns a function that withdraws this loan.
124
+ */
125
+ readonly lendGeometry: (id: string, geometry: BufferGeometry) => () => void;
126
+ /** Subscribes to loans and withdrawals of objects and textures. Returns an unsubscribe. */
127
+ readonly onLoanChanged: (listener: (loan: Loan) => void) => () => void;
128
+ /** Subscribes to shared material replacement. Returns an unsubscribe function. */
129
+ readonly onMaterialReplaced: (listener: (event: MaterialReplaced) => void) => () => void;
130
+ /**
131
+ * Applies new parameters to an owned material in place. Returns `false`, changing
132
+ * nothing, when `next` is a different kind: release it and acquire a new one instead.
133
+ */
134
+ readonly updateOwnedMaterial: (material: HostMaterial, next: SurfaceMaterial) => boolean;
135
+ /** The shared id of a live geometry, or `undefined` if it is owned or unknown. */
136
+ readonly sharedGeometryId: (geometry: BufferGeometry) => string | undefined;
137
+ /** The shared id of a live material, or `undefined` if it is owned or unknown. */
138
+ readonly sharedMaterialId: (material: HostMaterial) => string | undefined;
139
+ readonly stats: () => ResourceStats;
140
+ /** Disposes every live resource. Idempotent. */
141
+ readonly dispose: () => void;
142
+ };
143
+
144
+ type Shared<A> = { value: A; users: number };
145
+
146
+ type Counter = { created: number; disposed: number };
147
+
148
+ const counts = (counter: Counter): ResourceCounts => ({
149
+ created: counter.created,
150
+ disposed: counter.disposed,
151
+ live: counter.created - counter.disposed,
152
+ });
153
+
154
+ const userCounts = <A>(entries: ReadonlyMap<string, Shared<A>>): ReadonlyMap<string, number> => {
155
+ const result = new Map<string, number>();
156
+ for (const [id, entry] of entries) result.set(id, entry.users);
157
+ return result;
158
+ };
159
+
160
+ // A swapped-out shared geometry keeps its id in the WeakMap, so check it is still current.
161
+ const liveId = <A extends object>(
162
+ entries: ReadonlyMap<string, Shared<A>>,
163
+ ids: WeakMap<A, string>,
164
+ value: A,
165
+ ): string | undefined => {
166
+ const id = ids.get(value);
167
+ if (id === undefined || entries.get(id)?.value !== value) return undefined;
168
+ return id;
169
+ };
170
+
171
+ /**
172
+ * Text from its font: flat shapes, or extruded as three's `TextGeometry` extrudes them
173
+ * (its options, from shapes laid out with spacing and alignment).
174
+ */
175
+ const buildText = (shape: TextGeometry, font: Font | undefined): BufferGeometry => {
176
+ if (font === undefined) return new BufferGeometry();
177
+ const shapes = [...layoutText(font, shape.text, shape)];
178
+ const geometry =
179
+ shape.depth === 0 && shape.bevel === null
180
+ ? new ShapeGeometry3(shapes, shape.curveSegments)
181
+ : new ExtrudeGeometry(shapes, {
182
+ depth: shape.depth,
183
+ curveSegments: shape.curveSegments,
184
+ bevelEnabled: shape.bevel !== null,
185
+ bevelThickness: shape.bevel?.thickness ?? 0,
186
+ bevelSize: shape.bevel?.size ?? 0,
187
+ bevelOffset: shape.bevel?.offset ?? 0,
188
+ bevelSegments: shape.bevel?.segments ?? 1,
189
+ });
190
+ if (shape.center) geometry.center();
191
+ return geometry;
192
+ };
193
+
194
+ /** Builds the three.js geometry for a shape description. */
195
+ const buildShape = (
196
+ shape: ShapeGeometry,
197
+ fontOf: (id: string) => Font | undefined,
198
+ ): BufferGeometry => {
199
+ switch (shape._tag) {
200
+ case 'Text':
201
+ return buildText(shape, fontOf(shape.font));
202
+ case 'Box':
203
+ return new BoxGeometry(shape.width, shape.height, shape.depth);
204
+ case 'Sphere':
205
+ return new SphereGeometry(shape.radius, shape.widthSegments, shape.heightSegments);
206
+ case 'Plane':
207
+ return new PlaneGeometry(shape.width, shape.height);
208
+ case 'Cylinder':
209
+ return new CylinderGeometry(
210
+ shape.radiusTop,
211
+ shape.radiusBottom,
212
+ shape.height,
213
+ shape.radialSegments,
214
+ );
215
+ case 'Torus':
216
+ return new TorusGeometry(
217
+ shape.radius,
218
+ shape.tube,
219
+ shape.radialSegments,
220
+ shape.tubularSegments,
221
+ );
222
+ case 'Cone':
223
+ return new ConeGeometry(shape.radius, shape.height, shape.radialSegments);
224
+ case 'Capsule':
225
+ return new CapsuleGeometry(
226
+ shape.radius,
227
+ shape.height,
228
+ shape.capSegments,
229
+ shape.radialSegments,
230
+ );
231
+ case 'Ring':
232
+ return new RingGeometry(shape.innerRadius, shape.outerRadius, shape.thetaSegments);
233
+ case 'Circle':
234
+ return new CircleGeometry(shape.radius, shape.segments);
235
+ }
236
+ };
237
+
238
+ /** Creates an empty resource store. */
239
+ /** What a mesh shows while its lent geometry is not lent: nothing. Shared, never disposed. */
240
+ const notLent = new BufferGeometry();
241
+
242
+ export const createResourceStore = (): ResourceStore => {
243
+ const geometryDefinitions = new Map<string, ShapeGeometry>();
244
+ const materialDefinitions = new Map<string, SurfaceMaterial>();
245
+ const sharedGeometries = new Map<string, Shared<BufferGeometry>>();
246
+ const sharedMaterials = new Map<string, Shared<HostMaterial>>();
247
+ const sharedGeometryIds = new WeakMap<BufferGeometry, string>();
248
+ const sharedMaterialIds = new WeakMap<HostMaterial, string>();
249
+ const owned = new Set<BufferGeometry | HostMaterial>();
250
+ const replacedListeners = new Set<(event: GeometryReplaced) => void>();
251
+ const materialListeners = new Set<(event: MaterialReplaced) => void>();
252
+ const changeListeners = new Set<() => void>();
253
+ const loans = new Map<string, Object3D>();
254
+ const textures = new Map<string, Texture>();
255
+ const textureOf = (id: string): Texture | undefined => textures.get(id);
256
+ const fonts = new Map<string, Font>();
257
+ const fontOf = (id: string): Font | undefined => fonts.get(id);
258
+ const lentGeometries = new Map<string, BufferGeometry>();
259
+ const loanListeners = new Set<(loan: Loan) => void>();
260
+ const loanChanged = (loan: Loan): void => {
261
+ // Shared materials belong to the store: re-apply those that use a changed texture.
262
+ if (loan.kind === 'texture') {
263
+ for (const [id, definition] of materialDefinitions) {
264
+ const live = sharedMaterials.get(id);
265
+ if (live !== undefined && 'map' in definition && definition.map === loan.id) {
266
+ updateMaterial(live.value, definition, textureOf);
267
+ }
268
+ }
269
+ }
270
+ // Shared text is the store's too: rebuild what uses a changed font.
271
+ if (loan.kind === 'font') {
272
+ for (const [id, definition] of geometryDefinitions) {
273
+ if (definition._tag === 'Text' && definition.font === loan.id)
274
+ replaceShared(id, definition);
275
+ }
276
+ }
277
+ for (const listener of loanListeners) listener(loan);
278
+ };
279
+ const notifyChanged = (): void => {
280
+ for (const listener of changeListeners) listener();
281
+ };
282
+ const geometryCounter: Counter = { created: 0, disposed: 0 };
283
+ const materialCounter: Counter = { created: 0, disposed: 0 };
284
+
285
+ const makeGeometry = (description: ShapeGeometry): BufferGeometry => {
286
+ geometryCounter.created++;
287
+ return buildShape(description, fontOf);
288
+ };
289
+ const disposeGeometry = (geometry: BufferGeometry): void => {
290
+ geometryCounter.disposed++;
291
+ geometry.dispose();
292
+ };
293
+ const makeMaterial = (description: SurfaceMaterial): HostMaterial => {
294
+ materialCounter.created++;
295
+ return buildMaterial(description, textureOf);
296
+ };
297
+ const disposeMaterial = (material: HostMaterial): void => {
298
+ materialCounter.disposed++;
299
+ material.dispose();
300
+ };
301
+
302
+ /** Rebuilds a live shared geometry and swaps it onto every user. */
303
+ function replaceShared(id: string, definition: ShapeGeometry): void {
304
+ const live = sharedGeometries.get(id);
305
+ if (live === undefined) return;
306
+ const previous = live.value;
307
+ const replacement = makeGeometry(definition);
308
+ sharedGeometryIds.set(replacement, id);
309
+ live.value = replacement;
310
+ for (const listener of replacedListeners) listener({ previous, next: replacement });
311
+ disposeGeometry(previous);
312
+ notifyChanged();
313
+ }
314
+
315
+ const define = (definitions: ResourceDefinitions): void => {
316
+ for (const { id, geometry } of definitions.geometries) {
317
+ const before = geometryDefinitions.get(id);
318
+ geometryDefinitions.set(id, geometry);
319
+ if (before === undefined || geometryRefEquals(before, geometry)) continue;
320
+ replaceShared(id, geometry);
321
+ }
322
+ for (const { id, material } of definitions.materials) {
323
+ const before = materialDefinitions.get(id);
324
+ materialDefinitions.set(id, material);
325
+ const live = sharedMaterials.get(id);
326
+ if (live === undefined || before === undefined || materialRefEquals(before, material)) {
327
+ continue;
328
+ }
329
+ if (updateMaterial(live.value, material, textureOf)) {
330
+ notifyChanged();
331
+ continue;
332
+ }
333
+ const previous = live.value;
334
+ const replacement = makeMaterial(material);
335
+ sharedMaterialIds.set(replacement, id);
336
+ live.value = replacement;
337
+ for (const listener of materialListeners) listener({ previous, next: replacement });
338
+ disposeMaterial(previous);
339
+ notifyChanged();
340
+ }
341
+ };
342
+
343
+ const acquireGeometry = (ref: GeometryRef): BufferGeometry => {
344
+ // A lent geometry is the lender's: shown as it is, never counted or disposed.
345
+ if (ref._tag === 'LentGeometry') return lentGeometries.get(ref.id) ?? notLent;
346
+ if (ref._tag !== 'SharedGeometry') {
347
+ const geometry = makeGeometry(ref);
348
+ owned.add(geometry);
349
+ return geometry;
350
+ }
351
+ const live = sharedGeometries.get(ref.id);
352
+ if (live !== undefined) {
353
+ live.users++;
354
+ return live.value;
355
+ }
356
+ const definition = geometryDefinitions.get(ref.id);
357
+ if (definition === undefined)
358
+ throw new Error(`effect-three: undefined shared geometry "${ref.id}"`);
359
+ const geometry = makeGeometry(definition);
360
+ sharedGeometries.set(ref.id, { value: geometry, users: 1 });
361
+ sharedGeometryIds.set(geometry, ref.id);
362
+ return geometry;
363
+ };
364
+
365
+ const releaseGeometry = (ref: GeometryRef, geometry: BufferGeometry): void => {
366
+ if (ref._tag === 'LentGeometry') return;
367
+ if (ref._tag !== 'SharedGeometry') {
368
+ if (owned.delete(geometry)) disposeGeometry(geometry);
369
+ return;
370
+ }
371
+ const live = sharedGeometries.get(ref.id);
372
+ if (live === undefined || live.value !== geometry) return;
373
+ live.users--;
374
+ if (live.users > 0) return;
375
+ sharedGeometries.delete(ref.id);
376
+ disposeGeometry(live.value);
377
+ };
378
+
379
+ const acquireMaterial = (ref: MaterialRef): HostMaterial => {
380
+ if (ref._tag !== 'SharedMaterial') {
381
+ const material = makeMaterial(ref);
382
+ owned.add(material);
383
+ return material;
384
+ }
385
+ const live = sharedMaterials.get(ref.id);
386
+ if (live !== undefined) {
387
+ live.users++;
388
+ return live.value;
389
+ }
390
+ const definition = materialDefinitions.get(ref.id);
391
+ if (definition === undefined)
392
+ throw new Error(`effect-three: undefined shared material "${ref.id}"`);
393
+ const material = makeMaterial(definition);
394
+ sharedMaterials.set(ref.id, { value: material, users: 1 });
395
+ sharedMaterialIds.set(material, ref.id);
396
+ return material;
397
+ };
398
+
399
+ const releaseMaterial = (ref: MaterialRef, material: HostMaterial): void => {
400
+ if (ref._tag !== 'SharedMaterial') {
401
+ if (owned.delete(material)) disposeMaterial(material);
402
+ return;
403
+ }
404
+ const live = sharedMaterials.get(ref.id);
405
+ if (live === undefined || live.value !== material) return;
406
+ live.users--;
407
+ if (live.users > 0) return;
408
+ sharedMaterials.delete(ref.id);
409
+ disposeMaterial(live.value);
410
+ };
411
+
412
+ const dispose = (): void => {
413
+ for (const resource of owned) {
414
+ if (resource instanceof Material) disposeMaterial(resource);
415
+ else disposeGeometry(resource);
416
+ }
417
+ owned.clear();
418
+ for (const entry of sharedGeometries.values()) disposeGeometry(entry.value);
419
+ sharedGeometries.clear();
420
+ for (const entry of sharedMaterials.values()) disposeMaterial(entry.value);
421
+ sharedMaterials.clear();
422
+ };
423
+
424
+ return {
425
+ define,
426
+ acquireGeometry,
427
+ releaseGeometry,
428
+ acquireMaterial,
429
+ releaseMaterial,
430
+ updateOwnedMaterial: (material, next) => updateMaterial(material, next, textureOf),
431
+ onGeometryReplaced: (listener) => {
432
+ replacedListeners.add(listener);
433
+ return () => {
434
+ replacedListeners.delete(listener);
435
+ };
436
+ },
437
+ lend: (id, object) => {
438
+ loans.set(id, object);
439
+ loanChanged({ kind: 'object', id });
440
+ return () => {
441
+ if (loans.get(id) !== object) return;
442
+ loans.delete(id);
443
+ loanChanged({ kind: 'object', id });
444
+ };
445
+ },
446
+ lent: (id) => loans.get(id),
447
+ lendTexture: (id, texture) => {
448
+ textures.set(id, texture);
449
+ loanChanged({ kind: 'texture', id });
450
+ return () => {
451
+ if (textures.get(id) !== texture) return;
452
+ textures.delete(id);
453
+ loanChanged({ kind: 'texture', id });
454
+ };
455
+ },
456
+ texture: textureOf,
457
+ lendFont: (id, font) => {
458
+ fonts.set(id, font);
459
+ loanChanged({ kind: 'font', id });
460
+ return () => {
461
+ if (fonts.get(id) !== font) return;
462
+ fonts.delete(id);
463
+ loanChanged({ kind: 'font', id });
464
+ };
465
+ },
466
+ font: fontOf,
467
+ lendGeometry: (id, geometry) => {
468
+ lentGeometries.set(id, geometry);
469
+ loanChanged({ kind: 'geometry', id });
470
+ return () => {
471
+ if (lentGeometries.get(id) !== geometry) return;
472
+ lentGeometries.delete(id);
473
+ loanChanged({ kind: 'geometry', id });
474
+ };
475
+ },
476
+ onLoanChanged: (listener) => {
477
+ loanListeners.add(listener);
478
+ return () => {
479
+ loanListeners.delete(listener);
480
+ };
481
+ },
482
+ onSharedChanged: (listener) => {
483
+ changeListeners.add(listener);
484
+ return () => {
485
+ changeListeners.delete(listener);
486
+ };
487
+ },
488
+ onMaterialReplaced: (listener) => {
489
+ materialListeners.add(listener);
490
+ return () => {
491
+ materialListeners.delete(listener);
492
+ };
493
+ },
494
+ sharedGeometryId: (geometry) => liveId(sharedGeometries, sharedGeometryIds, geometry),
495
+ sharedMaterialId: (material) => liveId(sharedMaterials, sharedMaterialIds, material),
496
+ stats: () => ({
497
+ geometries: counts(geometryCounter),
498
+ materials: counts(materialCounter),
499
+ sharedGeometryUsers: userCounts(sharedGeometries),
500
+ sharedMaterialUsers: userCounts(sharedMaterials),
501
+ }),
502
+ dispose,
503
+ };
504
+ };