@opsydyn/effect-three-foldkit 0.0.0-stage → 0.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 (96) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/README.md +99 -2
  4. package/dist/assets.d.ts +53 -0
  5. package/dist/assets.d.ts.map +1 -0
  6. package/dist/dom/animationFrames.d.ts +12 -0
  7. package/dist/dom/animationFrames.d.ts.map +1 -0
  8. package/dist/dom/canvas.d.ts +26 -0
  9. package/dist/dom/canvas.d.ts.map +1 -0
  10. package/dist/dom/latch.d.ts +15 -0
  11. package/dist/dom/latch.d.ts.map +1 -0
  12. package/dist/dom/models.d.ts +10 -0
  13. package/dist/dom/models.d.ts.map +1 -0
  14. package/dist/dom/overlays.d.ts +24 -0
  15. package/dist/dom/overlays.d.ts.map +1 -0
  16. package/dist/dom/pointer.d.ts +49 -0
  17. package/dist/dom/pointer.d.ts.map +1 -0
  18. package/dist/dom/postprocessing.d.ts +22 -0
  19. package/dist/dom/postprocessing.d.ts.map +1 -0
  20. package/dist/dom/property.d.ts +15 -0
  21. package/dist/dom/property.d.ts.map +1 -0
  22. package/dist/dom/reconciled.d.ts +10 -0
  23. package/dist/dom/reconciled.d.ts.map +1 -0
  24. package/dist/dom/renderSettings.d.ts +58 -0
  25. package/dist/dom/renderSettings.d.ts.map +1 -0
  26. package/dist/dom/typedScenes.d.ts +14 -0
  27. package/dist/dom/typedScenes.d.ts.map +1 -0
  28. package/dist/dom/updates.d.ts +8 -0
  29. package/dist/dom/updates.d.ts.map +1 -0
  30. package/dist/dom/viewport.d.ts +40 -0
  31. package/dist/dom/viewport.d.ts.map +1 -0
  32. package/dist/dom/webgl.d.ts +12 -0
  33. package/dist/dom/webgl.d.ts.map +1 -0
  34. package/dist/dom/webgpu.d.ts +24 -0
  35. package/dist/dom/webgpu.d.ts.map +1 -0
  36. package/dist/dom/webgpuPostprocessing.d.ts +4 -0
  37. package/dist/dom/webgpuPostprocessing.d.ts.map +1 -0
  38. package/dist/dom/xr.d.ts +53 -0
  39. package/dist/dom/xr.d.ts.map +1 -0
  40. package/dist/dom/xrGlide.d.ts +49 -0
  41. package/dist/dom/xrGlide.d.ts.map +1 -0
  42. package/dist/dom/xrHub.d.ts +45 -0
  43. package/dist/dom/xrHub.d.ts.map +1 -0
  44. package/dist/dom/xrInputs.d.ts +40 -0
  45. package/dist/dom/xrInputs.d.ts.map +1 -0
  46. package/dist/dom/xrSpaces.d.ts +33 -0
  47. package/dist/dom/xrSpaces.d.ts.map +1 -0
  48. package/dist/dom/xrTarget.d.ts +36 -0
  49. package/dist/dom/xrTarget.d.ts.map +1 -0
  50. package/dist/element.d.ts +771 -0
  51. package/dist/element.d.ts.map +1 -0
  52. package/dist/environment.d.ts +64 -0
  53. package/dist/environment.d.ts.map +1 -0
  54. package/dist/index.d.ts +32 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +2041 -0
  57. package/dist/index.js.map +38 -0
  58. package/dist/mount.d.ts +14 -0
  59. package/dist/mount.d.ts.map +1 -0
  60. package/dist/overlayView.d.ts +23 -0
  61. package/dist/overlayView.d.ts.map +1 -0
  62. package/dist/view.d.ts +179 -0
  63. package/dist/view.d.ts.map +1 -0
  64. package/dist/xr.d.ts +34 -0
  65. package/dist/xr.d.ts.map +1 -0
  66. package/package.json +72 -4
  67. package/src/assets.ts +151 -0
  68. package/src/dom/animationFrames.ts +32 -0
  69. package/src/dom/canvas.ts +54 -0
  70. package/src/dom/latch.ts +36 -0
  71. package/src/dom/models.ts +37 -0
  72. package/src/dom/overlays.ts +63 -0
  73. package/src/dom/pointer.ts +163 -0
  74. package/src/dom/postprocessing.ts +116 -0
  75. package/src/dom/property.ts +43 -0
  76. package/src/dom/reconciled.ts +51 -0
  77. package/src/dom/renderSettings.ts +117 -0
  78. package/src/dom/typedScenes.ts +21 -0
  79. package/src/dom/updates.ts +14 -0
  80. package/src/dom/viewport.ts +97 -0
  81. package/src/dom/webgl.ts +216 -0
  82. package/src/dom/webgpu.ts +205 -0
  83. package/src/dom/webgpuPostprocessing.ts +102 -0
  84. package/src/dom/xr.ts +211 -0
  85. package/src/dom/xrGlide.ts +114 -0
  86. package/src/dom/xrHub.ts +118 -0
  87. package/src/dom/xrInputs.ts +309 -0
  88. package/src/dom/xrSpaces.ts +287 -0
  89. package/src/dom/xrTarget.ts +71 -0
  90. package/src/element.ts +114 -0
  91. package/src/environment.ts +78 -0
  92. package/src/index.ts +71 -0
  93. package/src/mount.ts +305 -0
  94. package/src/overlayView.ts +71 -0
  95. package/src/view.ts +254 -0
  96. package/src/xr.ts +36 -0
package/dist/view.d.ts ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The scene view: one call from a Foldkit `view` renders a scene and maps its events to
3
+ * the app's typed Messages. The scene is plain data derived from the Model.
4
+ *
5
+ * @module
6
+ */
7
+ import type { SceneDescription, SceneSchema } from '@opsydyn/effect-three';
8
+ import type { Html, HtmlBuilder } from 'foldkit/html';
9
+ import { type ReconciledDetail, type SceneError, type SceneLifecycle, type XrSessionDetail } from './element.ts';
10
+ import type { SceneMount } from './mount.ts';
11
+ import { type OverlayView } from './overlayView.ts';
12
+ /**
13
+ * Where the pointer met an interactive mesh: key, world-space point, distance from the
14
+ * camera, world-space face normal and texture coordinates.
15
+ */
16
+ export type HitDetail = typeof SceneSchema.HitDetail.Type;
17
+ /** Detail of click, double-click, pointer-down and pointer-up events: the hit and button. */
18
+ export type ButtonDetail = typeof SceneSchema.ButtonDetail.Type;
19
+ /** Detail of wheel events: the hit and the wheel deltas. */
20
+ export type WheelDetail = typeof SceneSchema.WheelDetail.Type;
21
+ /** Detail of a click that hit no interactive mesh. */
22
+ export type MissDetail = typeof SceneSchema.MissDetail.Type;
23
+ /** Detail of drag events: the captured mesh, the button and the pointer's world-space ray. */
24
+ export type DragDetail = typeof SceneSchema.DragDetail.Type;
25
+ /**
26
+ * A drag, wheel or pinch on the canvas, when the scene claims gestures. Pass it to
27
+ * `orbit` to move a camera kept in the Model.
28
+ */
29
+ export type Gesture = typeof SceneSchema.Gesture.Type;
30
+ /** Detail of collision events: the two bodies' keys, and whether they began touching. */
31
+ export type CollisionDetail = typeof SceneSchema.CollisionDetail.Type;
32
+ /** Detail of contact force events: the two bodies, and how hard they pushed. */
33
+ export type ContactForceDetail = typeof SceneSchema.ContactForceDetail.Type;
34
+ /** Detail of measured events: a node's world bounding box. */
35
+ export type MeasuredDetail = typeof SceneSchema.MeasuredDetail.Type;
36
+ /** Detail of auto-rotated events: where a camera turning by itself got to. */
37
+ export type AutoRotatedDetail = typeof SceneSchema.AutoRotatedDetail.Type;
38
+ /** Detail of label measured events: a label's laid-out size in its own units. */
39
+ export type LabelMeasuredDetail = typeof SceneSchema.LabelMeasuredDetail.Type;
40
+ /** Detail of XR stick events: a thumbstick pushed past halfway in a direction, or let back. */
41
+ export type XrStickDetail = typeof SceneSchema.XrStickDetail.Type;
42
+ /** Detail of XR button events: a controller button pressed or released. */
43
+ export type XrButtonDetail = typeof SceneSchema.XrButtonDetail.Type;
44
+ /** Detail of XR moved events: where gliding left the player. */
45
+ export type XrMovedDetail = typeof SceneSchema.XrMovedDetail.Type;
46
+ /** Detail of XR planes events: every plane the session detected, as it is now. */
47
+ export type XrPlanesDetail = typeof SceneSchema.XrPlanesDetail.Type;
48
+ /** Detail of XR meshes events: every mesh the session detected, as it is now. */
49
+ export type XrMeshesDetail = typeof SceneSchema.XrMeshesDetail.Type;
50
+ /** Detail of XR surface events: in AR, where a select met a real surface, and its normal. */
51
+ export type XrSurfaceDetail = typeof SceneSchema.XrSurfaceDetail.Type;
52
+ /** Detail of performance changes: the measured fps and the new factor, 0 to 1. */
53
+ export type PerformanceDetail = typeof SceneSchema.PerformanceDetail.Type;
54
+ /** Detail of pointer-leave events: the key that is no longer under the pointer. */
55
+ export type KeyDetail = typeof SceneSchema.KeyDetail.Type;
56
+ export type ViewConfig<Message> = {
57
+ /** The scene to show. Derive it from the Model; unchanged scenes cost nothing. */
58
+ readonly scene: SceneDescription;
59
+ /** The Mount from `makeSceneMount`, created once at the entry point. */
60
+ readonly mount: SceneMount;
61
+ /** Maps the mount outcome (started or failed) into the app's Messages. */
62
+ readonly onLifecycle: (message: SceneLifecycle) => Message;
63
+ /** Pressed and released on the same mesh with the same button. */
64
+ readonly onClick?: (detail: ButtonDetail) => Message;
65
+ /** A double-click on the mesh that was last pressed. */
66
+ readonly onDoubleClick?: (detail: ButtonDetail) => Message;
67
+ /** A context menu on the mesh that was last pressed. The browser's menu stays shut. */
68
+ readonly onContextMenu?: (detail: HitDetail) => Message;
69
+ readonly onPointerDown?: (detail: ButtonDetail) => Message;
70
+ readonly onPointerUp?: (detail: ButtonDetail) => Message;
71
+ readonly onPointerEnter?: (detail: HitDetail) => Message;
72
+ /** Every pointer movement over an interactive mesh. Can fire at the pointer's rate. */
73
+ readonly onPointerMove?: (detail: HitDetail) => Message;
74
+ readonly onPointerLeave?: (detail: KeyDetail) => Message;
75
+ /** A click that hit no interactive mesh and moved at most two pixels: clear a selection. */
76
+ readonly onPointerMissed?: (detail: MissDetail) => Message;
77
+ /** The wheel over an interactive mesh. Page scrolling is not blocked. */
78
+ readonly onWheel?: (detail: WheelDetail) => Message;
79
+ /**
80
+ * The pointer moved while pressed on a mesh, anywhere (the press captures it). Turn the
81
+ * ray into a point with `pointOnPlane` to drag along a plane.
82
+ */
83
+ readonly onDrag?: (detail: DragDetail) => Message;
84
+ /** A drag ended: released anywhere, or the pointer was cancelled. */
85
+ readonly onDragEnd?: (detail: DragDetail) => Message;
86
+ /**
87
+ * A drag, wheel or pinch anywhere on the canvas, reported while the scene claims
88
+ * gestures (`scene({ gestures: 'scene' })`). Apply it with `orbit` in `update`:
89
+ * the camera stays in the Model.
90
+ */
91
+ readonly onGesture?: (gesture: Gesture) => Message;
92
+ /**
93
+ * A gesture is about to begin on a camera turning by itself (`autoRotate`): keep where
94
+ * it got to with `autoRotated(model.camera, detail)` (a view's camera when `view` names
95
+ * one), so the gesture goes on from where the camera was shown.
96
+ */
97
+ readonly onAutoRotated?: (detail: AutoRotatedDetail) => Message;
98
+ /**
99
+ * Two bodies began or stopped touching (react-three-rapier's `onCollisionEnter` and
100
+ * `onCollisionExit`, for the whole scene). Needs `physics` and an engine.
101
+ */
102
+ readonly onCollision?: (detail: CollisionDetail) => Message;
103
+ /**
104
+ * Two bodies pushed on each other harder than a body's `contactForces` threshold during
105
+ * a step (react-three-rapier's `onContactForce`). Rapier reports every such step, so
106
+ * set the threshold high.
107
+ */
108
+ readonly onContactForce?: (detail: ContactForceDetail) => Message;
109
+ /**
110
+ * A node listed in the scene's `measure` has a new world bounding box (Drei's `Bounds`
111
+ * measuring, as a Message): fit the camera to it with `fitCamera`.
112
+ */
113
+ readonly onMeasured?: (detail: MeasuredDetail) => Message;
114
+ /**
115
+ * A measured label (`scene({ measure })`) was laid out anew: its block and tight glyph
116
+ * bounds in its own units, before its scale, to size what goes behind it.
117
+ */
118
+ readonly onLabelMeasured?: (detail: LabelMeasuredDetail) => Message;
119
+ /**
120
+ * An immersive session began presenting this scene (`mode`), or ended (`mode: null`),
121
+ * whether the app or the user (through the headset) ended it.
122
+ */
123
+ readonly onXRSession?: (detail: XrSessionDetail) => Message;
124
+ /**
125
+ * In a session, a thumbstick pushed past halfway in a direction, or let back to the
126
+ * centre: snap turn and teleport by changing the scene's `xr({ heading, origin })`, as
127
+ * pmndrs/xr's controller locomotion does.
128
+ */
129
+ readonly onXRStick?: (detail: XrStickDetail) => Message;
130
+ /** In a session, a controller button pressed or released (`xr-standard` mapping). */
131
+ readonly onXRButton?: (detail: XrButtonDetail) => Message;
132
+ /**
133
+ * In AR with `xr({ hitTest: true })`, a select whose ray met a real surface: place
134
+ * things there.
135
+ */
136
+ readonly onXRSurface?: (detail: XrSurfaceDetail) => Message;
137
+ /**
138
+ * Gliding with the sticks (`xr({ glide })`) stopped: keep where the player now stands
139
+ * with `glided(model.xr, detail)`.
140
+ */
141
+ readonly onXRMoved?: (detail: XrMovedDetail) => Message;
142
+ /**
143
+ * With `xr({ planes: true })`, the detected planes changed (found, reshaped, lost, or
144
+ * seen from where the player moved to): every plane, as it is now. Follow one with
145
+ * `group({ follow: planeSpace(id) })`.
146
+ */
147
+ readonly onXRPlanes?: (detail: XrPlanesDetail) => Message;
148
+ /**
149
+ * With `xr({ meshes: true })`, the detected meshes changed: every mesh, as it is now.
150
+ * Draw one as `mesh({ geometry: xrMesh(id), material })` inside a group that follows
151
+ * `meshSpace(id)`: its geometry is lent to the scene, not sent.
152
+ */
153
+ readonly onXRMeshes?: (detail: XrMeshesDetail) => Message;
154
+ /**
155
+ * Frames ran persistently slow or fast, and the performance factor moved (0 to 1).
156
+ * Store it in the Model and lower `render.resolution` with it, as Drei's
157
+ * `PerformanceMonitor` and `AdaptiveDpr` do together.
158
+ */
159
+ readonly onPerformanceChange?: (detail: PerformanceDetail) => Message;
160
+ /** A scene update was rejected; the scene keeps its last good state. */
161
+ readonly onError?: (detail: {
162
+ readonly error: SceneError;
163
+ }) => Message;
164
+ /**
165
+ * What each scene update did (operations by kind) and the GPU resources held afterwards.
166
+ * Fires at startup and after every update that changed something, never for no-ops.
167
+ */
168
+ readonly onReconciled?: (detail: ReconciledDetail) => Message;
169
+ /**
170
+ * HTML at points in the scene, as Drei's `Html`: each `overlay` says where (a point,
171
+ * perhaps on a node), `content` what. The scene moves the HTML there every frame it
172
+ * draws; the content is ordinary view HTML, rendered from the Model.
173
+ */
174
+ readonly overlays?: ReadonlyArray<OverlayView>;
175
+ readonly className?: string;
176
+ };
177
+ /** Renders a scene element. Only the event handlers you pass are bound. */
178
+ export declare const view: <Message>(config: ViewConfig<NoInfer<Message>>, h: HtmlBuilder<Message>) => Html;
179
+ //# sourceMappingURL=view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../src/view.ts"],"names":[],"mappings":"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"}
package/dist/xr.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Immersive sessions as Effects an application runs from Foldkit Commands: whether a mode
3
+ * is supported, entering it from a user gesture, and leaving it. The scene presented is
4
+ * the mounted one whose description has `xr`; while presenting, the session drives its
5
+ * frames and the view's `onXRSession` reports the session starting and ending.
6
+ *
7
+ * @module
8
+ */
9
+ import { Effect } from 'effect';
10
+ import type { XrHub, XrMode } from './dom/xrHub.ts';
11
+ declare const XrUnavailable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
12
+ readonly _tag: "XrUnavailable";
13
+ } & Readonly<A>;
14
+ /** A session could not start: no WebXR, no scene describing `xr`, or the request refused. */
15
+ export declare class XrUnavailable extends XrUnavailable_base<{
16
+ readonly mode: XrMode;
17
+ readonly cause: unknown;
18
+ }> {
19
+ }
20
+ /** The immersive session controls for an environment's hub. */
21
+ export declare const makeXr: (hub: XrHub) => {
22
+ /** Whether the browser can present `mode` (false without WebXR). */
23
+ supported: (mode: XrMode) => Effect.Effect<boolean, never, never>;
24
+ /**
25
+ * Enters `mode`, presenting the mounted scene that describes `xr`. Run it from the
26
+ * Command a click (or other user gesture) produces: browsers require one.
27
+ */
28
+ enter: (mode: XrMode) => Effect.Effect<void, XrUnavailable, never>;
29
+ /** Ends the session, if there is one. */
30
+ exit: Effect.Effect<void, never, never>;
31
+ };
32
+ export type XrControls = ReturnType<typeof makeXr>;
33
+ export {};
34
+ //# sourceMappingURL=xr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xr.d.ts","sourceRoot":"","sources":["../src/xr.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;;;;AAEpD,6FAA6F;AAC7F,qBAAa,aAAc,SAAQ,mBAAkC;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB,CAAC;CAAG;AAEL,+DAA+D;AAC/D,eAAO,MAAM,MAAM,QAAS,KAAK;IAC/B,oEAAoE;IACpE,SAAS,SAAS,MAAM;IACxB;;;OAGG;IACH,KAAK,SAAS,MAAM;IAKpB,yCAAyC;IACzC,IAAI;CACJ,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "@opsydyn/effect-three-foldkit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Thin Foldkit adapter for @opsydyn/effect-three.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/opsydyn/fold-kit-experiments.git",
9
+ "directory": "packages/effect-three-foldkit"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "README.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE",
17
+ "!dist/.tsbuildinfo",
18
+ "!dist/api-extractor",
19
+ "!dist/pack"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "bun": "./src/index.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "typecheck": "tsc -b . ../effect-three-troika && tsc -p tsconfig.test.json",
36
+ "lint": "bunx --bun oxlint --config oxlint.strict.ts",
37
+ "check": "bun run lint",
38
+ "test": "bun test --pass-with-no-tests ./src ./test",
39
+ "test:e2e": "bun test ./e2e",
40
+ "build": "tsc -b && bun build ./src/index.ts --root ./src --outdir ./dist --target browser --packages external --sourcemap=linked",
41
+ "api:report": "api-extractor run --local",
42
+ "api:check": "api-extractor run",
43
+ "pack:check": "bun run build && publint && rm -rf dist/pack && bun pm pack --destination dist/pack && attw dist/pack/*.tgz --profile esm-only"
44
+ },
45
+ "devDependencies": {
46
+ "@arethetypeswrong/cli": "0.18.5",
47
+ "@effect/platform-browser": "4.0.0",
48
+ "@fontsource/inter": "5.3.0",
49
+ "@microsoft/api-extractor": "7.59.4",
50
+ "@opsydyn/effect-three": "0.1.0",
51
+ "@opsydyn/effect-three-troika": "0.1.0",
52
+ "@opsydyn/oxlint-effect": "2.0.1",
53
+ "@types/bun": "1.4.3",
54
+ "@types/pngjs": "6.0.5",
55
+ "@types/three": "0.186.0",
56
+ "effect": "4.0.0",
57
+ "foldkit": "0.167.0",
58
+ "iwer": "2.5.0",
59
+ "oxlint": "1.88.0",
60
+ "oxlint-tsgolint": "7.0.2004",
61
+ "pixelmatch": "8.0.0",
62
+ "playwright": "1.64.1",
63
+ "pngjs": "7.0.0",
64
+ "publint": "0.3.25",
65
+ "three": "0.186.1",
66
+ "typescript": "7.0.2"
67
+ },
68
+ "peerDependencies": {
69
+ "@opsydyn/effect-three": "0.2.0",
70
+ "effect": "^4.0.0",
71
+ "foldkit": "0.167.0",
72
+ "three": ">=0.186.0 <0.187.0"
73
+ }
74
+ }
package/src/assets.ts ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Loading textures, glTF models and typeface fonts by URL, as Effects an application runs
3
+ * from a Foldkit Command and settles into `AsyncData`. Each asset is lent to the shared
4
+ * resource store under an id: materials use a texture with `map: id`, a `borrowed({ id })`
5
+ * node shows a model, and `text({ font: id })` geometry draws with a font. The environment owns what it loads: loading an id again replaces and disposes
6
+ * the earlier asset, and `unload` or the environment's `dispose` frees it.
7
+ *
8
+ * @module
9
+ */
10
+ import type { ResourceStore } from '@opsydyn/effect-three';
11
+ import { Data, Effect } from 'effect';
12
+ import {
13
+ EquirectangularReflectionMapping,
14
+ type Object3D,
15
+ SRGBColorSpace,
16
+ type Texture,
17
+ TextureLoader,
18
+ } from 'three';
19
+ import { type Font, FontLoader } from 'three/addons/loaders/FontLoader.js';
20
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
21
+ import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
22
+
23
+ import { disposeModel, namesIn } from './dom/models.ts';
24
+
25
+ /** An asset could not be fetched or decoded. */
26
+ export class AssetLoadError extends Data.TaggedError('AssetLoadError')<{
27
+ readonly url: string;
28
+ readonly cause: unknown;
29
+ }> {}
30
+
31
+ /** What a loaded model offers: named parts to match hits against, and animation clips. */
32
+ export type ModelInfo = {
33
+ readonly id: string;
34
+ /** Names of the objects inside the model, as hits report them in `part`. */
35
+ readonly parts: ReadonlyArray<string>;
36
+ /** Names of the model's animation clips. */
37
+ readonly clips: ReadonlyArray<string>;
38
+ };
39
+
40
+ /** Creates the asset loader for a store. One per environment. */
41
+ export const makeAssets = (store: ResourceStore) => {
42
+ const releases = new Map<string, () => void>();
43
+
44
+ /** Takes ownership of an asset under `id`, first releasing whatever was there. */
45
+ const own = (id: string, release: () => void): void => {
46
+ releases.get(id)?.();
47
+ releases.set(id, release);
48
+ };
49
+
50
+ const lendTexture = (id: string) => (texture: Texture) => {
51
+ texture.colorSpace = SRGBColorSpace;
52
+ const withdraw = store.lendTexture(id, texture);
53
+ own(id, () => {
54
+ withdraw();
55
+ texture.dispose();
56
+ });
57
+ return id;
58
+ };
59
+
60
+ const lendModel =
61
+ (id: string) =>
62
+ (gltf: { readonly scene: Object3D; readonly animations: Object3D['animations'] }) => {
63
+ // Keep the clips with the object, so a borrowed node can play them by name.
64
+ gltf.scene.animations = gltf.animations;
65
+ const withdraw = store.lend(id, gltf.scene);
66
+ own(id, () => {
67
+ withdraw();
68
+ disposeModel(gltf.scene);
69
+ });
70
+ const info: ModelInfo = {
71
+ id,
72
+ parts: namesIn(gltf.scene),
73
+ clips: gltf.animations.map((clip) => clip.name),
74
+ };
75
+ return info;
76
+ };
77
+
78
+ /** Lends an HDR panorama for `environment({ map })`: linear, mapped for reflection. */
79
+ const lendEnvironment = (id: string) => (texture: Texture) => {
80
+ texture.mapping = EquirectangularReflectionMapping;
81
+ const withdraw = store.lendTexture(id, texture);
82
+ own(id, () => {
83
+ withdraw();
84
+ texture.dispose();
85
+ });
86
+ return id;
87
+ };
88
+
89
+ const lendFont = (id: string) => (font: Font) => {
90
+ // Fonts hold no GPU resources: withdrawing the loan frees them.
91
+ own(id, store.lendFont(id, font));
92
+ return id;
93
+ };
94
+
95
+ const releaseAll = (): void => {
96
+ for (const release of releases.values()) release();
97
+ releases.clear();
98
+ };
99
+
100
+ return {
101
+ /** Loads a colour texture (sRGB) and lends it under `id`. Succeeds with the id. */
102
+ loadTexture: (id: string, url: string) => {
103
+ const load = Effect.tryPromise({
104
+ try: () => new TextureLoader().loadAsync(url),
105
+ catch: (cause) => new AssetLoadError({ url, cause }),
106
+ });
107
+ return Effect.map(load, lendTexture(id));
108
+ },
109
+ /** Loads a glTF or GLB model and lends its scene under `id`, with its clips. */
110
+ loadModel: (id: string, url: string) => {
111
+ const load = Effect.tryPromise({
112
+ try: () => new GLTFLoader().loadAsync(url),
113
+ catch: (cause) => new AssetLoadError({ url, cause }),
114
+ });
115
+ return Effect.map(load, lendModel(id));
116
+ },
117
+ /**
118
+ * Loads an equirectangular HDR panorama (`.hdr`, three.js's `HDRLoader`) and lends it
119
+ * under `id` for `environment({ map: id })`. Succeeds with the id. Image panoramas
120
+ * load with `loadTexture`; the scene maps them for reflection.
121
+ */
122
+ loadEnvironment: (id: string, url: string) => {
123
+ const load = Effect.tryPromise({
124
+ try: () => new HDRLoader().loadAsync(url),
125
+ catch: (cause) => new AssetLoadError({ url, cause }),
126
+ });
127
+ return Effect.map(load, lendEnvironment(id));
128
+ },
129
+ /**
130
+ * Loads a typeface JSON font (three.js's `FontLoader` format, as Drei's `Text3D` takes)
131
+ * and lends it under `id`. Succeeds with the id.
132
+ */
133
+ loadFont: (id: string, url: string) => {
134
+ const load = Effect.tryPromise({
135
+ try: () => new FontLoader().loadAsync(url),
136
+ catch: (cause) => new AssetLoadError({ url, cause }),
137
+ });
138
+ return Effect.map(load, lendFont(id));
139
+ },
140
+ /** Withdraws and disposes the asset loaded under `id`, if any. */
141
+ unload: (id: string) =>
142
+ Effect.sync(() => {
143
+ releases.get(id)?.();
144
+ releases.delete(id);
145
+ }),
146
+ /** Withdraws and disposes every loaded asset. */
147
+ dispose: releaseAll,
148
+ };
149
+ };
150
+
151
+ export type Assets = ReturnType<typeof makeAssets>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A `requestAnimationFrame` scheduler shared by every scene on the page: one browser
3
+ * callback per frame, running only while at least one scene is subscribed.
4
+ *
5
+ * @module
6
+ */
7
+ import type { FrameScheduler } from '@opsydyn/effect-three';
8
+
9
+ /** The part of `Window` the scheduler needs. */
10
+ export type AnimationFrameHost = Pick<Window, 'requestAnimationFrame' | 'cancelAnimationFrame'>;
11
+
12
+ /** Creates a shared animation-frame scheduler for `host` (normally `window`). */
13
+ export const animationFrameScheduler = (host: AnimationFrameHost): FrameScheduler => {
14
+ const callbacks = new Set<() => void>();
15
+ let handle: number | undefined;
16
+ const tick = (): void => {
17
+ handle = host.requestAnimationFrame(tick);
18
+ for (const onFrame of Array.from(callbacks)) onFrame();
19
+ };
20
+ return {
21
+ subscribe: (onFrame) => {
22
+ callbacks.add(onFrame);
23
+ handle ??= host.requestAnimationFrame(tick);
24
+ return () => {
25
+ callbacks.delete(onFrame);
26
+ if (callbacks.size > 0 || handle === undefined) return;
27
+ host.cancelAnimationFrame(handle);
28
+ handle = undefined;
29
+ };
30
+ },
31
+ };
32
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The canvas a scene draws into, and DOM event dispatch from scene events.
3
+ *
4
+ * @module
5
+ */
6
+ import type { Op, ResourceStore, SceneEvent } from '@opsydyn/effect-three';
7
+
8
+ import { domEventName, type SceneError } from '../element.ts';
9
+ import type { Latch } from './latch.ts';
10
+ import { summarise } from './reconciled.ts';
11
+
12
+ /** Creates a canvas filling `host` and appends it. */
13
+ export const appendCanvas = (host: Element): HTMLCanvasElement => {
14
+ const canvas = host.ownerDocument.createElement('canvas');
15
+ canvas.style.display = 'block';
16
+ canvas.style.width = '100%';
17
+ canvas.style.height = '100%';
18
+ // Pointer gestures go to the scene, not to page scrolling or zooming.
19
+ canvas.style.touchAction = 'none';
20
+ host.append(canvas);
21
+ return canvas;
22
+ };
23
+
24
+ /** Dispatches a scene event as the element's typed DOM event. */
25
+ export const dispatchSceneEvent = (element: Element, event: SceneEvent): void => {
26
+ element.dispatchEvent(new CustomEvent(domEventName[event._tag], { detail: event }));
27
+ };
28
+
29
+ /** Dispatches a rejected scene update as a `scene-error` event. */
30
+ export const dispatchSceneError = (element: Element, error: SceneError): void => {
31
+ element.dispatchEvent(new CustomEvent('scene-error', { detail: { error } }));
32
+ };
33
+
34
+ /** Thunk builders, so Effect code can run DOM work with `Effect.sync(thunk)`. */
35
+ export const canvasIn = (host: Element) => () => appendCanvas(host);
36
+ export const removal = (node: ChildNode) => () => node.remove();
37
+ /** Dispatches only while `live` is open: events are dropped during time-travel pause. */
38
+ export const sceneEventDispatch = (element: Element, live: Latch) => (event: SceneEvent) => () => {
39
+ if (live.isOpen()) dispatchSceneEvent(element, event);
40
+ };
41
+ export const sceneErrorDispatch = (element: Element) => (error: SceneError) => () =>
42
+ dispatchSceneError(element, error);
43
+
44
+ /**
45
+ * Dispatches a `scene-reconciled` summary when an update changed something. Updates with
46
+ * no operations dispatch nothing, so an app storing the summary in its Model can't loop.
47
+ */
48
+ export const reconciledDispatch =
49
+ (element: Element, store: ResourceStore) => (ops: ReadonlyArray<Op>) => () => {
50
+ if (ops.length === 0) return;
51
+ element.dispatchEvent(
52
+ new CustomEvent('scene-reconciled', { detail: summarise(ops, store.stats()) }),
53
+ );
54
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * A boolean read synchronously by DOM callbacks and written by Effect code.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ export type Latch = {
8
+ readonly isOpen: () => boolean;
9
+ readonly set: (open: boolean) => void;
10
+ };
11
+
12
+ export const createLatch = (open: boolean): Latch => {
13
+ let current = open;
14
+ return {
15
+ isOpen: () => current,
16
+ set: (next) => {
17
+ current = next;
18
+ },
19
+ };
20
+ };
21
+
22
+ const nothing: ReadonlyArray<never> = [];
23
+
24
+ /** Calls `route` only while `latch` is open, returning what it routed (nothing when shut). */
25
+ export const routedWhileOpen =
26
+ <A, B>(latch: Latch, route: (value: A) => ReadonlyArray<B>) =>
27
+ (value: A): ReadonlyArray<B> => {
28
+ if (!latch.isOpen()) return nothing;
29
+ return route(value);
30
+ };
31
+
32
+ /** A thunk calling `f(value)`, so Effect code can write `Effect.sync(applying(f, value))`. */
33
+ export const applying =
34
+ <A, B>(f: (value: A) => B, value: A) =>
35
+ (): B =>
36
+ f(value);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Plain three.js helpers for loaded models.
3
+ *
4
+ * @module
5
+ */
6
+ import { type Material, Mesh, type Object3D, Texture } from 'three';
7
+
8
+ /** Disposes every texture a material holds (maps of any kind). */
9
+ const disposeMaterial = (material: Material): void => {
10
+ for (const value of Object.values(material)) {
11
+ if (value instanceof Texture) value.dispose();
12
+ }
13
+ material.dispose();
14
+ };
15
+
16
+ /** Disposes a mesh's geometry and materials; other objects hold nothing to free. */
17
+ function disposeMesh(object: Object3D): void {
18
+ if (!(object instanceof Mesh)) return;
19
+ object.geometry.dispose();
20
+ const materials: ReadonlyArray<Material> = Array.isArray(object.material)
21
+ ? object.material
22
+ : [object.material];
23
+ for (const material of materials) disposeMaterial(material);
24
+ }
25
+
26
+ /** Disposes a loaded model's GPU resources: it was ours, not the application's. */
27
+ export const disposeModel = (root: Object3D): void => {
28
+ root.traverse(disposeMesh);
29
+ };
30
+
31
+ export const namesIn = (root: Object3D): ReadonlyArray<string> => {
32
+ const names: Array<string> = [];
33
+ root.traverse((object) => {
34
+ if (object !== root && object.name !== '') names.push(object.name);
35
+ });
36
+ return names;
37
+ };